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

在这里插入图片描述

深入浅出虚拟DOM和diff算法

  • 一、虚拟DOM(Vitual DOM)
    • 1、虚拟DOM(Vitual DOM)和diff的关系
    • 2、真实DOM的渲染过程
    • 3、虚拟DOM是什么?
    • 4、解决方案 - vdom
      • (1)问题引出
      • (2)vdom如何解决问题:将真实DOM转为JS对象的计算
    • 5、用JS模拟一个DOM结构
    • 6、通过snabbdom学习vdom
      • (1)snabbdom是什么
      • (2)snabbdom浅析
      • (2)snabbdom演示
    • 7、vdom总结
  • 二、diff算法
    • 1、diff算法
    • 2、diff算法概述
    • 3、树diff的时间复杂度O(n3)
    • 4、优化时间复杂度到O(n)
  • 三、深入diff算法源码
    • 1、生成vnode
    • 2、patch函数
    • 3、patchVnode函数
    • 4、updateChildren函数
  • 四、结束语

众所周知,在前端的面试中,面试官非常爱考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 是实现 vue 和 react 的重要基石。

谈到 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对象一次性 attach 到 DOM 树上,最后再进行后续的操作,这样子就避免了大量没有必要的计算。

所以,用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'}// ....]}]
}

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

6、通过snabbdom学习vdom

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

(1)snabbdom是什么

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

(2)snabbdom浅析

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

