面试中的网红虚拟DOM,你知多少呢?深入解读diff算法

3,125 阅读13分钟

深入浅出虚拟DOM和diff算法

这是我参与更文挑战的第28天,活动详情查看:更文挑战

📢前言

众所周知,在前端的面试中,面试官非常爱考vdom和diff算法。比如,可能会出现在以下场景🤏

滴滴滴,面试官发来一个面试邀请。接受邀请📞

🧑面试官:你知道 key 的作用吗?

🙎我:key 的作用是保证数据的唯一性。

🧑面试官:怎么保证数据的唯一性?

🙎我:就....

🧑面试官:你知道虚拟dom吗?

🙎我:虚拟dom就是……balabala

🧑面试官:(好像有点道理)那你知道diff算法吗?

🙎我:(心里:what……diff算法是什么??)

🧑面试官:本次面试结束,回去等面试结果通知。

🙋🙋🙋

我们都知道, key 的作用在前端的面试是一道很普遍的题目,但是呢,很多时候我们都只浮于知识的表面,而没有去深挖其原理所在,这个时候我们的竞争力就在这被拉下了。所以呢,深入学习原理对于提升自身的核心竞争力是一个必不可少的过程。

在接下来的这篇文章中,我们将讲解面试中很爱考的虚拟 DOM 以及其背后的 diff 算法。

一、☂️虚拟DOM(Vitual DOM)

1、虚拟DOM(Vitual DOM)和diff的关系

我们都知道 DOM 操作是非常耗费性能的,早期我们用 JQuery 来自行控制 DOM 操作的时机,也就是手动调整,这样子其实也不是特别方便。因此就出现了虚拟 DOM ,即 Vitual DOM (下文简称为 vdom ),来解决 DOM 操作的问题。 vdom 是现如今的一个热门话题,也是面试中的热门话题,基本上在前端的面试中都会问到 虚拟DOM 的问题。

而为什么会问到 vdom 的问题呢,原因在于现在流行的 vue react 框架,都是数据驱动视图,并且是基于 vdom 实现的,可以说 vdom 是实现 vuereact 的重要基石。

谈到 vdom ,我们不明觉厉的还会想到 diff算法 。那 diff算法vdom 是什么关系呢?

其实, vdom 是一个大的概念,而 diff算法vdom 的一部分, vdom 的核心价值在于最大程度的减少DOM的使用范围vdom 通过把 DOM 用JS的方式进行模拟,之后进行计算和对比,最后找出最小的更新范围去更新。那么这个对比的过程就是 diff 算法 。也就是说他们两者是包含关系如下图所示:

vdom和diff算法

可以说,diff 算法是 vdom 中最核心、最关键的部分,整个 vdom 的核心包围着大量的 diff算法

有了这几个概念的基础铺垫,接下来我们来开始了解 虚拟DOM 是什么。

2、真实DOM的渲染过程

在开始讲解 虚拟DOM 之前,我们先来了解真实的 DOM 在浏览器中是怎么解析的。浏览器渲染引擎工作流程大致分为以下4个步骤:

创建DOM树创建CSSOM树生成render树布局render树绘制render树

  • 第一步:创建 DOM 树。渲染引擎首先解析 HTML 代码,并生成 DOM 树。
  • 第二步:创建 CSSOM 树。浏览为获得外部 css 文件的数据后,就会像构建 DOM 树一样开始构建 CSSOM 树,这个过程与第一步没什么差别。
  • 第三步:生成 Render 树。将 DOM 树和 CSSOM 树关联起来,生成一棵 Render (渲染)树。
  • 第四步:布局 Render 树。有了 Render 树之后,浏览器开始对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
  • 第五步:绘制 Render 树。将每个节点绘制到屏幕上。

引用网上的一张图来呈现真实DOM的渲染过程:

真实DOM的渲染过程

3、虚拟DOM是什么?

当用原生 js 或者 jq 去操作真实 DOM 的时候,浏览器会从构建DOM树开始从头到尾执行一遍流程。那这样的话,就很有可能导致操作次数过多。当操作次数过多时,之前计算的与 DOM 节点相关的坐标值等各种值就......不知不觉的浪费掉了其性能,因此呢,虚拟DOM由此产生

4、解决方案 - vdom

(1)问题引出

大家都知道, DOM 树是具有一定的复杂度的,所以,在生成 DOM 树的过程中,会不断的进行计算操作,但难就难在,想要减少计算次数其实还是比较难的。

那换个思路考虑,我们都知道, JS 的执行速度很快很快,那能不能尝试着把这个计算,更多的转为JS计算呢?答案是肯定的。

