Vue中虚拟DOM的简单实现

 更新时间:2023年05月23日 10:09:59   作者:刘狗蛋O_o  
Vue中的虚拟DOM是通过JavaScript对象来描述真实DOM结构的一种方式,本文将介绍Vue中虚拟DOM的简单实现,具有一定的参考价值,感兴趣的可以了解一下

虚拟DOM

1. 什么是虚拟DOM?为什么要有虚拟DOM?

  • 虚拟DOM 是用于描述 DOM 节点的 JS 对象。
  • 操作真实DOM非常 耗费性能 。尽可能 减少 对 DOM 的操作,通过牺牲 JS 的计算性能来换取操作 DOM 所消耗的性能。

2. VNode类

省略了部分属性

/** src/core/vdom/vnode.ts **/
export default class VNode {
    tag;                 // 当前节点的标签名
    data;                // 当前节点对应的对象,包含了一些具体数据信息
    children;            // 当前节点的子节点数组
    text;                // 当前节点的文本
    elm;                 // 当前节点对应的真实DOM节点
    context;             // 当前节点的上下文(Vue实例)
    componentInstance;   // 当前节点对应的组件实例
    parent;              // 当前节点对应的真实的父DOM节点
    // diff 优化的属性
    isStatic;            // 是否静态节点,是则跳过 diff
    constructor(
        tag?,
        data?,
        children?,
        text?,
        elm?,
        context?
    ) {
        this.tag = tag;
        this.data = data;
        this.children = children;
        this.text = text;
        this.elm = elm;
        this.context = context;
        this.componentInstance = undefined;
        this.parent = undefined;
        this.isStatic = false;
    }
}

3. VNode的类型

3.1 注释节点

注释节点的描述非常简单: vnode.text 表示注释内容, vnode.isComment 表示是一个注释节点。

/** src/core/vdom/vnode.ts **/
export const createEmptyVNode = (text) => {
    const node = new VNode();
    node.text = text;
    node.isComment = true;
    return node;
};

3.2 文本节点

文本节点 的描述比 注释节点 更简单,只需要一个 text 属性。

/** src/core/vdom/vnode.ts **/
export function createTextVNode(val) {
    return VNode(undefined, undefined, undefined, String(val));
};

3.3 克隆节点

克隆节点是复制一个已存在的节点,主要是为了做 模版编译优化 时使用。

克隆时会新建一个 VNode实例 ,然后将需要复制的节点信息 浅拷贝 到新的节点上,并通过 vnode.isCloned 标识该节点是克隆节点。

/** src/core/vdom/vnode.ts **/
export function cloneVNode(vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children && vnode.children.slice(),
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  );
  cloned.ns = vnode.ns;
  cloned.isStatic = vnode.isStatic;
  cloned.key = vnode.key;
  cloned.isComment = vnode.isComment;
  cloned.fnContext = vnode.fnContext;
  cloned.fnOptions = vnode.fnOptions;
  cloned.fnScopeId = vnode.fnScopeId;
  cloned.asyncMeta = vnode.asyncMeta;
  cloned.isCloned = true;
  return cloned;
};

3.4 元素节点

/** src/core/vdom/create-element.ts **/
export function _createElement(
    context,
    tag?,
    data?,
    children?
) {
    // 如果 data 存在且已转成可观测对象,则返回一个注释节点
    if (isDef(data) && isDef(data.__ob__)) {
        return createEmptyVNode();
    }
    // 根据 data.is 重新给 tag 赋值
    // :is 的实现原理
    if (isDef(data) && isDef(data.is)) {
        tag = data.is;
    }
    // 如果不存在 tag ,则返回一个注释节点
    if (!tag) {
        return createEmptyVNode();
    }
    let vnode;
    if (typeof tag === 'string') {
        let Ctor;
        if ((!data || !data.pre) && isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) {
            // 根据 tag 从 options.components 中获取要创建的组件节点
            vnode = createComponent(Ctor, data, context, children);
        } else {
            // 创建普通的 vnode 节点
            vnode = new VNode(tag, data, children, undefined, undefined, context);
        }
    } else {
        // 创建组件节点
        vnode = createComponent(tag, data, context, children);
    }
    return vnode;
}

