【源码&库】 Vue3 的虚拟DOM生成规则

之前两章讲到组件更新,期间一直都绕不开的就是虚拟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);

可以看到的是验证结果和我们上面的定义是一致的:

这里的文本子节点和数组子节点的值是917,这里的值是由shapeFlag的值和TEXT_CHILDRENARRAY_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就是ELEMENT8就是TEXT_CHILDREN,所以最后的结果就是ELEMENT | TEXT_CHILDREN,也就是9

位运算

这样做有什么意义呢?其实阅读了这么长时间的源码,不难发现经常会出现这样的代码:

js 复制代码
if (shapeFlag & 8) {
    // ...
}

这里就是一个位运算,这样写无疑是增加了阅读的难度,但是对代码的性能以及一些逻辑上的判断是有帮助的;

还是我们刚才的例子,我们来看看ELEMENTTEXT_CHILDREN合并的值是9ELEMENTARRAY_CHILDREN合并的值是17

我们对它进行一个位运算,看看结果是什么:

  • ELEMENTTEXT_CHILDREN合并的值,与所以类型进行与运算,结果如下:
  • ELEMENTARRAY_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属性,还有很多其他的属性,例如我们传入的propschildrenslot等等;

这些属性在Vue的整个系统中又是如何使用的呢?这些将会在我们继续深入源码的过程中一一揭晓;

历史章节

相关推荐
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
苹果酱05676 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
web1309332039810 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
supermapsupport12 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
m0_7482548812 小时前
vue+elementui实现下拉表格多选+搜索+分页+回显+全选2.0
前端·vue.js·elementui
苹果醋314 小时前
Golang的文件加密工具
运维·vue.js·spring boot·nginx·课程设计
关你西红柿子14 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
济南小草根14 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
m0_7482565615 小时前
Vue - axios的使用
前端·javascript·vue.js
慢知行16 小时前
Vite 构建 Vue3 组件库之路:工程基础搭建与目录结构优化
前端·vue.js