(2)vdom如何解决问题:将真实DOM转为JS对象的计算

假设在一次操作中有1000个节点需要更新 DOM ,那么 虚拟DOM 不会立即去 操作DOM ,而是将这1000次更新的 diff 内容保存到本地的一个 JS 对象当中,之后将这个 JS 对象一次性 attachDOM 树上,最后再进行后续的操作,这样子就避免了大量没有必要的计算

所以,用JS对象模拟DOM节点的好处是,先将页面的更新全部反映到虚拟 DOM 上,这样子就先**操作内存中的JS对象**。值得注意的是,操作内存中 JS 对象的速度是相当快的。因此,等到全部 DOM节点 更新完成之后,再将 最后的JS对象 映射到 真实的DOM 上,交由 浏览器 去绘制。

这样,就解决了真实 DOM 渲染速度慢性能消耗大的问题。

5、用JS模拟一个DOM结构

根据下方的 html 代码,用 v-node 模拟出该 html 代码的 DOM 结构。

html代码:

<div id="div1" class="container">
    <p>
        vdom
    </p>
    <ul style="font-size:20px;">
        <li>a</li>
    </ul>
</div>

用JS模拟出以上代码的DOM结构:

{
	tag: 'div',
    props:{
        className: 'container',
        id: 'div1'
    },
    children: [
        {
            tag: 'p',
            chindren: 'vdom'
        },
        {
            tag: 'ul',
            props:{ style: 'font-size: 20px' },
            children: [
                {
                    tag: 'li',
                    children: 'a'
                }
                // ....
            ]
        }
    ]
}

通过以上代码我们可以分析出,我们用 tagpropschildren 来模拟 DOM 树结构。用 JS 模拟 DOM 树的结构,这样做的好处在于,可以计算出最小的变更,操作最少的DOM

6、通过snabbdom学习vdom

vuevdomdiff算法 是参考 github 上的一个开源库 snabbdom 改造过来的,那么我们接下来就用这个库为例,来学习 vdom 的思想。

(1)snabbdom是什么

  • snabbdom 是一个简洁又强大的 vdom 库,易学易用;
  • Vue 参考它实现的 vdomdiff
  • Vue3.0 重写了 vdom 的代码,优化了性能。

(2)snabbdom浅析

我们先来看 snabbdom 首页上的 example ,先简单了解其思想。下面先贴上代码:

import {
  init,
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
  h,
} from "snabbdom";

const patch = init([
  // Init patch function with chosen modules
  classModule, // makes it easy to toggle classes
  propsModule, // for setting properties on DOM elements
  styleModule, // handles styling on elements with support for animations
  eventListenersModule, // attaches event listeners
]);

const container = document.getElementById("container");
//h函数输入一个标签,之后再输入一个data,最周输入一个子元素
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
  h("span", { style: { fontWeight: "bold" } }, "This is bold"),
  " and this is just normal text",
  h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);

//第一个patch函数
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);

const newVnode = h(
  "div#container.two.classes",
  { on: { click: anotherEventHandler } },
  [
    h(
      "span",
      { style: { fontWeight: "normal", fontStyle: "italic" } },
      "This is now italic type"
    ),
    " and this is still just normal text",
    h("a", { props: { href: "/bar" } }, "I'll take you places!"),
  ]
);

//第二个patch函数
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

通过官方的例子我们可以知道, h 函数输入一个标签,之后输入一个 data ,最后输入一个子元素。并且h函数是一个 vnode 的结构( vnode 结构见上述第5点),层级般的一层一层递进。最后就是 patch 函数,第一个patch 函数用来对元素进行渲染,第二个 patch 函数用来比较新旧节点

(2)snabbdom演示

接下来我们用 cdn 的方式引入 snabbdom 的库,来演示一遍 snabbdom 是如何操作 vdom 的。附上代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>

    <script>
        const snabbdom = window.snabbdom

        // 定义 patch
        const patch = snabbdom.init([
            snabbdom_class,
            snabbdom_props,
            snabbdom_style,
            snabbdom_eventlisteners
        ])

        // 定义 h
        const h = snabbdom.h

        const container = document.getElementById('container')

        // 生成 vnode
        const vnode = h('ul#list', {}, [
            h('li.item', {}, 'Item 1'),
            h('li.item', {}, 'Item 2')
        ])
        patch(container, vnode)

        document.getElementById('btn-change').addEventListener('click', () => {
            // 生成 newVnode
            const newVnode = h('ul#list', {}, [
                h('li.item', {}, 'Item 1'),
                h('li.item', {}, 'Item B'),
                h('li.item', {}, 'Item 3')
            ])
            patch(vnode, newVnode) // vnode = newVnode → patch 之后,应该用新的覆盖现有的 vnode ,否则每次 change 都是新旧对比

        })

    </script>
