learn vue3
vue 响应式驱动 ui,ui 通过 template 来描述
整个过程,依赖几个核心包 complier,runtime-dom,runtime-core,reactivity
- complier 编译 template 生成 render 函数
- runtime-dom vnode挂载到页面
- runtime-core 数据和dom 关联
- reactivity 实现响应式
vue -> runtime-dom -> runtime-core -> reactivity
-> compiler
complier 可以脱离vue运行时,好处是打包工具可以提前将 template 编译成 render 函数
- reactive 数据劫持
- ref 基础数据劫持
- computed 计算属性
- 维护一个dirty状态,判断是否使用缓存
- effect
- get 收集依赖
- set 触发依赖
- scheduler 处理依赖触发时机,比如异步更新
- stop 删除依赖
- 其他
- shallowReadonly 处理 props
数据和dom 关联
- setup
- 初始化数据方法
- render
- 创建虚拟dom
- 拿到setup的数据
- patch
- 基于不同的vnode 进行处理
- 数据更新后,会触发依赖,重新执行render,拿到新的vnode,然后进行patch
- diff 对比
- type 不同直接替换
- 对比儿子
- 双端算法,找到哪些儿子不一样
- 前后新增儿子
- 先后添加儿子
- 中间的儿子发生变化
- 删除 添加
- 移动
- 最长递增子序列,基于此进行移动
- 对比属性
- 删除 添加
- 处理dom 操作
- 将 runtime-core 的 vnode 转换成 dom, 挂载到页面
template => ast => transform ast => generate code => render
-
parse
- 状态机原理将 template 编译成 render 函数
- 遇到 < 处理元素节点
- 遇到 {{ 处理插值节点
- 其他情况 处理成文本节点
-
transform
- 遍历处理ast
- 处理文本 插值等
- 创建联合类型
-
generate
- 遍历ast
- 创建render 函数
const data = reactive({a:1})
let newData
effect(() => {
newData = data.a + 1
})
data => set trigger dep
=> get track dep
- effect 的参数(函数),立即执行
- get 的时候 track依赖
- set 的时候 trigger依赖
- effect 运行返回 runner
- 这个effect 和 watchEffect 有什么区别?
effect(fn, { scheduler })
- 初始化时 fn 会被调用
- 当响应式数据 set, trigger的时候 fn 不会被调用,scheduler 会被调用
- stop 的时候,会从依赖中删除当前 effect
- onStop: 当 stop 执行后,会执行 onStop,处理用户逻辑
- 只读,不能修改
- 使用场景:把响应式变成只读
- 没啥用
- isReadonly 判断是否是只读的
- isReactive 判断是否是响应式的
- stop 的时候,会从依赖中删除当前 effect
- 如果后续又有get操作,又会会重新track
性能角度考虑,有时候并不希望递归处理我们的数据,这里就需要用shallowReadonly 比如 处理 props
- 判断是否是代理对象 isReactive 和 isReadonly
- 为布尔、数字、字符串创建响应式数据
- 由于不是对象无法使用 proxy,所以在内容构造一个对象,赋值给value
- 注:边缘case,当修改的值和原值相等的时候,不会触发更新
- 计算属性
- 依赖的响应式数据发生变化,才会重新计算
- 计算属性也是个 ref
- .value 时才会去获取,执行effect
- 当属性没有发生变化时,.value 调用缓存
- 当计算属性发生改变,需要重新计算缓存,利用 scheduler 把 dirty 改为 true
createApp => createVNode => render => patch => processComponent => mountComponent => createComponentInstance => setupComponent => setupStatefulComponent => handleSetupResult => finishComponentSetup => setupRenderEffect => patch => processElement => mountElement => children => string => append to container => array => patch
yarn add @rollup/plugin-typescript@8.2.5 rollup@2.57.0 tslib@2.3.1
- type 判断是组件还是元素 如果是 div p 等元素字符串 则处理元素
- 元素属性 分为 type props children
- props 通过 setAttribute 处理
- children 则继续调用patch 处理
processElement => mountElement => create element => append to container => process props => setAttribute => process children => string => append to container => array => patch
render 方法需要访问 setup state
instance 挂载一个proxy属性,将proxy绑定到render函数,render 内部调用this,实际上是访问到 proxy,拦截get 基于 key 拿到 setup的state
- for components with a single root element, $el will point to that element
- 同样通过 proxy 拦截 key 为 $el 的 get 返回 root element
- 标记当前元素是组件还是元素
- 标记当前元素儿子是文本还是数组
- 通过二进制的方式来描述类型
- 复合类型用一个值来表示
- 使用起来更高效
- 但是可读性更低
| 表示都是0才为0 & 表示都是1才为1
组件 元素 儿子文本 儿子数组 0101 表示元素和儿子数据 是否是元素 0101 & 0100 = 0100 true
检测 props 的参数 on + Event 比如onClick 转换成 window.addEventListener('click', () => {}) onClick => Click => click
- 创建 instance 的时候,将 props 挂载到 instance.props
- 调用 setup 时,将props作为参数传入
- props,shadow readonly 不能被修改,如果被修改报错提醒
- 在 render 方法可以直接通过this 访问props属性 这里需要通过 instance.proxy 代理
- 儿子调用父组件的自定义事件
- emit 挂载到 instance, 通过 props 拿到父组件定义的自定义事件
- 在执行 setup 时候传入 emit
- 事件名处理 onAdd -> add onAddFoo -> add-foo
- 具名插销,没有名字的插销默认default名字
- 插槽作用域
- 创建虚拟节点:定义插槽类型,如果是组件且children是对象
- 初始化插槽:将插槽对象挂载到instance.slots
- 调用renderSlot方法,将插槽对象通过代理获取到,然后作为参数传入
增加 TEXT 类型,CHILDREN_TEXT 其实就没有用了
- 创建虚拟节点 避免用div包裹
getCurrentInstance 在 setup 中调用,所以在 setup 执行前,赋值 currentInstance 当setup 执行完,currentInstance 置为null
- 父组件 provider 子组件 inject,inject 取不到可以设置默认值
- 子组件先找父亲,再找爷爷,再找曾爷爷,直到找到为止,类似于js原型链的,可以基于此实现跨层级读取
- 设计很巧妙
vue3 高度模块化,编译 运行时 dom渲染 都进行了分离。可以将dom 渲染替换成自定义的 比如可以把dom渲染替换成渲染到canvas,用 PixiJS 这个库来操作canvas的渲染
- 创建虚拟dom =》 创建真实节点
- 更新数据 =》 更新虚拟dom
- 对比新旧虚拟dom =》 update
利用 effect 收集虚拟dom创建方法,数据变化后update
- 原来的props.key 的值被修改了 =》 触发update
- 原来的props.key 的值被设置为了undefind =》 触发remove
- 原来的props.key 的key不存在了 =》 触发remove
有四种情况
- 原来的children 是 text,新children 是 array
- 选清空text 然后 mountChildren
- 原来的children 是 array,新children 是 text
- unmountChildren 然后 setText
- 原来的children 是 text,新children 是 text
- 直接setText
- 原来的children 是 array,新children 是 array
- 两端对比算法
- 基于场景来选择算法
- 基于children 经常被修改的场景,采用双指针算法
- 通常是某个部分被修改了,两端对比可以快速找到修改的部分,再后续的循环遍历中 只针对这部分进而提升算法效率
- 场景:
- 头部添加更新删除
- 尾部添加更新删除
- 中间添加更新删除
- 找到中间修改的区域
- 删除
- 添加
- 最长递增子序列,来控制dom移动
三个指针 prevchildren: a b c e1
nextchildren: a b i e2
ab abc
i=0 e1=1 e2=2 i=1 e1=1 e2=2 i=2 e1=1 e2=2
i > e1
ab cab
i=0 e1=1 e2=2 i=0 e1=0 e2=1 i=0 e1=-1 e2=0
i > e1
abc ab
i=0 e1=2 e2=1 i=1 e1=2 e2=1 i=2 e1=2 e2=1
i <= e1
abc bc
i=0 e1 = 2 e2 = 1 i=0 e1 = 1 e2 = 0 i=0 e1 = 0 e2 = -1
i <= e1
a,b,(c,d,e,z),f,g a,b,(d,c,y,e),f,g
找到中间不同的区域 老的 s1 e1 0 3 新的 s2 e2 0 3
- 删除不存在的
- 新的存入map,key -> index
- 遍历老的,如果map中没有,则删除
- 存在则patch,判断children props的区别
- 从后遍历新的
- 添加
- 移动
newIndexToOldIndexMap=[4,3,0,5] 最长递归子序列下标 3 5 => [1, 3]
update component => trigger effect => newSubTree 和 oldSubTree 进行patch
- effect 返回的 run 方法存到instance.update
- componentUpdate trigger instance.update, 修改 instance 的props,生成新的 subTree
- patch 对比 newSubTree 和 oldSubTree
- 业务场景中,可能需要多次修改data, 会触发多次更新,造成性能问题
- 对于页面的呈现,我们只需要等data修改完毕后再进行更新,不管修改多少次
- 同步代码结束后,在微任务中执行更新,实现异步更新
字符串 -》 render函数
字符串很难进行遍历以及简历关系,所以需要将字符串转换成语法树, 然后再通过语法树转换成render函数
- 解析模板,生成ast语法树
- transform 遍历ast语法树,对语法树进行改造,得到新的ast语法树
- generate 遍历ast语法树,生成render函数
例子:
<div>hi,{{message}}</div>
状态机 --- 遇到<符号 --> parseElement ----> parseTag -----> 状态机
----遇到\{\{符号----> parseInterpolation ----> 状态机
---- 遇到非其他两种情况 ---> parseText -------> 状态机
root
- element
- children
- text
- parseInterpolation
{{message}}
<div></div>
message
- 遍历节点
- 通过插件机制去修改ast语法树
Hello World
=>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return "Hello World"
}
compiler-sfc
| |
v v
vue -> compiler-dom -> complier-core
-> runtime-dom -> runtime-core -> reactivity
好处
- 共用配置,多个项目不用重复配置
- 项目之间调用更方便
- 版本管理更方便,某一个版本升级,不用每一个库都升级
- build 发版 更方便
缺点
- 项目权限管理不能独立控制某个库
- 软链接节约磁盘空间
- 非扁平结构,nodemodule 展示更清晰, 也可以规避某些包冲突问题
- create pnpm-workspace.yaml
- 每个包都需要一个 package.json pnpm init -y
- 执行项目安装 pnpm i @miao-vue/shared --filter @miao-vue/reactivity
- 修改tsconfig.json, paths
- 根目录安装 pnpm i vitest -D -w
- 在dom挂载前调用,effect 与 dom 无关
- 依赖发生变化会调用 watchEffect
- onCleanup 会在 watchEffect 之前调用, 初始化不调用
- stop 停止监听,调用 onCleanup