Skip to content

AHABHGK

VDOM runtime 源码入门

FE Tutorial, Front End Framework1 min read

Table of Contents

1. 核心部分小于 200 行

去掉 key diff 后真的很简单

2. 框架原理入门的入门

Go deeper 部分就是框架原理入门

3. 学习框架原理的起点

Go deeper 链接中的链接中的链接……

VNode

1[Virtual Node]
2--------------
3[DOM Node]

用来找到两个帧之间不同的地方(diff),仅对不同的地方进行实际的 DOM 更新(patch),减少与 DOM 通信、回流重回等性能消耗

于是需要在真是的 DOM Node 上建立一层 VNode,用以描述帧的 DOM 结构

就是普通的 JS 对象,存在内存中,下一帧的 VNode 和当前帧的 VNode 进行比对,找到需要更新的地方,然后做相应处理

Vue 是一边 diff 一边 patch;React16 是先 diff 然后对要更新的地方做标记(render phase),之后在对标记过的地方进行 patch(commit phase)。实现的不同也决定了一些 feature 的不同

前两部分先不考虑组件,仅对应 DOM 结构的两种 VNode,标签(div、span……)和文本(#text)进行处理(对应 DOM 中 Element、Text、Comment 都继承于 Node 类型)

1{
2 type,
3 props,
4 key, // key diff 用的
5 node, // 宿主环境的元素(dom node……)
6 parent, // parent VNode
7 children, // VNode[],建立内部 VNode 树结构
8}

Diff

1div diff
2 / \ diffChildren
3 div div diff diff
4 | | diffChildren diffChildren
5hello world diff diff

diff

  1. 只对同级元素进行 diff(diffChildren)
  2. 两个不同类型的元素会产生出不同的树(isSameVNodeType)
  3. 开发者可以通过 key 来暗示哪些子元素在不同的渲染下能保持稳定(key diff)

key diff 位于 diffChildren 这里,是 diff 算法最难的地方,代码中我们简化了很多,详细的可以看 Go deeper 链接中的链接

Vue 使用的是递归式的 diff,React16 使用的是迭代式的 diff,区别在于递归式的不可打断,迭代式的可以打断,这也决定了两个框架 feature 的不同 代码中因为参考的是 Vue3 的源码,边 diff 边 patch,所以命名是 patch,但实际上做的是 diff + patch

Component

现在我们加入 Component 的 diff

1const hooksComponent = {
2 patch(internals, { n1, n2, container }) {
3 if (n1 == null) {
4 const instance = n2.instance = {
5 subTree: null,
6 vnode: n2,
7 update: () => {
8 // this: extends hooksComponent
9 const renderResult = this.render(instance.vnode.props) // 获取 render 的结果
10
11 instance.vnode.children = [renderResult] // 不考虑 Fragment,组件的子节点只有一个
12 renderResult.parent = instance.vnode // 建立父子联系
13 internals.patch(instance.subTree, renderResult, container) // 跟上一次结果进行 diff
14 instance.subTree = renderResult // 存储这次结果,供下一次 patch 用
15 },
16 }
17 } else {
18 const instance = n2.instance = n1.instance
19 instance.vnode = n2
20 }
21 n2.instance.update()
22 },
23
24 unmount(internals, { vnode, doRemove }) {
25 vnode.children.forEach(c => unmount(c, doRemove))
26 },
27
28 getNode(internals, { vnode }) {
29 return vnode.instance.subTree.node
30 }
31}
32
33export const createHooksComponent = (render) => ({ ...hooksComponent, render }) // “继承“ patch、unmount 等方法

现在的组件的更新是同步的,就是说比如一个 input,输入多少次就触发了多少次 diff,可以通过异步进行优化

1enqueueRender(n2.instance.update)

State

组件的 State 我们通过 Hooks “钩”入

1const instance = n2.instance = {
2 // ...
3 hooks: {
4 list: [],
5 pendingEffects: [],
6 },
7 update: () => {
8 currentHooksComponent = instance
9 currentIndex = 0
10 // this: extends hooksComponent
11 const renderResult = this.render(instance.vnode.props)
12 currentHooksComponent = null
13 // ...
14 invokePendingEffects(instance)
15 },
16}
17
18function getHookState(index) {
19 const { hooks } = currentHooksComponent
20 if (index >= hooks.list.length) {
21 hooks.list.push({});
22 }
23 return hooks.list[index];
24}
25
26export function useEffect(effect, args) {
27 const hookState = getHookState(currentIndex++);
28 if (argsChanged(hookState.args, args)) {
29 hookState.effect = effect;
30 hookState.args = args;
31 currentHooksComponent.hooks.pendingEffects.push(hookState);
32 }
33}

两类 Reactivity 的处理方式:一种是以 Vue 为代表的 mutable + change tracking。即可变的数据结构,配合变更追踪,触发更新函数。另一种是以 React 为代表的 immutability + referential equality testing。即不可变的数据结构,配合反复执行的渲染函数,以及在函数执行过程中,通过数据的引用相等性判断,找出变更部分,只应用变化的部分到 UI 上。—— 工业聚:打破框架的范式之争

Go deeper

👉 源码在这里