ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Vue原理-diff比对算法

2021-12-18 16:02:35  阅读:184  来源: 互联网

标签:Vue oldVnode elm 算法 虚拟 let key 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,oldVnode,elm,算法,虚拟,let,key,diff,节点
来源: https://blog.csdn.net/s2422617864/article/details/122013265

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有