关于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,旧的没有
      创建元素添加(把旧的内容删除清空掉,增加新的)

下面详细说说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.54.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 );
}

界面初始:
在这里插入图片描述

点击按钮之后,列表变化了。同时控制台依次打印为3331

在这里插入图片描述

这个应该很好理解。

  • 一开始界面如下:
    在这里插入图片描述
  • 如上,两个虚拟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);
};

页面初始:
在这里插入图片描述
点击按钮:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值