import {init,classModule,propsModule,styleModule,eventListenersModule,h,
} from "snabbdom";const patch = init([// Init patch function with chosen modulesclassModule, // makes it easy to toggle classespropsModule, // for setting properties on DOM elementsstyleModule, // handles styling on elements with support for animationseventListenersModule, // 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// 定义 patchconst patch = snabbdom.init([snabbdom_class,snabbdom_props,snabbdom_style,snabbdom_eventlisteners])// 定义 hconst h = snabbdom.hconst container = document.getElementById('container')// 生成 vnodeconst vnode = h('ul#list', {}, [h('li.item', {}, 'Item 1'),h('li.item', {}, 'Item 2')])patch(container, vnode)document.getElementById('btn-change').addEventListener('click', () => {// 生成 newVnodeconst 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 是通过什么方式呢,它是通过把 DOM 用 JS 来去模拟,之后进行计算和进行对比,最后找出最小的更新范围去更新。那么这个对比的过程对应的就是我们经常听到的 diff 算法。

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

1、diff算法

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

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

2、diff算法概述

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

我们来看个例子🌰:

树的对比

看到上面两棵树,我们可以想象下它是如何进行 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次方就1亿次了,因此,树的这个算法不可用。那我们怎么解决呢?继续看下面。

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

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

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

三、深入diff算法源码

1、生成vnode

我们先来回顾下上面讲的 snabbdom , diff 比较先是在 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下的vnodereturn 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时我们使用的keyreturn { 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 时,就可以对新旧节点进行比较。而新旧节点的比较是先判断 key 和 sel 是否相同,如果相同,则用 pathVNode 函数对新旧节点进行比较。如果是不同的 vnode ,则直接删掉重建。

3、patchVnode函数

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

function patchVnode(oldVnode: VNode,vnode: VNode,insertedVnodeQueue: VNodeQueue) {// 执行prepatch hookconst hook = vnode.data?.hook; hook?.prepatch?.(oldVnode, vnode);// 设置vnode.elemconst elm = (vnode.elm = oldVnode.elm)!;// 旧的 childrenconst oldCh = oldVnode.children as VNode[];// 新的childrenconst 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都有childrenif (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的textif (isDef(oldVnode.text)) api.setTextContent(elm, "");// 添加childrenaddVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);// 旧的vnode有children,新的vnode没有children} else if (isDef(oldCh)) {// 移除旧vnode的childrenremoveVnodes(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的childrenif (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1);}// 设置新的textapi.setTextContent(elm, vnode.text!);}hook?.postpatch?.(oldVnode, vnode);}

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

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

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

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

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

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

来看下图的呈现:

 新旧节点的对比

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 rightpatchVnode(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 leftpatchVnode(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中的某个节点的keyidxInOld = oldKeyToIdx[newStartVnode.key as string];// 没有对应上if (isUndef(idxInOld)) {// New elementapi.insertBefore(parentElm,createElm(newStartVnode, insertedVnodeQueue),oldStartVnode.elm!);// 对应上了} else {// 对应上key的节点elmToMove = oldCh[idxInOld];// sel是否相等(sameVnode的条件)if (elmToMove.sel !== newStartVnode.sel) {// sel不相等,可能只是key相等;那也没有用,只能重建 New Elementapi.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,而更新的方式就是,通过对oldStartIdx,newStartIdx,oldEndIdx和newEndIdx这四个值进行比较,来得出是否需要更新操作。

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

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

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

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

四、结束语

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

关于虚拟DOM和diff的讲解到此就结束啦!如有不理解或有误的地方欢迎评论区评论或私信我交流~

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.tpcf.cn/news/307989.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Blazor带我重玩前端(六)

本文主要讨论Blazor事件内容&#xff0c;由于blazor事件部分很多&#xff0c;所以会分成上下两篇&#xff0c;本文为第二篇。双向绑定概述如图所示当点击单项绑定的时候&#xff0c;MyOnewayComponent里的属性值会发生变化&#xff0c;这种变化是单项的&#xff0c;仅仅只是本地…

leetcode707:设计链表(增删差)

一:题目 二:上码 class MyLinkedList { public://定义链表节点结构体struct LinkedNode {int val;LinkedNode* next;LinkedNode(int val):val(val), next(nullptr){}};// 初始化链表MyLinkedList() {node new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点&#xff0…

深入探究.Net Core Configuration读取配置的优先级

前言在之前的文章.Net Core Configuration源码探究一文中我们曾解读过Configuration的工作原理&#xff0c;也.Net Core Configuration Etcd数据源一文中探讨过为Configuration自定义数据源需要哪些操作。由于Configuration配置系统也是.Net Core的核心&#xff0c;其中也包含了…

TypeScript,从0到入门带你进入类型的世界

从0到入门进入TS的世界一、什么是TypeScript&#xff1f;1、编程语言的类型2、TypeScript究竟是什么&#xff1f;二、为什么要学习TypeScript&#xff1f;1、程序更容易理解2、效率更高3、更少的错误4、非常好的包容性5、一点小缺点三、typescript入门1、如何安装TypeScript2、…

编写第一个 .NET 微服务

介绍本文的目的是&#xff1a;通过创建一个返回列表的简单服务&#xff0c;并在 Docker 容器中运行该服务&#xff0c;让您熟悉使用 .NET 创建微服务的构建过程。安装 .NET SDK要开始构建 .NET 应用程序&#xff0c;首先下载并安装 .NET Core SDK&#xff08;软件开发工具包&am…

模板编译template的背后,究竟发生了什么事?带你了解template的纸短情长

解析模板编译template的背后发生了什么一、&#x1f4d1;初识模板编译1、vue组件中使用render代替template2、模板编译总结二、✏️感受模板编译的美1、with语法&#xff08;1&#xff09;例子展示&#x1f330;&#xff08;2&#xff09;知识点归纳三、&#x1f4c8;编译模板1…

leetcode24. 两两交换链表中的节点(思路+解析)

一:题目 二:思路 思路: 1.分析题意 这是相邻结点进行交换 如果是4个结点 那么1和2交换 3和4交换 如果是3个结点 那么就1和2进行交换 3不动 2.这里我们定义一个虚拟头节点方便操作&#xff0c;我们只需三步实现结点的交换 <1>:让虚拟结点指向第二个结点(进行交换的结点我…

把Autofac玩的和java Spring一样6

大家好&#xff0c;今天来介绍我开源的一个autofac.Annotation项目 源码&#xff1a;https://github.com/yuzd/Autofac.Annotation本项目是autofa的一个扩展组件&#xff0c;autofac是一个老牌的DI容器框架 &#xff0c;支持netframework和netcoreAnnotdation是注解的意思&…

『软件测试5』测开岗只要求会黑白盒测试?NO!还要学会性能测试!

浅谈软件测试中的性能测试一、&#x1f92a;性能测试概念1、为什么要有性能测试&#xff1f;2、性能测试是什么&#xff1f;3、性能测试的目的二、&#x1f910;性能测试指标1、响应时间2、吞吐量3、并发用户数4、TPS(Transaction Per Second)5、点击率6、资源利用率三、&#…

CLR的简单理解

CLR加载程序生成进程&#xff0c;一个进程中可以存在多个线程&#xff0c;当创建一个线程时&#xff0c;会分配1Mb的空间&#xff0c;也就是线程的栈空间&#xff0c;对应jvm的虚拟机堆栈&#xff0c;是线程执行过程中用到的工作内存。这片内存用于方法传递实参&#xff0c;并存…

『软件测试6』bug一两是小事,但安全漏洞是大事!

详解软件测试中的安全测试一、&#x1f4bf;安全测试概念1、安全测试概述2、安全测试与软件生命周期的关系3、常规测试与安全测试的不同&#xff08;1&#xff09;测试目标不同&#xff08;2&#xff09;假设条件不同&#xff08;3&#xff09;思考域不同&#xff08;4&#xf…

我们真的需要JWT吗?

JWT&#xff08;JSON Web Token&#xff09;是目前最流行的认证方案之一。博客园、各种技术公众号隔三差五就会推一篇JWT相关的文章&#xff0c;真的多如牛毛。但我对JWT有点困惑&#xff0c;今天写出来跟大家探讨探讨&#xff0c;不要喷哈。JWT原理本文默认读者已经对JWT有所了…

leetcode面试题 02.07. 链表相交

一:题目 二:思路 1.这道题我们是需要找到一个结点&#xff0c;并且从这个结点往后的结点都相等 2.我们需要将两个链表 右对齐 3.然后将长链表的指针移动到和短链表头结点相同的位置 4.接下来就是比较指针&#xff0c;当一个指针相同也就意味着往后的结点的数值也相等 三:上码…

详解队列在前端的应用,深剖JS中的事件循环Eventloop,再了解微任务和宏任务

队列在前端中的应用一、队列是什么二、应用场景三、前端与队列&#xff1a;事件循环与任务队列1、event loop2、JS如何执行3、event loop过程4、 DOM 事件和 event loop5、event loop 总结四、宏任务和微任务1、引例2、宏任务和微任务&#xff08;1&#xff09;常用的宏任务和微…

终于弄明白了 Singleton,Transient,Scoped 的作用域是如何实现的

一&#xff1a;背景1. 讲故事前几天有位朋友让我有时间分析一下 aspnetcore 中为什么向 ServiceCollection 中注入的 Class 可以做到 Singleton&#xff0c;Transient&#xff0c;Scoped&#xff0c;挺有意思&#xff0c;这篇就来聊一聊这一话题&#xff0c;自从 core 中有了 S…

leetcode142. 环形链表 II(暴力+双链表)

一:题目 二:思路 1.双指针 快慢指针(快指针一次一个结点&#xff0c;慢指针一次两个结点) 2.如果有环的话&#xff0c;那么快慢指针肯定会相遇 3.那么相遇的地点一定在环中 因为如果没有环的话慢指针是永远追不到快指针的 4.接下来就是判断出口在那里&#xff0c;我们定义一个…

动态 Restful API 生成

介绍通常在DDD开发架构中&#xff0c;我们写完服务层需要在控制器中写API&#xff0c;今天介绍一个组件 Plus.AutoApi 可以用它来动态生成 Restful 风格的 WebApi&#xff0c;不用写 Controller。快速使用在你的应用服务层中添加组件Install-Package Plus.AutoApi在 Startup 中…

卷死了!再不学vue3就没有人要你了!速来围观vue3新特性

一文全面了解vue3新特性一、&#x1f636;vue3比vue2有什么优势&#xff1f;二、&#x1f9d0;Vue3升级了哪些重要的功能1、createApp2、emits(父子组件间的通信)&#xff08;1&#xff09;通信方式&#xff08;2&#xff09;举个例子&#x1f330;3、多事件处理4、Fragment5、…

idea报错Class not found (在target中没有生成对应的class文件)

一&#xff1a;问题描述 二:解决 既然他不自动生成&#xff0c;那么我们就手动导入&#xff1b; 点击后应用 然后再次运行我们的测试用例&#xff1b;如果不行 再取消勾选 然后再运行我们的测试用例

敲黑板!vue3重点!一文了解Composition API新特性:ref、toRef、toRefs

一文了解Composition API新特性&#xff1a;ref、toRef、toRefs一、&#x1f64e;如何理解ref、toRef和toRefs1、ref、toRef和toRefs是什么&#xff08;1&#xff09;ref1&#xff09;ref是什么2&#xff09;举个例子&#x1f330;&#xff08;2&#xff09;toRef是什么1&#…