</body>
</html>

此时我们来看浏览器的显示效果:

snabbdom演示

我们可以看到,最终的效果是当我们点击时, DOM不会一整棵树重新渲染,而是只针对改变的值进行重新比较,最终只将改变的节点进行渲染。

通过这样的演示,相信大家对真实 DOM 和虚拟 DOM 的区别有了一定的了解。

7、vdom总结

讲到这里,我们来对vdom做一个总结:

  • 可以通过 JS 来模拟 DOM 结构(vnode);
  • 新旧 vnode 对比,得出最小的更新范围,最后更新DOM
  • 数据驱动视图的模式下,可以有效地控制DOM操作

二、🧶diff算法

我们在上述讲 vdom 的时候说过, vdom 的核心价值就在于最大程度的减少DOM的使用范围。那 vdom 是通过什么方式呢,它是通过把 DOMJS 来去模拟,之后进行计算和进行对比,最后找出最小的更新范围去更新。那么这个对比的过程对应的就是我们经常听到的 diff 算法。

接下来就让我们一起来了解 vdom 的另外一个内容, diff 算法。

1、diff算法

  • diff算法是前端的一个热门话题,同时也是 vdom 中最核心、最关键的部分。

  • diff算法在日常使用 vuereact 中经常出现(如key)。

2、diff算法概述

  • diff对比,是一个广泛的概念,如linux diff命令git diff命令等。
  • 两个js对象也可以做 diff ,如 github 上的jiff库,这个库可以直接用来给两个js对象做diff。
  • 两棵树做 diff ,如上述所说的 vdomdiff

我们来看个例子🌰:

树的对比

看到上面两棵树,我们可以想象下它是如何进行 diff 算法的。我们可以看到,右边这棵树要把左边的 E 改为 X ,同时要新增一个节点 H 。因此如果通过 diff 来实现的话,我们可以对其进行新旧节点的比较,如果比较完一样,则不动它;如果比较完不一样,则对它进行修改。这样处理的话,5个节点只需要修改2次,而不用修改5次,效率很是UpUp。

3、树diff的时间复杂度O(n3)

对于树来说,原始的时间复杂度有O(n3)。那么这个 O(n3) 是怎么来的呢?

首先,遍历tree1;其次,遍历tree2;最后,对树进行排序。这样 n*n*n ,就达到了O(n3)

假设现在有1000个节点要操作,那1000的3次方就10亿次了,因此,树的这个算法不可用。那我们怎么解决呢?继续看下面。

4、优化时间复杂度到O(n)

因为树的时间复杂度是O(n3),因此,我们就想办法,优化其时间复杂度从O(n3)到O(n),以达到操作 vdom 节点,那这个优化过程其实我们所说的 diff 算法。通过 diff 算法,我们可以将时间复杂度从O(n3)优化到O(n)diff算法的具体思想如下:

  • 只比较同一层级不跨级比较
  • tag 不相同,则直接删掉重建,不再深度比较;
  • tagkey ,两者都相同,则认为是相同节点,不再深度比较。

三、🎩深入diff算法源码

1、生成vnode

我们先来回顾下上面讲的 snabbdomdiff 比较先是在 h 函数里面进行,这个 h 函数输入一个标签,之后输入一个 data ,最后输入一个子元素。并且 h 函数是一个 vnode 的结构,层级般的一层一层递进。最后就是 patch 函数, 第一个patch 函数用来对元素进行渲染,第二个 patch 函数用来比较新旧节点

接下来我们来看下它是如何生成vnode的。

先克隆一份snabbdom的代码下来,打开 src|h.ts 文件,直接来看 h 函数,具体代码如下:

export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(
  sel: string,
  data: VNodeData | null,
  children: VNodeChildren
): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
  let data: VNodeData = {};
  let children: any;
  let text: any;
  let i: number;
  if (c !== undefined) {
    if (b !== null) {
      data = b;
    }
    if (is.array(c)) {
      children = c;
    } else if (is.primitive(c)) {
      text = c;
    } else if (c && c.sel) {
      children = [c];
    }
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b;
    } else if (is.primitive(b)) {
      text = b;
    } else if (b && b.sel) {
      children = [b];
    } else {
      data = b;
    }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i]))
        children[i] = vnode(
          undefined,
          undefined,
          undefined,
          children[i],
          undefined
        );
    }
  }
  if (
    sel[0] === "s" &&
    sel[1] === "v" &&
    sel[2] === "g" &&
    (sel.length === 3 || sel[3] === "." || sel[3] === "#")
  ) {
    addNS(data, children, sel);
  }
  // 返回vnode,这个vnode对应patch下的vnode
  return vnode(sel, data, children, text, undefined);
}