3.5 组件节点

组件节点除了元素节点具有的属性外,还有两个特有属性:

  • componentOptions: 组件的 option选项,如组件的 props 等
  • componentInstance: 组件节点对应的 Vue实例

3.6 函数式组件节点

函数式组件节点相较于组件节点又有两个特有属性:

  1. fnContext: 函数式组件对应的 Vue实例
  2. fnOptions: 组件的 option选项

4. 总结

在视图渲染之前,将写好的 template模版 编译成 vnode 缓存下来。等到 数据发生变化 页面需要 重新渲染 时,将数据发生变化后生成的 vnode 与前一次缓存的 vnode 进行对比,找出差异,根据 有差异的vnode 创建 真实DOM节点再插入到视图中,完成试图更新。

Diff

1. 创建节点

为了避免直接修改 vnode 而引起 状态混乱 问题,创建节点时若 vnode 已被之前的渲染使用,则 克隆该节点 ,修改克隆的 vnode 的属性。

创建节点时,会根据当前 宿主环境 调用封装好的 nodeOps.createElement() 方法,在 web端 等同于 document.createElement() 。

/** src/core/vdom/patch.ts **/
function createElm(
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
) {
    // vnode 有真实DOM节点时,克隆生成新的 vnode
    if (isDef(vnode.elm) && isDef(ownerArray)) {
        vnode = ownerArray[index] = cloneVNode(vnode);
    }
    // 是否是组件的根节点
    vnode.isRootInsert = !nested;
    const data = vnode.data;
    const children = vnode.children;
    const tag = vnode.tag;
    if (isDef(tag)) {
        // tag 不为 null/undefined 时
        // 创建新的真实DOM节点
        vnode.elm = nodeOps.createElement(tag, vnode);
        // 创建子节点
        createChildren(vnode, children, insertedVnodeQueue);
        // 插入节点
        insert(parentElm, vnode.elm, refElm);
    } else if (isTrue(vnode.isComment)) {
        // 创建注释节点
        vnode.elm = nodeOps.createComment(vnode.text);
        insert(parentElm, vnode.elm, refElm);
    } else {
        // 创建文本节点
        vnode.elm = nodeOps.createTextNode(vnode.text);
        insert(parentElm, vnode.elm, refElm);
    }
}

2. 删除节点

删除节点的逻辑非常简单,找到 父节点 再移除 子节点 。

/** src/core/vdom/patch.ts **/
function removeNode(el) {
    const parent = nodeOps.parentNode(el);
    nodeOps.removeChild(parent, el);
}

3. 更新节点

更新节点比较复杂:

  • 判断新/旧节点是否相同,若 新/旧节点相同 , 则 结束更新流程
  • 克隆节点
  • 新旧节点是否为 静态节点 ,新/旧节点的 key 是否相同,新节点是否为 克隆节点 或新节点是否只创建一次。若 新/旧节点都为静态节点,新旧节点的 key相同 ,新节点为克隆节点或新节点只能被创建一次 ,则更新 vnode.componentInstance 并 结束更新流程
  • 新节点是否包含文本
  • 新节点不包含文本
    • 新/旧节点都包含子节点,且子节点不同 ,则 更新子节点
    • 只有新节点包含子节点 ,则 清空DOM中的文本内容,并更新子节点
    • 只有旧节点包含子节点 ,则 移除子节点
    • 新/旧节点都不包含子节点,且旧节点包含文本 ,则 清空DOM中的文本内容
  • 新节点包含文本,且新/旧节点的文本不同 ,则 更新DOM中的文本内容
