之前两章讲到组件更新,期间一直都绕不开的就是虚拟DOM
,这一章我们就来看看虚拟DOM
是如何生成的。
在之前的两章中,我们编写的demo
都会使用到h
函数,这个函数就是用来生成虚拟DOM
的,我们来看看h
函数的定义:
现在我们就来深入h
函数的实现,看看它是如何生成虚拟DOM
的。
h 函数
在官网上可以看到对h
函数的介绍和函数签名;
可以先去看看官网的介绍,然后再来看看源码的实现,传送门:
h 函数的实现
还是跟着我们之前的节奏,可以直接在h函数
调用上面打上断点,然后开始调试进入源码:
js
const {h} = Vue;
debugger;
h('div');
直接就这样进入了h
函数的实现,我们来看看h
函数的实现:
js
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
函数如下:
js
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
的源码中有他们的定义:
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
来看看:
js
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
的一些属性,我们拿文本子节点来做示例看看运行逻辑(删除不会执行的逻辑的简化版代码):
js
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
函数的实现:
js
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
;
位运算
这样做有什么意义呢?其实阅读了这么长时间的源码,不难发现经常会出现这样的代码:
js
if (shapeFlag & 8) {
// ...
}
这里就是一个位运算
,这样写无疑是增加了阅读的难度,但是对代码的性能以及一些逻辑上的判断是有帮助的;
还是我们刚才的例子,我们来看看ELEMENT
和TEXT_CHILDREN
合并的值是9
,ELEMENT
和ARRAY_CHILDREN
合并的值是17
;
我们对它进行一个位运算
,看看结果是什么:
ELEMENT
和TEXT_CHILDREN
合并的值,与所以类型进行与运算
,结果如下:
ELEMENT
和ARRAY_CHILDREN
合并的值,与所有类型进行与运算
,结果如下:
可以看到合并后的值,只会与参与合并的值
进行与运算
得到的结果是参与合并的值
,这样就可以通过与运算
来判断shapeFlag
的值是否包含某个类型;
而将这个过程进行二进制
来描述,就是这样的:
log
# 这是 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 学习前端模块化
- 【源码&库】在调用 createApp 时,Vue 为我们做了那些工作?
- 【源码&库】细数 Vue3 的实例方法和属性背后的故事
- 【源码&库】Vue3 中的 nextTick 魔法背后的原理
- 【源码&库】Vue3 的响应式核心 reactive 和 effect 实现原理以及源码分析
- 【源码&库】跟着 Vue3 的源码学习 reactive 背后的实现原理
- 【源码&库】 Vue3 的依赖收集,这里的依赖指代的是什么?
- 【源码&库】 Vue3 的依赖收集和依赖触发是如何工作的
- 【源码&库】 Vue3 的组件是如何挂载的?
- 【源码&库】 Vue3 的组件是如何更新的?
- 【源码&库】 Vue3 的组件更新核心算法