您现在的位置是:首页 > 正文

Vue原理-diff比对算法

2024-04-01 00:45:18阅读 3

diff比对算法

源码版

https://blog.csdn.net/s2422617864/article/details/119855400

原理版
path函数
  1. 如果是同一个就会把真实的转化为虚拟的 如果不是则直接替换

  2. 将真实dom转化为虚拟dom形式

  3. 根据生成的新的虚拟dom生成新的节点 插入到页面中(注意此处涉及到父节点需要考虑子节点的遍历)[当结点不同时]

  4. 新节点与老节点类型相同时

    1. 新节点没有子节点–直接覆盖
    2. 新节点有子节点老节点没有 --直接覆盖
    3. 新的有老的也有–最复杂的情况要深度讨论
  5. 新的也有老的也有(以下6条规则每次对比都是从第一条开始对比 没匹配上则继续向下)

    1. 旧前和新前
      • 匹配上则新旧指针同时向后++
      • 未匹配则走2
    2. 旧后和新后
      • 匹配上则指针同时向前
      • 未匹配则走3
    3. 旧前和新后
      • 匹配上则旧指针往后新指针向前
      • 未匹配则走4
    4. 旧后和新前
      • 匹配上则新指针往后旧指针向前
      • 未匹配则走5
    5. 新的指针向后,将新的元素添加到页面上 所添加的元素如果旧的里面有则设置旧中的元素为undefined(这里有个操作如果是遇到undefined则继续向后查找)
    6. 创建或删除(创建没有的 删除多余的)

首先:

  • h函数用于生成虚拟节点,path比对新老虚拟节点之后替换真实dom树
  • path算法替换新老节点 没有key暴力删除 有key按照key判断然后调整顺序(如果节点为同一节点致)
  • 且path比对算法只能同层比较不能跨层比较
手写diff算法
整体代码

就是单纯将js转换为一个虚拟dom对象的形式(包含类型、内容、子节点、key等)

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;
}

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 );
	}
}

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);
			}
		}
	}
}

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];
		}
	}

	//结束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);
		}	
	}

}	

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);
	}

}

vnode.js

export default function( sel , data , children , text , elm  ){

	let key = data.key;
	return {
		sel, 
		data, 
		children, 
		text, 
		elm,
		key
	}

}

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 );
}

网站文章

  • svn搭建

    svn搭建

    一、首先准备三个软件: 1.VisualSVN-Server-3.6.3-x64.msi(svn服务端) 2.TortoiseSVN-1.9.6.27867-x64-svn-1.9.6.msi(svn客户端) 3.LanguagePack_1.9.6.27867-x64-zh_CN.msi(TortoiseSVN 的汉化包) 软件下载地址:http://

    2024-04-01 00:45:09
  • Playwright之录制

    Playwright之录制

    前言前段时间看了大佬分享的关于Playwright.NET的文章感觉挺有意思,想要阅读点击:此处,然后跟随大佬的脚步,学习了一点自动化玩,其中有一个录制功能感觉挺好玩,下面就来简单看看介绍手动操作浏览器,会录制我们的操作,然后生成脚本。操作创建项目--创建控制台(这点需要注意,会直接安装最新版本) dotnetnewconsole-nPlaywrightDemo...

    2024-04-01 00:45:01
  • java String字符串与二维数组互相转换

    com.alibaba fastjson 1.2.40 字符串转数组: String s = &quot;[[22,23,23],[1,10,20]]&quot;; //字符串转换成二维数组 .

    2024-04-01 00:44:37
  • 嵌入式实时操作系统的设计与开发 (启动过程学习)

    嵌入式实时操作系统的设计与开发 (启动过程学习)

    在ARM中用户模式与系统模式使用的是相同的寄存器,系统模式与用户模式共用堆栈。

    2024-04-01 00:44:25
  • mutipass安装ubuntu 20.04的桌面xfce4

    1/xfce4先依次安装依赖,再安装xfce4 x11-xkb-utils libxklavier16 xfce4-session xfce4-pulseaudio-plugin libpulse-m...

    2024-04-01 00:44:19
  • C++实现通讯管理系统

    本教程主要利用C++来实现一个通讯录管理系统系统中需要实现的功能如下:1、添加联系人:向通讯录中添加新人,信息包括(姓名、性别、年龄、联系电话、家庭住址)最多记录1000人;2、显示联系人:显示通讯录...

    2024-04-01 00:43:45
  • java Web 学习案例

    Javaweb_bookstore/BookStore at master · eson15/Javaweb_bookstore · GitHub参考了这个案例,目前照着敲了一遍,理解了一下。目前添加了一个删除订单的按钮,需要待完善的因为学习了如何将本地项目上传到github,所以将我本地练习的也上传到github上了https://github.com/zoeyqq/b...

    2024-04-01 00:43:39
  • 设计模式9——装饰模式(结构型模式)

    设计模式9——装饰模式(结构型模式)

    本文的内容参考了以下博客和《大话设计模式》:https://www.cnblogs.com/jzb-blog/p/6717349.html装饰模式是一种常见的设计模式,个人理解装饰就是锦上添花之意,即在原有功能基础上增加新功能。这个模式的设计思想和实现方式比较简单,直接上图。UML标题Component 为统一接口,也是装饰类和被装饰类的基本类型。 Concrete...

    2024-04-01 00:43:32
  • 数据库的隔离级别

    标题SQL标准中定义了四种隔离级别1.未提交读(READ UNCOMMITTED)在未提交读级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读(Dirty Read)。性能来说未提交读不会比其他的级别好太多,所以除非真的非常必要的理由,在实际应用中不推荐使用。2.提交读(READ COMMITTED)(也叫不可重复读nonrepeatable)...

    2024-04-01 00:43:07
  • Win10上使用WSL安装Centos8 最新发布

    Win10上使用WSL安装Centos8 最新发布

    解压后,您将在目标目录中看到2个文件:rootfs.tar.gz和CentOS.exe。在解压的目录,以管理员身份运行。,以便解压其中的文件并注册到。解压在任何目录都可以。

    2024-04-01 00:42:59