关于Vue的Diff算法
1.概述
1.1 什么是diff算法
diff
就是比较两个树,render
会生成两颗树,一个新树newVnode
,一棵旧树oldVnode
。然后两棵树进行对比更新差异就是diff
,全称是difference
, 在vue
里面diff
算法就是通过patch
函数来完成的,所以有的时候也叫patch
算法。目的是为了提升性能。
1.2 关于虚拟dom
虚拟 DOM是一种通过 JavaScript 对象模拟真实 DOM 树结构的技术。它的主要目标是提高页面渲染的效率,减少浏览器 DOM 操作的频繁性。虚拟 DOM 并不是一种新的 DOM 技术,而是一种通过抽象层的设计,使得对 UI 更新的操作变得更加高效的技术。
1.3 操作dom和操作数据的对比
第一种方式:操作dom
<div id="box">11111</div>
<script>
let box = document.querySelector("#box");
console.time("a");
for (let i = 0; i <= 10000; i++) {
box.innerHTML = i;
}
console.timeEnd("a");
</script>
第二种方式:数据化
<body>
<div id="box">11111</div>
<script>
let box = document.querySelector("#box");
console.time("a");
let count = 0;
for (let i = 0; i <= 10000; i++) {
count = i;
}
box.innerHTML = count;
console.timeEnd("a");
</script>
</body>
可以发现第二种方式所用的时间比较短。因为它减少了回流重绘的次数,这个与浏览器渲染原理有关。详情见我写的文章https://blog.csdn.net/fageaaa/article/details/146296227。
1.4 主流的思路
-
snabbdom https://www.npmjs.com/package/snabbdom
Vue2的Diff算法是基于snabbdom实现,通过同层比较和双端比较策略来减少不必要的DOM操作。如果新旧vnode相同,则直接返回;否则,更新子节点或新增/删除节点。Vue 3的Diff算法:通过LIS优化,减少DOM操作,提高性能,尤其是在处理大规模列表时表现更优 -
virtual-dom
2.关于snabbdom
2.1 环境搭建
npm init -y
cnpm install webpack webpack-cli webpack-dev-server html-webpack-plugin -S
cnpm install snabbdom -S
新建webpack.config.js
:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "[name].js",
// 注意这个dist的路径设置成上一级
path: path.resolve(__dirname, "./dist"),
clean: true,
},
plugins: [new HtmlWebpackPlugin(
template: "./public/index.html", //指向的html
)],
};
新建src/index.js
,然后去https://www.npmjs.com/package/snabbdom
(snabbdom官网地址),找到Example
:
把示例代码复制到src/index.js
中(要进行适当修改)
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");
const btn = document.getElementById('btn');
const vnode1 = h('ul',{},[
h('li',{key:'a'},'a'),
h('li',{key:'b'},'b'),
h('li',{key:'c'},'c')
]);
patch(container,vnode1);
const vnode2 = h('ul',{},[
h('li',{key:'b'},'b'),
h('li',{key:'c'},'c')
]);
console.log( vnode2 )
btn.onclick = function(){
patch(vnode1,vnode2);
}
2.2 虚拟节点和真实节点
虚拟节点:
{
children: undefined
data: {}
elm: h1 //真实的dom节点
key: undefined
sel: "h1"
text: "你好h1"
}
真实节点:
<h2>你好</h2>
我们再来看一个例子,如上有一部分代码:
const vnode2 = h('ul',{},[
h('li',{key:'b'},'b'),
h('li',{key:'c'},'c')
]);
console.log( vnode2 )
我们去看看这个虚拟节点vnode2
:
2.3 新老节点替换的规则
(1) 如果新老节点不是同一个节点名称(标签),那么就暴力删除旧的节点,创建插入新的节点。
(2) 只能同级比较,不能跨层比较。如果跨层那么就暴力删除旧的节点,创建插入新的节点。
(3) 如果是相同节点,又分为很多情况
- 新节点有没有children
如果新的节点没有children,那就证明新节点是文本,那直接把旧的替换成新的文本 - 新节点有children
- 新的有children,旧的也有children
这就涉及到diff算法的核心了 - 新的有children,旧的没有
创建元素添加(把旧的内容删除清空掉,增加新的)
- 新的有children,旧的也有children
下面详细说说diff算法的核心部分(最复杂的情况):
- 第一步:
旧前
和新前
匹配:旧前的指针++ 、 新前的指针++ - 第二步:
旧后
和新后
匹配:旧后的指针-- 、 新后的指针– - 第三步:
旧前
和新后
匹配:旧前的指针++ 、 新后的指针– - 第四步:
旧后
和新前
匹配:旧后的指针-- 、 新前的指针++ - 第五步:以上都不满足条件,就会查找
新的指针++,新的添加到页面上并且新在旧的中有,要给旧的复制成undefined - 第六步:创建或者删除
现在可能不好理解这些。下面直接手写diff算法来辅助理解。
3.手写diff算法
3.1 生成虚拟dom
先看看虚拟dom的大概样子:
//真实节点:
<h2>你好</h2>
//虚拟节点
{
children: undefined
data: {}
elm: h1 //真实的dom节点
key: undefined
sel: "h1"
text: "你好h1"
}
src/index.js:
//src/index.js:
import h from "./dom/h";
let vnode1 = h("div", {}, "你好吖");
console.log(vnode1);
let vnode2 = h("ul", {}, [
h("li", {}, "a"),
h("li", {}, "b"),
h("li", {}, "c"),
h("li", {}, "你把花木兰都沉默"),
]);
console.log(vnode2);
src/dom/h.js:
//src/dom/h.js
import vnode from "./vnode";
export default function (sel, data, params) {
//h函数的 第三个参数是字符串类型【意味着:他没有子元素】
if (typeof params == "string") {
return vnode(sel, data, undefined, params, undefined);
} else if (Array.isArray(params)) {
//h函数的第三个参数,是不是数组,如果是数组【意味着:有子元素】
let children = [];
for (let item of params) {
children.push(item);
}
return vnode(sel, data, children,undefined,undefined);
}
}
src/dom/vnode.js:
//src/dom/vnode.js:
export default function( sel , data , children , text , elm ){
return {
sel,
data,
children,
text,
elm
}
}
看index.js
中打印了vnode1
、vnode2
两个虚拟节点
3.2 patch不是同一个节点类型
src/dom/patch.js:
//src/dom/patch.js:
//oldVnode ===> 旧虚拟节点
//newVnode ===> 新虚拟节点
import vnode from './vnode';
import createElement from './createElement'
export default function( oldVnode , newVnode ){
//如果oldVnode 没有sel ,就证明是非虚拟节点 ( 就让他变成虚拟节点 )
if( oldVnode.sel == undefined ){
oldVnode = vnode(
oldVnode.tagName.toLowerCase(), //sel
{},//data
[],
undefined,
oldVnode
)
}
//判断 旧的虚拟节点 和 新的虚拟节点 是不是同一个节点
if( oldVnode.sel === newVnode.sel ){
//判断就条件就复杂了(很多了)
}else{//不是同一个节点,那么就暴力删除旧的节点,创建插入新的节点。
//把新的虚拟节点 创建为 dom节点
let newVnodeElm = createElement( newVnode );
//获取旧的虚拟节点 .elm 就是真正节点
let oldVnodeElm = oldVnode.elm;
//创建新的节点
if( newVnodeElm ){
oldVnodeElm.parentNode.insertBefore(newVnodeElm ,oldVnodeElm);
}
//删除旧节点
oldVnodeElm.parentNode.removeChild( oldVnodeElm );
}
}
src/dom/createElement.js:
//src/dom/createElement.js:
//vnode 为新节点,就是要创建的节点
export default function createElement( vnode ){
//创建dom节点
let domNode = document.createElement( vnode.sel );
//判断有没有子节点 children 是不是为undefined
if( vnode.children == undefined ){
domNode.innerText = vnode.text;
}else if( Array.isArray(vnode.children) ){//新的节点有children(子节点)
//说明内部有子节点 , 需要递归创建节点
for( let child of vnode.children){
let childDom = createElement(child);
domNode.appendChild( childDom );
}
}
//补充elm属性
vnode.elm = domNode;
return domNode;
}
下面来测试一下:
src/index.js:
//src/index.js:
import h from './dom/h'
import patch from './dom/patch'
//获取到了真实的dom节点
let container = document.getElementById('container');
//虚拟节点1
//let vnode1 = h('h1',{},'你好吖');
//虚拟节点2
let vnode2 = h('ul',{},[
h('li',{},'a'),
h('li',{},'b'),
h('li',{},'c'),
h('li',{},'你把花木兰都沉默hhhhh')
]);
patch( container,vnode2 );
public/index.html:
//public/index.html:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div id="container">这是container</div>
</body>
</html>
3.3 相同节点类型有没有children
src/dom/patch.js:
//src/dom/patch.js:
//oldVnode ===> 旧虚拟节点
//newVnode ===> 新虚拟节点
import vnode from './vnode';
import createElement from './createElement'
import patchVnode from './patchVnode'
export default function( oldVnode , newVnode ){
//如果oldVnode 没有sel ,就证明是非虚拟节点 ( 就让他变成虚拟节点 )
if( oldVnode.sel == undefined ){
oldVnode = vnode(
oldVnode.tagName.toLowerCase(), //sel
{},//data
[],
undefined,
oldVnode
)
}
//判断 旧的虚拟节点 和 新的虚拟节点 是不是同一个节点
if( oldVnode.sel === newVnode.sel ){
//判断就条件就复杂了(很多了)
patchVnode( oldVnode,newVnode );
}else{//不是同一个节点,那么就暴力删除旧的节点,创建插入新的节点。
//把新的虚拟节点 创建为 dom节点
let newVnodeElm = createElement( newVnode );
//获取旧的虚拟节点 .elm 就是真正节点
let oldVnodeElm = oldVnode.elm;
//创建新的节点
if( newVnodeElm ){
oldVnodeElm.parentNode.insertBefore(newVnodeElm ,oldVnodeElm);
}
//删除旧节点
oldVnodeElm.parentNode.removeChild( oldVnodeElm );
}
}
src/dom/patchVnode.js:
//src/dom/patchVnode.js:
import createElement from "./createElement";
export default function patchVnode(oldVnode, newVnode) {
//判断新节点有没有children(新的没有子节点)
if (newVnode.children === undefined) {
//新节点的文本 和 旧节点的文本内容是不是一样的
if (newVnode.text !== oldVnode.text) {
oldVnode.elm.innerText = newVnode.text;
}
} else {
//新的有子节点
//新的虚拟节点有children ,旧的虚拟节点有children
if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
//最复杂的情况了 diff核心了
console.log("新旧都有children");
} else {
//新的虚拟节点有children,旧的虚拟节点没有children
//把旧节点的内容清空
oldVnode.elm.innerHTML = "";//这只是示意
//遍历新的子节点,创建dom元素,添加到页面中
for (let child of newVnode.children) {
let childDom = createElement(child);
oldVnode.elm.appendChild(childDom);
}
}
}
}
下面来测试一下:
src/index.js:
//src/index.js:
import h from "./dom/h";
import patch from "./dom/patch";
//获取到了真实的dom节点
let container = document.getElementById("container");
//获取到了按钮
let btn = document.getElementById("btn");
//虚拟节点
let vnode1 = h("ul", {}, [
h("li", {}, "a"),
h("li", {}, "b"),
h("li", {}, "c"),
]);
btn.onclick = function () {
patch(container, vnode1);
};
public/index.html:
//public/index.html:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div id="container">这是container</div>
<button id="btn">按钮</button>
</body>
</html>
页面初始如下:
点击按钮后:
4.diff算法的核心部分
4.1 diff算法的核心理论
上面我说了diff算法核心部分步骤:
现在要好好分析这一块!!!
4.1.1 第一步:旧前和新前
ps:下面的这些虚拟dom默认都是带key。
- 如上,两个虚拟dom做比对。
旧前和新前
的指针首先都指向0
- 首先使用
旧前和新前
策略进行比对,都是a
(满足条件
),旧前和新前
的两个指针都++
,指向1
- 进行下一次循环,还是首先使用
旧前和新前
策略进行比对,都是b
(满足条件
),旧前和新前
的两个指针都++
,指向2
- 进行下一次循环,还是首先使用
旧前和新前
策略进行比对,都是c
(满足条件
),旧前和新前
的两个指针都++
,指向3
- 进行下一次循环,还是首先使用
旧前和新前
策略进行比对,都是d
(满足条件
)。旧前和新前
的两个指针都++
,这时候超出范围了,会退出循环
4.1.1 第二步:旧后和新后
- 如上,两个虚拟dom做比对。
旧前和新前
的指针首先都在0
;旧后和新后
的指针都指向2
; - 首先使用
旧前和新前
策略进行比对,旧节点是b
新节点是a
(不满足条件
) - 使用
旧后和新后
策略进行比对,都是c
(满足条件
),新后和旧后
两个指针都--
,指向1
- 进行下一次循环,首先还是使用
旧前和新前
策略进行比对,旧节点是b
新节点是a
(不满足条件
) - …
4.1.3 第三步:旧前和新后
- 如上,两个虚拟dom做比对。
旧前和新前
的指针首先都在0
;旧后和新后
的指针都指向3
; - 首先使用
旧前和新前
策略进行比对,都是a
(满足条件
),旧前和新前
的两个指针都++
,指向1
- 进行下一次循环,首先使用
旧前和新前
策略进行比对,都是c
(满足条件
),旧前和新前
两个指针都++
,指向2
- 进行下一次循环,首先使用
旧前和新前
策略进行比对,旧前
指针指向c
,新前
指针指向d
(不满足条件
) - 使用
旧后和新后
策略进行比对,旧后指针
指向d
,新后节点
指向c
(不满足条件
) - 使用
旧前和新后
策略进行比对,旧前指针
指向c
,新后节点
指向c
(满足条件
),旧前
指针++
(指向3
),新后
指针--
(指向2
)
此时旧后
指针指向3
,新前
指针指向2
- 进行下一次循环,首先使用
旧前和新前
策略进行比对,旧前
指针指向d
,新前
指针指向c
(满足条件
),退出循环
4.1.4 第四步:旧后和新前
按照上面的思路,应该很好理解。在这就不多说
4.1.5 以上四个都不满足,就会查找
新的指针++,新的添加到页面上并且新在旧的中有,要给旧的复制成undefined。
4.1.6 创建和删除
4.1.5
和4.1.6
将在下面的代码中详细讲清楚。
4.2 手写diff核心:判断前四种情况
生成虚拟dom时候将key
也带上。
src/dom/vnode/.js:
//src/dom/vnode/.js
export default function (sel, data, children, text, elm) {
let key = data.key;
return {
sel,
data,
children,
text,
elm,
key,
};
}
src/dom/patchVnode.js:
//src/dom/patchVnode.js
import createElement from './createElement'
import updateChildren from './updateChildren'
export default function patchVnode( oldVnode,newVnode ){
//判断新节点有没有children
if( newVnode.children === undefined ){ //新的没有子节点
//新节点的文本 和 旧节点的文本内容是不是一样的
if( newVnode.text !== oldVnode.text ){
oldVnode.elm.innerText = newVnode.text;
}
}else{//新的有子节点
//新的虚拟节点有 , 旧的虚拟节点有
if( oldVnode.children !== undefined && oldVnode.children.length > 0 ){
//最复杂的情况了 diff核心了
updateChildren( oldVnode.elm , oldVnode.children , newVnode.children )
}else{//新的虚拟节点有 , 旧的虚拟节点“没有”
//把旧节点的内容 清空
oldVnode.elm.innerHTML = '';
//遍历新的 子节点 , 创建dom元素,添加到页面中
for( let child of newVnode.children ){
let childDom = createElement(child);
oldVnode.elm.appendChild(childDom);
}
}
}
}
src/dom/updateChildren.js:
//src/dom/updateChildren.js
import patchVnode from './patchVnode'
//判断倆个虚拟节点是否为同一个节点
function sameVnode( vNode1, vNode2 ){
return vNode1.key == vNode2.key;
}
//参数一:真实dom节点
//参数二:旧的虚拟节点
//参数三:新的虚拟节点
export default ( parentElm , oldCh , newCh ) => {
let oldStartIdx = 0; //旧前的指针
let oldEndIdx = oldCh.length-1; //旧后的指针
let newStartIdx = 0; //新前的指针
let newEndIdx = newCh.length-1; //新后的指针
let oldStartVnode = oldCh[0]; //旧前虚拟节点
let oldEndVnode = oldCh[oldEndIdx]; //旧后虚拟节点
let newStartVnode = newCh[0]; //新前虚拟节点
let newEndVnode = newCh[newEndIdx]; //新后虚拟节点
while( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx ){
if( sameVnode( oldStartVnode,newStartVnode ) ){
//第一种情况:旧前 和 新前
console.log('1');
patchVnode( oldStartVnode,newStartVnode );
if( newStartVnode ) newStartVnode.elm = oldStartVnode?.elm;
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}else if( sameVnode( oldEndVnode,newEndVnode ) ){
//第二种情况:旧后 和 新后
console.log('2');
patchVnode( oldEndVnode,newEndVnode );
if( newEndVnode ) newEndVnode.elm = oldEndVnode?.elm;
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}else if( sameVnode( oldStartVnode,newEndVnode ) ){
//第三种情况:旧前 和 新后
console.log('3');
patchVnode( oldStartVnode,newEndVnode );
if( newEndVnode ) newEndVnode.elm = oldStartVnode?.elm;
//把旧前指定的节点移动到旧后指向的节点的后面
parentElm.insertBefore( oldStartVnode.elm , oldEndVnode.elm.nextSibling );
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}else if( sameVnode( oldEndVnode,newStartVnode ) ){
//第四种情况:旧后 和 新前
console.log('4');
patchVnode( oldEndVnode,newStartVnode );
if( newStartVnode ) newStartVnode.elm = oldEndVnode?.elm;
//将旧后指定的节点移动到旧前指向的节点的前面
parentElm.insertBefore( oldEndVnode.elm , oldStartVnode.elm );
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}else{
//第五种情况:以上都不满足条件 ===》查找
}
}
}
下面来测试一下:
src/index.js:
import h from './dom/h'
import patch from './dom/patch'
//获取到了真实的dom节点
let container = document.getElementById('container');
//获取到了按钮
let btn = document.getElementById('btn');
//虚拟节点
let vnode1 = h('ul',{},[
h('li',{key:'a'},'a'),
h('li',{key:'b'},'b'),
h('li',{key:'c'},'c'),
h('li',{key:'d'},'d'),
]);
patch( container,vnode1 );
let vnode2 = h('ul',{},[
h('li',{key:'d'},'d'),
h('li',{key:'c'},'c'),
h('li',{key:'b'},'b'),
h('li',{key:'a'},'a'),
]);
btn.onclick = function(){
patch( vnode1,vnode2 );
}
界面初始:
点击按钮之后,列表变化了。同时控制台依次打印为3
、3
、3
、1
。
这个应该很好理解。
- 一开始界面如下:
- 如上,两个虚拟dom做比对。
旧前
指针指向a
,旧后
的指针在d
;新前
指针指向d
,和新后
的指针在a
- 首先使用
旧前和新前
策略进行比对,旧前
指针指向a
,新前
指针指向d
,不一致(不满足条件
) - 使用
旧后和新后
策略进行比对,旧后指针
指向d
,新后节点
指向a
(不满足条件
) - 使用
旧前和新后
策略进行比对,旧前指针
指向a
,新后节点
指向a
(满足条件
)。说明是同一节点。打印3
,同时旧前
指针++
,新后
指针--
- 进行下一次循环,首先使用
旧前和新前
策略进行比对,旧前
指针指向b
,新前
指针指向d
,不一致(不满足条件
) - 使用
旧后和新后
策略进行比对,旧后指针
指向d
,新后节点
指向b
(不满足条件
) - 使用
旧前和新后
策略进行比对,旧前指针
指向b
,新后节点
指向b
(满足条件
)。说明是同一节点。打印3
,同时旧前
指针++
,新后
指针--
- 进行下一次循环,首先使用
旧前和新前
策略进行比对,旧前
指针指向c
,新前
指针指向d
,不一致(不满足条件
) - 使用
旧后和新后
策略进行比对,旧后指针
指向d
,新后节点
指向c
(不满足条件
) - 使用
旧前和新后
策略进行比对,旧前指针
指向c
,新后节点
指向c
(满足条件
)。说明是同一节点。打印3
,同时旧前
指针++
,新后
指针--
- 进行下一次循环,首先使用
旧前和新前
策略进行比对,旧前
指针指向d
,新前
指针指向d
(满足条件
),说明是同一节点。打印1
,同时旧前
指针++
,新前
指针++
。 - 此时
旧前
指针>旧后
指针,新前
指针>新后
指针,会退出循环。
4.3 手写diff核心:判断第五种情况
src/dom/updateChildren.js:
//src/dom/updateChildren.js
import patchVnode from "./patchVnode";
import createElement from "./createElement";
//判断倆个虚拟节点是否为同一个节点
function sameVnode(vNode1, vNode2) {
return vNode1.key == vNode2.key;
}
//参数一:真实dom节点
//参数二:旧的虚拟节点
//参数三:新的虚拟节点
export default (parentElm, oldCh, newCh) => {
let oldStartIdx = 0; //旧前的指针
let oldEndIdx = oldCh.length - 1; //旧后的指针
let newStartIdx = 0; //新前的指针
let newEndIdx = newCh.length - 1; //新后的指针
let oldStartVnode = oldCh[0]; //旧前虚拟节点
let oldEndVnode = oldCh[oldEndIdx]; //旧后虚拟节点
let newStartVnode = newCh[0]; //新前虚拟节点
let newEndVnode = newCh[newEndIdx]; //新后虚拟节点
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == undefined) {
oldStartVnode = oldCh[++oldStartIdx];
}
if (oldEndVnode == undefined) {
oldEndVnode = oldCh[--oldEndVnode];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
//第一种情况:旧前 和 新前
console.log("1");
patchVnode(oldStartVnode, newStartVnode);
if (newStartVnode) newStartVnode.elm = oldStartVnode?.elm;
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
//第二种情况:旧后 和 新后
console.log("2");
patchVnode(oldEndVnode, newEndVnode);
if (newEndVnode) newEndVnode.elm = oldEndVnode?.elm;
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
//第三种情况:旧前 和 新后
console.log("3");
patchVnode(oldStartVnode, newEndVnode);
if (newEndVnode) newEndVnode.elm = oldStartVnode?.elm;
//把旧前指定的节点移动到旧后指向的节点的后面
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
//第四种情况:旧后 和 新前
console.log("4");
patchVnode(oldEndVnode, newStartVnode);
if (newStartVnode) newStartVnode.elm = oldEndVnode?.elm;
//将旧后指定的节点移动到旧前指向的节点的前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
//第五种情况:以上都不满足条件 ===》查找
console.log("5");
//创建一个对象,存虚拟节点的(判断新旧有没有相同节点)
const keyMap = {};
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i]?.key;
if (key) keyMap[key] = i;
}
//在旧节点中寻找新前指向的节点
let idxInOld = keyMap[newStartVnode.key];
//如果有,说明数据在新旧虚拟节点中都存在
if (idxInOld) {
const elmMove = oldCh[idxInOld];
patchVnode(elmMove, newStartVnode);
//处理过的节点,在旧虚拟节点的数组中,设置为undefined
oldCh[idxInOld] = undefined;
parentElm.insertBefore(elmMove.elm, oldStartVnode.elm);
} else {
//如果没有找到==》说明是一个新的节点【创建】
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
}
//新数据(指针)+1
newStartVnode = newCh[++newStartIdx];
}
}
};
4.4 手写diff核心:创建和删除
src/dom/updateChildren.js:
//src/dom/updateChildren.js
import patchVnode from "./patchVnode";
import createElement from "./createElement";
//判断倆个虚拟节点是否为同一个节点
function sameVnode(vNode1, vNode2) {
return vNode1.key == vNode2.key;
}
//参数一:真实dom节点
//参数二:旧的虚拟节点
//参数三:新的虚拟节点
export default (parentElm, oldCh, newCh) => {
let oldStartIdx = 0; //旧前的指针
let oldEndIdx = oldCh.length - 1; //旧后的指针
let newStartIdx = 0; //新前的指针
let newEndIdx = newCh.length - 1; //新后的指针
let oldStartVnode = oldCh[0]; //旧前虚拟节点
let oldEndVnode = oldCh[oldEndIdx]; //旧后虚拟节点
let newStartVnode = newCh[0]; //新前虚拟节点
let newEndVnode = newCh[newEndIdx]; //新后虚拟节点
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
...
}
//下面的判断不是很严谨
//结束while 只有俩种情况 (新增和删除)
//1. oldStartIdx > oldEndIdx
//2. newStartIdx > newEndIdx
if (oldStartIdx > oldEndIdx) {
const before = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null;
for (let i = newStartIdx; i <= newEndIdx; i++) {
parentElm.insertBefore(createElement(newCh[i]), before);
}
} else {
//进入删除操作
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
parentElm.removeChild(oldCh[i].elm);
}
}
};
测试一下:
//src/index.js
import h from "./dom/h";
import patch from "./dom/patch";
//获取到了真实的dom节点
let container = document.getElementById("container");
//获取到了按钮
let btn = document.getElementById("btn");
//虚拟节点
let vnode1 = h("ul", {}, [
h("li", { key: "a" }, "a"),
h("li", { key: "b" }, "b"),
h("li", { key: "c" }, "c"),
]);
patch(container, vnode1);
let vnode2 = h("ul", {}, [
h("li", { key: "a" }, "a"),
h("li", { key: "b" }, "b"),
h("li", { key: "c" }, "c"),
h("li", { key: "d" }, "d"),
]);
btn.onclick = function () {
patch(vnode1, vnode2);
};
页面初始:
点击按钮: