详解Vue3的虚拟DOM是如何生成的
作者:田八
h 函数
在官网上可以看到对h
函数的介绍和函数签名;
可以先去看看官网的介绍,然后再来看看源码的实现,传送门:
h 函数的实现
还是跟着我们之前的节奏,可以直接在h函数
调用上面打上断点,然后开始调试进入源码:
const {h} = Vue; debugger; h('div');
直接就这样进入了h
函数的实现,我们来看看h
函数的实现:
function h(type, propsOrChildren, children) { // 通过参数数量来进行重载 const l = arguments.length; // 如果参数数量为2,那么就有两种情况 if (l === 2) { // 如果第二个参数是对象,并且不是数组 if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { // 如果第二个参数是虚拟dom,那么就将第二个参数作为子节点进行处理 if (isVNode(propsOrChildren)) { return createVNode(type, null, [propsOrChildren]); } // 如果第二个参数是对象,那么就将第二个参数作为props进行处理 return createVNode(type, propsOrChildren); } else { // 如果第二个参数是数组,那么就将第二个参数作为子节点进行处理 return createVNode(type, null, propsOrChildren); } } else { // 如果参数数量不是2 if (l > 3) { // 并且参数数量大于3,那么就将第三个参数以及后面的参数作为子节点进行处理 children = Array.prototype.slice.call(arguments, 2); } else if (l === 3 && isVNode(children)) { // 如果参数数量等于3,并且第三个参数是虚拟dom,那么就将第三个参数作为子节点进行处理 children = [children]; } // 最后将第二个参数作为props,其余的参数作为子节点进行处理 return createVNode(type, propsOrChildren, children); } }
h
函数就是一个重载函数,根据参数的不同,会有不同的处理逻辑,其实没有什么好看的;
它最后将所有的参数都传递给了createVNode
函数,也就是核心是createVNode
函数;
createVNode 函数
由于我们上面的示例代码中,只传入了一个参数,所以会跳过很多逻辑,简化后的createVNode
函数如下:
function _createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) { // 获取 shapeFlag const shapeFlag = isString(type) ? 1 : isSuspense(type) ? 128 : isTeleport(type) ? 64 : isObject(type) ? 4 : isFunction(type) ? 2 : 0; // 如果是一个组件,并且还被设置成响应式的了,则会提示并解包 if (shapeFlag & 4 && isProxy(type)) { type = toRaw(type); warn( `Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with \`markRaw\` or using \`shallowRef\` instead of \`ref\`.`, ` Component that was made reactive: `, type ); } // 最后调用 createBaseVNode 创建 VNode return createBaseVNode( type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true ); }
这里主要是获取了shapeFlag
,我们上面传入了一个字符串的div
,所以shapeFlag
的值为1
;
这里的shapeFlag
其实是一个二进制的值,它的值是由type
的类型来决定的,在ts
的源码中有他们的定义:
// packages\shared\src\shapeFlags.ts export const enum ShapeFlags { ELEMENT = 1, // 普通dom元素 二进制:0000 0001 十进制:1 FUNCTIONAL_COMPONENT = 1 << 1, // 函数组件 二进制:0000 0010 十进制:2 STATEFUL_COMPONENT = 1 << 2, // 有状态组件 二进制:0000 0100 十进制:4 TEXT_CHILDREN = 1 << 3, // 文本子节点 二进制:0000 1000 十进制:8 ARRAY_CHILDREN = 1 << 4, // 数组子节点 二进制:0001 0000 十进制:16 SLOTS_CHILDREN = 1 << 5, // 插槽 二进制:0010 0000 十进制:32 TELEPORT = 1 << 6, // TELEPORT组件 二进制:0100 0000 十进制:64 SUSPENSE = 1 << 7, // SUSPENSE组件 二进制:1000 0000 十进制:128 COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 没弄清 二进制:0001 0000 0000 十进制:256 COMPONENT_KEPT_ALIVE = 1 << 9, // 没弄清 二进制:0010 0000 0000 十进制:512 COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 普通组件,应该是有状态组件和函数组件的并集 }
这里我们可以验证一下这些值,写个demo
来看看:
const {h} = Vue; // 普通元素 const element = h('div'); console.log('ELEMENT', element.shapeFlag); // 函数式组件 const functionalComponent = h(() => h('div')); console.log('FUNCTIONAL_COMPONENT', functionalComponent.shapeFlag); // 有状态组件 const statefulComponent = h({ render() { return h('div'); } }); console.log('STATEFUL_COMPONENT', statefulComponent.shapeFlag); // 文本子节点 const textChildren = h('div', 'text'); console.log('TEXT_CHILDREN', textChildren.shapeFlag); // 数组子节点 const arrayChildren = h('div', [h('span'), h('span')]); console.log('ARRAY_CHILDREN', arrayChildren.shapeFlag); // 插槽子节点 const slotsChildren = h({ render() { return h('div', this.$slots.default()); } }, null, () => 'slotChildren'); console.log('SLOTS_CHILDREN', slotsChildren.shapeFlag); // teleport组件 const teleport = h(Vue.Teleport); console.log('TELEPORT', teleport.shapeFlag); // suspense组件 const suspense = h(Vue.Suspense); console.log('SUSPENSE', suspense.shapeFlag);
可以看到的是验证结果和我们上面的定义是一致的:
这里的文本子节点和数组子节点的值是9
和17
,这里的值是由shapeFlag
的值和TEXT_CHILDREN
和ARRAY_CHILDREN
的值进行或运算得到,这就要进入到createBaseVNode
函数中去看看了。
createBaseVNode 函数
这里的createBaseVNode
函数就是定义了VNode
的一些属性,我们拿文本子节点来做示例看看运行逻辑(删除不会执行的逻辑的简化版代码):
function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1, isBlockNode = false, needFullChildrenNormalization = false) { // 定义 vnode const vnode = { __v_isVNode: true, __v_skip: true, type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, slotScopeIds: null, children, component: null, suspense: null, ssContent: null, ssFallback: null, dirs: null, transition: null, el: null, anchor: null, target: null, targetAnchor: null, staticCount: 0, shapeFlag, patchFlag, dynamicProps, dynamicChildren: null, appContext: null, ctx: currentRenderingInstance }; // 普通节点固定走这个分支 if (needFullChildrenNormalization) { // 使用 normalizeChildren 处理 children normalizeChildren(vnode, children); } // 最后返回 vnode return vnode; }
这里的代码并不复杂,就是定义了vnode
,然后对children
进行了处理,最后返回了vnode
;
我们当前测试的文本子节点,shapeFlag
的值为9
,这里就是通过normalizeChildren
函数来处理的,我们来看看normalizeChildren
函数的实现:
function normalizeChildren(vnode, children) { let type = 0; const { shapeFlag } = vnode; if (children == null) { // ... } else if (isArray(children)) { // ... } else if (typeof children === "object") { // ... } else if (isFunction(children)) { // ... } else { // 走到这里,说明 children 需要被规范为文本节点 // 直接转为字符串 children = String(children); // 如果是 teleport ,子节点会被标记为 16,也就是数组节点 if (shapeFlag & 64) { type = 16; // 这里会将 children 转为数组 children = [createTextVNode(children)]; } else { // 如果是普通节点,直接标记为文本节点,也就是 8 type = 8; } } // 最后将 children 赋值给 vnode.children vnode.children = children; // 然后将 type 的值进行或运算,赋值给 vnode.shapeFlag vnode.shapeFlag |= type; }
可以看到这里写了一堆条件分支,来判断不同的子节点类型,最后将children
赋值给vnode.children
,然后将type
的值进行或运算,赋值给vnode.shapeFlag
;
或运算
会得到什么结果呢?其实我们完全可以自己尝试一下:
1 | 8
的结果是9
,这里的1
就是ELEMENT
,8
就是TEXT_CHILDREN
,所以最后的结果就是ELEMENT | TEXT_CHILDREN
,也就是9
;
位运算
这样做有什么意义呢?其实阅读了这么长时间的源码,不难发现经常会出现这样的代码:
if (shapeFlag & 8) { // ... }
这里就是一个位运算
,这样写无疑是增加了阅读的难度,但是对代码的性能以及一些逻辑上的判断是有帮助的;
还是我们刚才的例子,我们来看看ELEMENT
和TEXT_CHILDREN
合并的值是9
,ELEMENT
和ARRAY_CHILDREN
合并的值是17
;
我们对它进行一个位运算
,看看结果是什么:
ELEMENT
和TEXT_CHILDREN
合并的值,与所以类型进行与运算
,结果如下:
ELEMENT
和ARRAY_CHILDREN
合并的值,与所有类型进行与运算
,结果如下:
可以看到合并后的值,只会与参与合并的值
进行与运算
得到的结果是参与合并的值
,这样就可以通过与运算
来判断shapeFlag
的值是否包含某个类型;
而将这个过程进行二进制
来描述,就是这样的:
# 这是 ELEMENT 和 TEXT_CHILDREN 合并的值 0000 1001 # 这是 ELEMENT 的值 0000 0001 # 进行与运算 0000 1001 &&&& &&&& 0000 0001 = = = = = 0000 0001
通过上面的例子,其实与运算
就是将两个值的二进制中的相同位置的值进行比较,如果都是1
,那么结果就是1
,否则就是0
;
而Vue
将每个节点的类型都定义成了2的n次方
,这样就可以避免会出现相同位置的1
,这样在进行或运算
的时候,就可以将所有的类型进行合并,从而产生一个新的值;
如果是相同类型的节点,那么shapeFlag
的值就是相同的,在进行或运算
的时候会得到相同的值,新值和原来的值是相同的,因为本身就包含了这个类型;
这样新值就会包含所有参与合并的值的类型,就可以通过与运算
来判断shapeFlag
的值是否包含某个类型,设计非常的巧妙;
总结
这一篇主要学习了vnode
的擦创建过程,其实一个vnode
就是一个js对象
,本身并没有什么特殊的;
特殊的是这个vnode
自带的属性,例如这一章详细介绍的sahpeFlag
,这个属性就是通过位运算
来进行合并的,这样就可以通过与运算
来判断shapeFlag
的值是否包含某个类型;
而一个vnode
中并不是只有一个shapeFlag
属性,还有很多其他的属性,例如我们传入的props
、children
、slot
等等;
这些属性在Vue
的整个系统中又是如何使用的呢?这些将会在我们继续深入源码的过程中一一揭晓;
以上就是详解Vue3的虚拟DOM是如何生成的的详细内容,更多关于Vue3虚拟DOM生成的资料请关注脚本之家其它相关文章!