手写 mini-vue3¶
Intro¶
vue3 源码结构¶
- runtime
- compiler
- reactivity
runtime 与 reactive¶
- 如果熟悉渲染函数,可以直接用 runtime 和 reactive 写应用;
template
与渲染函数¶
template
,即将模板编译成 渲染函数。
Reactive¶
effect & reactive¶
- reactive与effect是怎样建立联系的呢?
- 依赖收集
track
: 保存副作用与它的依赖关系; - 触发更新
trigger
:当依赖变更时,找到并执行依赖于它的副作用。 -
执行顺序
- 1、执行副作用函数
- 2、执行过程中发现依赖
- 3、 在 get 中,对响应式对象的依赖收集
- 4、 在set中,响应式对象发生变化时触发更新
targetMap¶
- 用于 存储副作用,并建立副作用和依赖的对应关系。
- 一个副作用可能依赖多个响应式对象,一个响应式对象可能有多个属性,一个属性又可能被多个副作用依赖。
- 因此,数据结构设计:
// 一个 new WeakMap
{
// key 是 reactiveObject, value 是 Map
[target]: {
// key 是 reactiveObject 的 key, value 是 Set
[key]: []
}
}
-
WeakMap 里,当 reactiveObject 不再使用,GC 可以自动回收。
特殊问题处理¶
-
1) reactive(reactive(obj))
isReactive(target)
判断是否被代理过:若是,则返回 target。
-
2) a = reactive(obj); b = reactive(obj);
proxyMap
缓存 obj 的代理- 同一对象,不重复代理。
-
3) hasChanged
-
4) 深层对象代理
- 延迟(懒)递归:get 触发时,才开始往下递归。
return isObject(result) ? reactive(result) : result
-
5) 数组
if (oldLength !== target.length)
- 手动
trigger(target, 'length');
-
6) 嵌套 effect
- 问题
effect(() => { effect(() => { console.log('observed.b is', observed.b); }); console.log('observed.a is', observed.a); }); // observed.a 更新,无法触发effect
- 解决: effectStack 存放当前执行的副作用
ref¶
- class RefImpl {}
- toReactive
computed¶
- class ComputedImpl {}
this._dirty
: 记录依赖是否被更新lazy
: 懒执行,第一次 effectFn() 不立即执行scheduler
: 如果存在,scheduler() 优先执行;否则执行 effectFn()。
Runtime¶
VNode¶
-
种类
- Element: 普通元素,document.createElement
- Text: 文本结点,document.createTextNode
- Fragment: 不会被真实渲染的结点,children 会挂在其父节点上
- Component: 组件
-
ShapeFlags
- 一组标记,用于快速辨识 VNode 类型和 children 类型。
- 值
const enum ShapeFlags { ELEMENT = 1, // 普通元素 FUNCTIONAL_COMPONENT = 1 << 1, // 函数组件 STATEFUL_COMPONENT = 1 << 2, // 状态组件 TEXT_CHILDREN = 1 << 3, // 文本子节点 ARRAY_CHILDREN = 1 << 4, // 数组子节点 SLOTS_CHILDREN = 1 << 5, // 插槽子节点 TELEPORT = 1 << 6, // 传送组件 SUSPENSE = 1 << 7, // 悬念组件 }
-
h 函数(渲染函数)
- 模板更接近编译器
- 参数:标签名、prop、包含其子节点的数组
mount¶
- render
- mount(vnode, container)
patch¶
-
流程图
- 在 render 中,判断 n2 是否存在:若存在,则需要进行 patch;不存在的话,则原先的 n1 需要卸载。
- 在 patch 中,n1 和 n2 类型相同,可以直接进行 process;不相同,则意味着节点整个发生变化,需要先将 n1 卸载,再挂载 n2。
- 在 processXXX 中,若 n1 存在,则可以复用 n1 的节点,只需要更新内容,而不需要重新创建新的节点;而如果不存在,需要重新挂载。
-
-
- 两个不同类型的元素会产生出不同的树;
-
- 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定;
-
-
patchChildren
- 3 * 3 种情况: Text, Array, null
- patchArrayChildren: c1、c2 都是数组
-
VNode.anchor
- anchor 是 Fragment 专有属性。
- 子元素根据anchor来确定在dom中的位置。
- Fragment 首尾各 upsert 一个空 TextNode 作为 anchor。
-
Patch流程细节
Component¶
- prop & attr
- normalizeVNode
-
主动更新
- 场景:内部状态改变,导致组件重新渲染,称为主动更新。
mountComponent -> instance.update -> effect(render)
-
被动更新
- 场景:父组件传入的 props 值改变,导致子组件重新渲染,称为子组件的被动更新。
- 调用 updateComponent
Scheduler¶
延迟 render¶
- 针对 Component 的render 方法
- 同步函数对所有依赖计算完成之后,再执行 render。
- 复用 computed 方法的懒计算机制(即 scheduler 属性)
- queue 保存去重之后的 job,然后
Promise.resolve().then(flushJobs)
nextTick¶
- 始终等待当前任务队列 flush 之后,放入浏览器的微任务队列,等待下一次 Event-Loop 执行。
Compiler¶
编译步骤¶
模版代码 --parse--> AST --transform--> AST + codegenNode --codegen--> 渲染函数代码
- parse: 原始的模版字符串,通过
parse
转换成 原始 AST 抽象语法树。 -
transform: AST 经过
transform
生成 codegenNode。- codegenNode 是 AST 转化为渲染函数的中间代码,解析原始 AST 语义而来。
transform
的作用在于,处理像v-if
这类带有特殊语义的语法节点。
-
codegen: 遍历 codegenNode,递归生成渲染函数代码。
parse 的实现¶
-
什么是 AST?
- 对于模版字符串
<div id="foo" v-if="ok">Hello {{name}}</div>
- 依次有:元素节点、属性节点、指令节点、文本节点、插值节点
- 对于模版字符串
-
AST 节点类型
const NodeTypes = {
ROOT: 'ROOT',
ELEMENT: 'ELEMENT',
TEXT: 'TEXT',
SIMPLE_EXPRESSION: 'SIMPLE_EXPRESSION',
INTERPOLATION: 'INTERPOLATION',
ATTRIBUTE: 'ATTRIBUTE',
DIRECTIVE: 'DIRECTIVE',
};
- Whitespace 优化
codegen¶
- tranverseNode/tranversechildren
- createTextVNode/createInterpolationVNode/createElementVNode
Directives¶
-
类型
- 属性指令:
v-on、v-bind、v-html
- 结构指令:
v-if、v-for
;会改变 DOM 的结构。 - v-model
- 属性指令:
-
v-for
- 依赖 renderList() 方法
- 模版语法
<div v-for="(item, index) initems ">{{item + index)}}</div>
- 渲染函数
h( Fragment, null, renderList(items, (item, index) => h(tag, null, item + index)) )
-
v-if
- 模版语法
<div v-if="ok"></div>
- 渲染函数
ok ? h('div') : h(Text, null, '')
-
v-model
- 本质上一个语法糖
- 模版语法
<input v-mode="name" /> <!--本质上是--> <input :value="name" @input="name = $event.target.value" />
组件的引入¶
- 组件引入方法:在
createApp()
里声明components
createApp({
compoents: { Foo }
});
- 在
runtime
里配合resolveComponent
使用