VDOM runtime 源码入门
— FE Tutorial, Front End Framework — 1 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 VNode7 children, // VNode[],建立内部 VNode 树结构8}
Diff
1div diff2 / \ diffChildren3 div div diff diff4 | | diffChildren diffChildren5hello world diff diff
- 只对同级元素进行 diff(diffChildren)
- 两个不同类型的元素会产生出不同的树(isSameVNodeType)
- 开发者可以通过 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 hooksComponent9 const renderResult = this.render(instance.vnode.props) // 获取 render 的结果1011 instance.vnode.children = [renderResult] // 不考虑 Fragment,组件的子节点只有一个12 renderResult.parent = instance.vnode // 建立父子联系13 internals.patch(instance.subTree, renderResult, container) // 跟上一次结果进行 diff14 instance.subTree = renderResult // 存储这次结果,供下一次 patch 用15 },16 }17 } else {18 const instance = n2.instance = n1.instance19 instance.vnode = n220 }21 n2.instance.update()22 },2324 unmount(internals, { vnode, doRemove }) {25 vnode.children.forEach(c => unmount(c, doRemove))26 },2728 getNode(internals, { vnode }) {29 return vnode.instance.subTree.node30 }31}3233export 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 = instance9 currentIndex = 010 // this: extends hooksComponent11 const renderResult = this.render(instance.vnode.props)12 currentHooksComponent = null13 // ...14 invokePendingEffects(instance)15 },16}1718function getHookState(index) {19 const { hooks } = currentHooksComponent20 if (index >= hooks.list.length) {21 hooks.list.push({});22 }23 return hooks.list[index];24}2526export 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 上。—— 工业聚:打破框架的范式之争