我们看到最后一行, h 函数返回的是一个 vnode 函数。之后我们继续找 vnode 的文件,在 src|vnode.ts 文件中。附上最关键部分代码:

export function vnode(
  sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined
): VNode {
  const key = data === undefined ? undefined : data.key;
  // 返回一个对象
  // elm表示vnode结构对应的是哪一个DOM元素
  // key可以理解为v-for时我们使用的key
  return { sel, data, children, text, elm, key };
}

同样定位到最后一行,大家可以发现, vnode 实际上是返回一个对象。而这个对象里,有6个元素。其中, sel, data, children, text 四个元素对应我们上面讲 vnode 时对应的结构(第一点的第5点)。而 elm 表示 vnode 结构对应的是哪一个 DOM 元素,最后的 key 大家可以理解为是我们使用 v-for 时用的 key ,同时需要注意是, key 不一定只有在 v-for 时可以使用,在定义组件等各种场景时均可使用。

2、patch函数

看完 vnode ,我们来看下如何用patch函数来对比 vnode 。从官方文档中我们可以定位到, patch 函数在 src|init.ts 文件下,我们找到 init.ts 文件。同样,我们定位到 patch 函数部分,具体代码如下:

// 返回一个patch函数
  return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    // 执行pre hook,hook 即 DOM 节点的生命周期
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

    // 第一个参数不是vnode,是一个DOM元素
    if (!isVnode(oldVnode)) {
      // 创建一个空的 vnode,关联到这个DOM元素
      oldVnode = emptyNodeAt(oldVnode);
    }

    // 相同的vnode(key 和 sel 都相等)
    if (sameVnode(oldVnode, vnode)) {
      // vnode进行对比
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } 
    // 不同的 vnode , 直接删掉重建
    else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm) as Node;

      // 重建
      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  };

阅读以上代码我们可以知道,我们刚开始创建时,第一个参数不是 vnode ,而是一个 DOM 元素,这个时候我们需要先创建一个空的 vnode ,来关联到这个 DOM 元素上。

有了第一个 vnode 之后,我们在第二次 patch 时,就可以对新旧节点进行比较。而新旧节点的比较是先判断 keysel 是否相同,如果相同,则用 pathVNode 函数对新旧节点进行比较。如果是不同的 vnode ,则直接删掉重建。

3、patchVnode函数

上面我们说到了 patchVnode 函数进行新旧节点的比较,下面来对 patchVnode 进行详细剖析。同样在 src|init.ts 文件中,附上patchVnode函数的代码:

function patchVnode(
    oldVnode: VNode,
    vnode: VNode,
    insertedVnodeQueue: VNodeQueue
  ) {
    // 执行prepatch hook
    const hook = vnode.data?.hook; 
    hook?.prepatch?.(oldVnode, vnode);

    // 设置vnode.elem
    const elm = (vnode.elm = oldVnode.elm)!;

    // 旧的 children
    const oldCh = oldVnode.children as VNode[];
    // 新的children
    const ch = vnode.children as VNode[];

    // 当新旧节点相等时则返回
    if (oldVnode === vnode) return;

    // hook 相关
    if (vnode.data !== undefined) {
      for (let i = 0; i < cbs.update.length; ++i)
        cbs.update[i](oldVnode, vnode);
      vnode.data.hook?.update?.(oldVnode, vnode);
    }

    // vnode.text === undefined (vnode.children 一般有值;children和text只能存在一个,不能共存)
    if (isUndef(vnode.text)) {
      // 新旧vnode都有children
      if (isDef(oldCh) && isDef(ch)) {
        // updateChildren 两者都有children时要进行对比
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
        // 新的vnode有chindren,旧的vnode没有children (旧的vnode有text)
      } else if (isDef(ch)) {
        // 清空旧的vnode的text
        if (isDef(oldVnode.text)) api.setTextContent(elm, "");
        // 添加children
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
        // 旧的vnode有children,新的vnode没有children
      } else if (isDef(oldCh)) {
        // 移除旧vnode的children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        // 旧的vnode有text
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, "");
      }

      // else: vnode.text != undefined (说明 vnode.text 有值,旧的vnode.children 没有值)
    } else if (oldVnode.text !== vnode.text) {
      // 移除旧vnode的children
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      // 设置新的text
      api.setTextContent(elm, vnode.text!);
    }
    hook?.postpatch?.(oldVnode, vnode);
  }

