面试中的网红虚拟DOM,你知多少呢?深入解读diff算法
众所周知,在前端的面试中,面试官非常爱考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 算法 。也就是说他们两者是包含关系,如下图所示:
可以说,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的渲染过程:
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 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>
此时我们来看浏览器的显示效果:
我们可以看到,最终的效果是当我们点击时, 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下的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
时,就可以对新旧节点进行比较。而新旧节点的比较是先判断 key
和 sel
是否相同,如果相同,则用 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) 当旧的 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 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);
}
}
}
我们先来看两张图:
大家先看图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
的讲解到此就结束啦!如有不理解或文章有误欢迎留言交流~