详解Vue3的虚拟DOM是如何生成的

 更新时间:2023年09月11日 09:10:38   作者:田八  
这篇文章给大家详细介绍了 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);

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

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

位运算

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

if (shapeFlag & 8) {
    // ...
}

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

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

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

  • ELEMENTTEXT_CHILDREN合并的值,与所以类型进行与运算,结果如下:

  • ELEMENTARRAY_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属性,还有很多其他的属性,例如我们传入的propschildrenslot等等;

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

以上就是详解Vue3的虚拟DOM是如何生成的的详细内容,更多关于Vue3虚拟DOM生成的资料请关注脚本之家其它相关文章!

相关文章

  • 使用vue3实现一个人喵交流小程序

    使用vue3实现一个人喵交流小程序

    Vue3 在经过多个开发版本的迭代后,终于迎来了它的正式版本,下面这篇文章主要给大家介绍了关于如何使用vue3实现一个人喵交流小程序的相关资料,需要的朋友可以参考下
    2021-11-11
  • Vue.js中用webpack合并打包多个组件并实现按需加载

    Vue.js中用webpack合并打包多个组件并实现按需加载

    对于现在前端插件的频繁更新,我也是无力吐槽,但是既然入了前端的坑就得认嘛,所以多多少少要对组件化有点了解,下面这篇文章主要给大家介绍了在Vue.js中用webpack合并打包多个组件并实现按需加载的相关资料,需要的朋友可以参考下。
    2017-02-02
  • 基于Vue3实现旋转木马动画效果

    基于Vue3实现旋转木马动画效果

    这篇文章主要为大家介绍了如何利用Vue3实现旋转木马的动画效果,文中的示例代码讲解详细,对我们学习Vue有一定的帮助,需要的可以参考一下
    2022-05-05
  • Vue+Three加载glb文件报错问题及解决

    Vue+Three加载glb文件报错问题及解决

    当使用Three.js加载GLB模型时,遇到加载错误常常是路径问题,解决方案:1. 将GLB模型文件置于public目录,避免打包时路径编码变化;2. 从node_modules的three库中复制draco解码器至public目录;3. 确认场景、摄像机和光源设置正确
    2024-10-10
  • vue-router history模式下的微信分享小结

    vue-router history模式下的微信分享小结

    本篇文章主要介绍了vue-router history模式下的微信分享小结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-07-07
  • vue使用jsencrypt实现rsa前端加密的操作代码

    vue使用jsencrypt实现rsa前端加密的操作代码

    这篇文章主要介绍了vue使用jsencrypt实现rsa前端加密,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-09-09
  • vue弹窗嵌入其它vue页面的操作代码

    vue弹窗嵌入其它vue页面的操作代码

    这篇文章主要介绍了vue弹窗如何嵌入其它vue页面,实现方式是将其他页面作为组件传入,在父页面将该组件引入到弹框内,实例代码简单易懂需要的朋友可以参考下
    2022-11-11
  • Vue指令指令大全

    Vue指令指令大全

    本文为大家介绍了VUE中内置指令包括:v-text,v-html,v-show,v-if,v-else,v-else-if,v-for,v-on,v-bind,v-model,v-pre,v-cloak,v-once
    2019-02-02
  • Vue使用vm.$set()解决对象新增属性不能响应的问题

    Vue使用vm.$set()解决对象新增属性不能响应的问题

    这篇文章主要介绍了Vue使用vm.$set()解决对象新增属性不能响应的问题,为了解决这个问题,Vue提供了一个特殊的方法vm.$set(object, propertyName, value),也可以使用全局的Vue.set(object, propertyName, value)方法,需要的朋友可以参考下
    2023-05-05
  • Vue 无限滚动加载指令实现方法

    Vue 无限滚动加载指令实现方法

    这篇文章主要介绍了Vue 无限滚动加载指令的实现代码,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值 ,需要的朋友可以参考下
    2019-05-05

最新评论