阅读以上源码我们可以知道:

(1) 当旧的 vnodetext 时,则说明旧的 children 没有值,且新的 vnodetext 有值。这个时候我们就把旧的 vnodechildren 进行删除,删除结束给新的 vnode 设置 text

(2) 当新旧节点都有 children 时,我们需要对其进行更新操作,也就是操作 updateChildren 函数。这个我们将在下面进行讲解。

(3) 如果新的 vnodechildren ,旧的 vnode 没有 children ,则说明旧的 vnodetext ,所以此时需要清空旧的 vnodetext ,并添加新的 children 上去。

(4) 如果旧的 vnode children ,新的 vnode 没有 children ,则移除旧的 vnodechildren

(5) 如果新旧节点都有 text ,则直接把新的 vnodetext 值赋值给旧的 vnodetext

来看下图的呈现:

 新旧节点的对比

4、updateChildren函数

上面分析 pathVnode 时我们讲到了用 updateChildren 函数来更新新旧节点的 children接下来我们来看下这个函数:

function updateChildren(
    parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue
  ) {
    let oldStartIdx = 0;
    let newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];

        // 开始和开始进行对比
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];

        // 结束和结束进行对比
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];

        // 开始和结束做对比
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(
          parentElm,
          oldStartVnode.elm!,
          api.nextSibling(oldEndVnode.elm!)
        );
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];

        // 结束和开始做对比
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];

        // 以上四个都未命中
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }

        // 拿新节点的key,能否对应上oldCh中的某个节点的key
        idxInOld = oldKeyToIdx[newStartVnode.key as string];

        // 没有对应上
        if (isUndef(idxInOld)) {
          // New element
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!
          );

          // 对应上了
        } else {
          // 对应上key的节点
          elmToMove = oldCh[idxInOld];

          // sel是否相等(sameVnode的条件)
          if (elmToMove.sel !== newStartVnode.sel) {
            // sel不相等,可能只是key相等;那也没有用,只能重建 New Element
            api.insertBefore(
              parentElm,
              createElm(newStartVnode, insertedVnodeQueue),
              oldStartVnode.elm!
            );

            // sel 相等,key 相等;执行patchVnode函数
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
        }
        newStartVnode = newCh[++newStartIdx];
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(
          parentElm,
          before,
          newCh,
          newStartIdx,
          newEndIdx,
          insertedVnodeQueue
        );
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

我们先来看两张图:

updateChildren图示1 updateChildren图示2

大家先看图1, updateChildren 要做得事情就是,将新旧节点进行对比,如果相同则不进行更新,如果不同则对其进行更新操作。

再看图2,而更新的方式就是,通过对oldStartIdxnewStartIdxoldEndIdxnewEndIdx这四个值进行比较,来得出是否需要更新操作。

那这四个值如何进行比较呢?接下来我们继续看。

阅读源码我们可以分析出,通过对4种类型的节点进行比较,来判断如何更新节点

第一种,旧的开始节点 oldStartIdx 和新的开始节点 newStartIdx 比较。第二种,旧的开始节点 oldStartIdx 和新的结束节点 newEndIdx 比较。第三种,旧的结束节点 oldEndIdx 和新的开始节点 newStartIdx 比较。第四种,旧的结束节点 oldEndIdx 和新的结束节点 newEndIdx 比较。

如果以上这四种比较都没有命中,则拿取新节点的key ,之后将这个 key 查看是否对应上 oldCh 中某个节点的 key如果没有对应上,则直接重建元素如果对应上了,还要再判断 selkey 是否相等,如果相等,则执行patchVnode函数,如果不相等,那跟前面一样,也只能重建元素

四、🩸结束语

vdom的核心概念主要在 hvnodepatchdiffkey 这几个内容,个人觉得,整个 diff 的比较都在围绕着这几个函数进行,所以了解这几个核心概念很重要。同时,vdom存在的另一个更重要的价值莫过于数据驱动视图了, vdom 通过控制 DOM 的操作来使得数据可以去驱动视图

关于虚拟DOM和diff的讲解到此就结束啦!希望对大家有帮助!

  • 关注公众号 星期一研究室 ,不定期分享学习干货,学习路上不迷路~
  • 如果这篇文章对你有用,记得点个赞加个关注再走哦~
  • 我们下期见!🥂🥂🥂