Vue源码速读 | 第二章:深入理解Vue虚拟DOM:从vnode创建到渲染

虚拟DOM是现代化前端开发框架基本必备的功能,所以到底什么是虚拟DOM,为什么我们要使用虚拟DOM呢?

下面我们将从VUE创建应用实例APP的vnode来学习vue中的虚拟DOM。

二.vue中的虚拟dom--vnode

1. 什么是虚拟DOM

Vue 中的虚拟 DOM(Virtual DOM)是 Vue 框架的一个核心概念。虚拟 DOM 是一个轻量级的、内存中的 DOM 表示形式,它可以帮助 Vue 快速地更新和渲染 DOM。

为什么需要虚拟 DOM?

在传统的 DOM 操作中,每次更新 DOM 都需要手动操作 DOM 元素,这会导致浏览器重新渲染整个页面,这个过程非常耗时。尤其是在大型应用中,这种情况会变得更加严重。

虚拟 DOM 解决了这个问题。它在内存中维护一个 DOM 树的副本,每次更新时,只需要更新虚拟 DOM 树,然后再将虚拟 DOM 树与真实 DOM 树进行对比,找出需要更新的部分,然后才进行真实 DOM 的更新。

实际上虚拟Dom即为用对象的形式来模拟真实的Dom的结构,在更新时只需首先更新虚拟Dom树,之后与真实Dom树对比再进行真实Dom树结构的更新 -- 减少真实Dom操作

2. vue中的虚拟Dom组成

控制台输出的虚拟Dom对象。

  • VNode:虚拟 DOM 树中的一个节点,每个 VNode 都包含了当前节点的信息,如标签名、属性、子节点等。
  • createVNode:创建一个新的 VNode 实例。
  • patch:比较两个 VNode 实例,找出需要更新的部分,然后更新真实 DOM。
  • render:将虚拟 DOM 树渲染成真实 DOM 树。

3. createVnode

回到在上一节中,我们通过createApp创建了应用实例app,之后我们使用app上的mount方法进行挂载。

javascript 复制代码
const app = {
    _component: rootComponent,
    mount(rootContainer) {
        const vnode = createVNode(rootComponent);
        render(vnode, rootContainer);
    },
};
javascript 复制代码
const createVNode = function (type, props, children) {
    const vnode = {
        el: null,
        component: null,
        key: props === null || props === void 0 ? void 0 : props.key,
        type,
        props: props || {},
        children,
        shapeFlag: getShapeFlag(type),
    };
    if (Array.isArray(children)) {
        vnode.shapeFlag |= 16;
    }
    else if (typeof children === "string") {
        vnode.shapeFlag |= 8;
    }
    normalizeChildren(vnode, children);
    return vnode;
};

3.1 getShapeFlag

判断传入的type是否为字符串,是则为元素节点否则为组件节点。显然我们传入的type为rootComponent --> App为对象,所以这里代表我们创建的是有状态组件节点。

javascript 复制代码
    function getShapeFlag(type) {
        return typeof type === "string"
            ? 1
            : 4;
    }

3.1.1 shapeFlag是什么?

shapeFlag 是一个数字值,它用来描述一个 VNode 的类型和特征。在 Vue 中,每个 VNode 都有一个唯一的 shapeFlag 值,这个值可以用来快速判断 VNode 的类型和特征。

为什么需要shapeFlag?

shapeFlag 值可以用来快速判断 VNode 的类型和特征,这可以帮助 Vue 在渲染和更新过程中做出更好的优化和决策。例如,在渲染过程中,Vue 可以根据 shapeFlag 值来决定是否需要创建一个新的 DOM 元素或更新一个现有的 DOM 元素。

vue中的shapeFlag代表的节点

  • 0x1(1):ELEMENT - 表示 vnode 是一个普通的 HTML 元素
  • 0x2(2):FUNCTIONAL_COMPONENT - 表示 vnode 是一个函数式组件
  • 0x4(4):STATEFUL_COMPONENT - 表示 vnode 是一个有状态组件
  • 0x8(8):TEXT_CHILDREN - 表示 vnode 的子节点是文本类型
  • 0x10(16):ARRAY_CHILDREN - 表示 vnode 的子节点是数组类型
  • 0x20(32):SLOTS_CHILDREN - 表示 vnode 的子节点是插槽类型
  • 0x40(64):TELEPORT - 表示 vnode 是一个 teleport 组件
  • 0x80(128):SUSPENSE - 表示 vnode 是一个 suspense 组件
  • 0x100(256):COMPONENT_SHOULD_KEEP_ALIVE - 表示 vnode 是一个需要被 keep-live 的有状态组件
  • 0x200(512):COMPONENT_KEPT_ALIVE - 表示 vnode 是一个已经被 keep-live 的有状态组件