/** src/core/vdom/patch.ts **/
function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly?
) {
    // 新、旧节点相同,直接返回
    if (oldVnode === vnode) {
        return;
    }
    // 克隆节点,为什么需要克隆节点的原因不做赘述
    if (isDef(vnode.elm) && isDef(ownerArray)) {
        vnode = ownerArray[index] = cloneVNode(vnode);
    }
    // 重新对克隆节点的真实DOM赋值
    const elm = (vnode.elm = oldVnode.elm);
    // vnode 与 oldVnode 都是静态节点,且 key 相同,直接返回
    if (
        isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
        vnode.componentInstance = oldVnode.componentInstance;
        return;
    }
    const oldCh = oldVnode.children;
    const ch = vnode.children;
    // vnode 没有文本属性
    if (isUndef(vnode.text)) {
        if (isDef(oldCh) && isDef(ch)) {
            // 若存在 oldCh 和 ch ,且二者不同
            // 则更新子节点
            if (oldCh !== ch) {
                updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
            }
        } else if (isDef(ch)) {
            // 若只存在 ch 
            // 则清空 DOM 中的文本,再添加子节点
            if (isDef(oldVnode.text)) {
                nodeOps.setTextContent(elm, '');
            }
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
        } else if (isDef(oldCh)) {
            // 若只存在 oldCh
            // 则移除子节点
            removeVnodes(oldCh, 0, oldCh.length - 1);
        } else if (isDef(oldVnode.text)) {
            // 若都没有子节点,且 oldVnode 有文本属性
            // 则清空 DOM 中的文本
            nodeOps.setTextContent(elm, '');
        }
    } else if (oldVnode.text !== vnode.text) {
        // vnode 有文本属性则与 oldVnode 的文本属性比较
        // 若有差异则更新为新的文本
        nodeOps.setTextContent(elm, vnode.text);
    }
}

4. 总结

Vue 的 diff算法(patch) 的过程干了三件事: 创建节点,删除节点,更新节点。

更新子节点

Vue 中通过 updateChildren()方法 更新子节点。其思想就是循环新/旧子节点,然后对比。这部分代码较长,按照代码结构逐步分析。

对源码中的一些属性的中文名作以下约定:

  • 新子节点数组中第一个未处理的子节点: newStartVnode 新前
  • 新子节点数组中最后一个未处理的子节点:newEndVnode 新后
  • 旧子节点数组中第一个未处理的子节点: oldStartVnode 旧前
  • 旧子节点数组中最后一个未处理的子节点:oldEndVnode 旧后

1. 新前与旧前相同

/** src/core/vdom/patch.ts **/
else if (sameVnode(oldStartVnode, newStartVnode)) {
   // 进入 patch 流程,对比新旧 vnode 更新节点
   patchVnode(
       oldStartVnode,
       newStartVnode,
       insertedVnodeQueue,
       newCh,
       newStartIdx
   );
   // 从前向后切换待处理子节点
   oldStartVnode = oldCh[++oldStartIdx];
   newStartVnode = newCh[++newStartIdx];
}

2. 新后与旧后相同

/** src/core/vdom/patch.ts **/
else if (sameVnode(oldEndVnode, newEndVnode)) {
   // 进入 patch 流程,对比新旧 vnode 更新节点
   patchVnode(
       oldEndVnode,
       newEndVnode,
       insertedVnodeQueue,
       newCh,
       newEndIdx
   );
   // 从后向前切换待处理子节点
   oldEndVnode = oldCh[--oldEndIdx];
   newEndVnode = newCh[--newEndIdx];
}

3. 新后与旧前相同

/** src/core/vdom/patch.ts **/
else if (sameVnode(oldStartVnode, newEndVnode)) {
    // 进入 patch 流程,更新节点
    // 省略调用 patchVnode 的代码
    // ...
    // 将旧前节点插到旧后节点后面
    nodeOps.insertBefore(
        parentElm,
        oldStartVnode.elm,
        nodeOps.nextSibling(oldEndVnode.elm)
    );
    // 切换待处理子节点
    oldStartVnode = oldCh[++oldStartIdx];
    newEndVnode = newCh[--newEndIdx];
}

4. 新前与旧后相同

/** src/core/vdom/patch.ts **/
else if (sameVnode(oldEndVnode, newStartVnode)) {
    // 进入 patch 流程,更新节点
    // 省略调用 patchVnode 的代码
    // ...
    // 将旧后节点插到旧前前面
    nodeOps.insertBefore(
        parentElm,
        oldEndVnode.elm,
        oldStartVnode.elm
    );
    // 切换待处理子节点
    oldEndVnode = oldCh[--oldEndIdx];
    newStartVnode = newCh[++newStartIdx];
}

