Vue原理-diff比对算法

Posted 火腿肠烧烤大赛冠军

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue原理-diff比对算法相关的知识,希望对你有一定的参考价值。

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

以上是关于Vue原理-diff比对算法的主要内容,如果未能解决你的问题,请参考以下文章

vue和react的diff算法的区别

vue虚拟dom原理

web前端diff 算法深入一下?

你怎么理解vue中的diff算法?

Vue原理-虚拟DOM和diff算法

React diff算法