3.1.2 按位或

按位或(bitwise OR)操作是指将两个二进制数进行逐位或运算。也就是说,对于每个对应的位,如果任意一个数的该位为 1,则结果的该位为 1。

在这里,我们使用按位或操作来组合 shapeFlag 的值。这是因为每个值代表一个特定的特征,我们可以通过组合这些值来描述一个 VNode 的多个特征。

让我们以 1 | 8 为例:

  • 1 代表元素节点(Element)
  • 8 代表vnode 的子节点是文本类型(TEXT_CHILDREN)

当我们进行按位或操作时,我们得到:

yaml 复制代码
      0001
    | 1000
    ------
      1001 //9

结果是 9。这个值同时代表元素节点和子节点是文本类型的节点。

这样,我们就可以使用一个单独的值来描述一个 VNode 的多个特征。在 Vue 中,这个值可以被用来优化渲染和更新过程。

3.2 normalizeChildren

判断vnode的子组件类型

如果 children 是一个对象,并且 vnode 不是一个普通的 HTML 元素(即不是 ELEMENT),则表示 vnode 的子节点是插槽类型的。

因此,设置 vnode.shapeFlag 中的 SLOTS_CHILDREN 标志(32)是为了标识 vnode 的子节点是插槽类型的。

javascript 复制代码
    function normalizeChildren(vnode, children) {
        if (typeof children === "object") {
            if (vnode.shapeFlag & 1) ;
            else {
                vnode.shapeFlag |= 32;
            }
        }
    }

3.3 vnode

通过createVnode创建了vnode节点,而我们创建app组件的shapeFlag很明显为4,代表为组件节点,同时这里的type属性为调用函数传入的rootComponent即为App组件。

javascript 复制代码
     const vnode = {
            el: null,
            component: null,
            key: props === null || props === void 0 ? void 0 : props.key,
            type, //rootComponent
            props: props || {},
            children,
            shapeFlag: getShapeFlag(type)  
     }

下图为输出的vnode对象,注意这里除了component和el均与目前创建的虚拟节点相同。

4. render

这里调用的render实际为虚拟dom组成成分中的patch而不是真实的render

render(vnode, rootContainer);调用的两个参数分别为刚刚使用createVnode创建的vnode和调用mount时传入的root真实dom节点。

javascript 复制代码
    const render = (vnode, container) => {
        console.log("调用 patch");
        patch(null, vnode, container);
    };

4.1 patch

注意我们调用patch时的参数,n1为null,n2为创建的app的vnode,container为root真实dom。

解构出的type为rootComponent,shapeFlag为4,所以执行了处理component的逻辑,进入了processComponent函数。

javascript 复制代码
    function patch(n1, n2, container = null, anchor = null, parentComponent = null) {
            const { type, shapeFlag } = n2;  // type为rootComponent,shapeFlag为4
            console.log('patch',n2)
            switch (type) {
                case Text:
                    processText(n1, n2, container);
                    break;
                case Fragment:
                    processFragment(n1, n2, container);
                    break;
                default:
                    if (shapeFlag & 1) {
                        console.log("处理 element");
                        processElement(n1, n2, container, anchor, parentComponent);
                    }
                    else if (shapeFlag & 4) {
                        console.log("处理 component");
                        processComponent(n1, n2, container, parentComponent);
                    }
            }
        }

4.2 processComponent

判断是挂载组件还是更新组件,n1为null走mountComponent流程。

javascript 复制代码
    function processComponent(n1, n2, container, parentComponent) {
        //n1 == null
            if (!n1) {
                mountComponent(n2, container, parentComponent);
            }
            else {
                updateComponent(n1, n2);
            }
        }
相关推荐
luoluoal10 分钟前
java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)
java·vue.js·spring boot
mez_Blog1 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
深情废杨杨1 小时前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS1 小时前
【vue3】vue3.3新特性真香
前端·javascript·vue.js
众生回避1 小时前
鸿蒙ms参考
前端·javascript·vue.js
洛千陨1 小时前
Vue + element-ui实现动态表单项以及动态校验规则
前端·vue.js
GHUIJS2 小时前
【vue3】vue3.5
前端·javascript·vue.js
计算机学姐3 小时前
基于python+django+vue的家居全屋定制系统
开发语言·vue.js·后端·python·django·numpy·web3.py
秋沐3 小时前
vue中的slot插槽,彻底搞懂及使用
前端·javascript·vue.js