5. 不满足以上4种情况

/** src/core/vdom/patch.ts **/
else {
    // idxInOld 有两种取值方法
    // 1. 获取 旧节点数组中 与 新节点的 key 相同的 vnode 的索引
    // 2. 旧节点数组中 与 新节点相同的 vnode 的索引
    if (isUndef(idxInOld)) {
        // 旧子节点数组 中不存在 新前
        // 创建新元素
        createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
        );
    } else {
        vnodeToMove = oldCh[idxInOld];
        if (sameVnode(vnodeToMove, newStartVnode)) {
            // 进入 patch 流程,更新节点
            // 省略调用 patchVnode 的代码
            // ...
            oldCh[idxInOld] = undefined;
            // 将节点移动到 旧前节点 前面
            nodeOps.insertBefore(
                parentElm,
                vnodeToMove.elm,
                oldStartVnode.elm
            );
        } else {
            // vnode 不同,则创建新元素
            createElm(
                newStartVnode,
                insertedVnodeQueue,
                parentElm,
                oldStartVnode.elm,
                false,
                newCh,
                newStartIdx
            );
        }
    }
}

6. 结束while循环中的逻辑后

if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    // 插入新节点
    addVnodes(
        parentElm,
        refElm,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
    );
} else if (newStartIdx > newEndIdx) {
    // 移除旧子节点数组中剩余未处理的节点
    removeNodes(oldCh, oldStartIdx, oldEndIdx);
}

到此这篇关于Vue中虚拟DOM的简单实现的文章就介绍到这了,更多相关Vue 虚拟DOM内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 关于vue项目部署后刷新网页报404错误解决

    关于vue项目部署后刷新网页报404错误解决

    这篇文章主要介绍了关于vue项目部署后刷新网页报404错误解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • Vue组件渲染与更新实现过程浅析

    Vue组件渲染与更新实现过程浅析

    这篇文章主要介绍了Vue组件渲染与更新实现过程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-03-03
  • 一文了解Vue 3 的 generate 是这样生成 render 函数的

    一文了解Vue 3 的 generate 是这样生成 render&n

    本文介绍generate阶段是如何根据javascript AST抽象语法树生成render函数字符串的,本文中使用的vue版本为3.4.19,感兴趣的朋友跟随小编一起看看吧
    2024-06-06
  • vue项目中监听手机物理返回键的实现

    vue项目中监听手机物理返回键的实现

    这篇文章主要介绍了vue项目中监听手机物理返回键的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-01-01
  • Vue向下滚动加载更多数据scroll案例详解

    Vue向下滚动加载更多数据scroll案例详解

    这篇文章主要介绍了Vue向下滚动加载更多数据scroll案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-08-08
  • Vue3中el-table表格数据不显示的原因和解决方法

    Vue3中el-table表格数据不显示的原因和解决方法

    这篇文章主要给大家介绍了Vue3中el-table表格数据不显示的原因和解决方法,文中有详细的代码示例供大家参考,如果有遇到相同问题的朋友可以参考阅读本文,希望能够帮到您
    2023-11-11
  • vue实现简单无缝滚动效果

    vue实现简单无缝滚动效果

    这篇文章主要为大家详细介绍了vue实现简单无缝滚动效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • 详解如何在你的Vue项目配置vux

    详解如何在你的Vue项目配置vux

    这篇文章主要介绍了详解如何在你的Vue项目配置vux,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-06-06
  • vue2实现pdf电子签章问题记录

    vue2实现pdf电子签章问题记录

    仿照e签宝,实现pdf电子签章 => 拿到pdf链接,移动章的位置,获取章的坐标,怎么实现呢,下面小编给大家介绍vue2实现pdf电子签章问题记录,感兴趣的朋友一起看看吧
    2023-12-12
  • vue.js实现点击图标放大离开时缩小的代码

    vue.js实现点击图标放大离开时缩小的代码

    这篇文章主要介绍了vue.js实现点击图标放大离开时缩小,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01

最新评论