vue之虚拟dom及diff算法(一)

当数据变化时,vue是如何更新节点的?

渲染真实DOM的开销是很大的,对DOM的操作会引起整个dom树的重绘和重排。因此,vue采用虚拟dom来对节点进行更新。比如当某个div的属性发生变化时,这时候只需要比较变化前的oldVnode和变换后的Vnode,删除多余属性,更新属性或添加新属性,而不需要删除整个dom元素。

diff过程

diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁

虚拟dom

virtual DOM 是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构。

真实DOMvirual DOM
<div> <p>123</p></div>var Vnode = {tag:'div', data:{} children: [{tag: 'p', text: '123'}]}
patch

diff比较新旧节点的时候,只会在同层级进行,不会跨层级比较。

开始

如图所示,即是数据变化前后,新旧节点(虚拟dom)的比较流程。
在这里插入图片描述

function patch(oldVnode,vnode)
.....  // some code
if(sameVnode(oldVnode,vnode)){
		patchVnode(oldVnode,vnode)
}else{
		// 新旧节点不一样时
		const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
		let parentEle = api.parentNode(oEL)   //父元素
		createEle(vnode)   //根据Vnode生成新元素
		if(parentEle !== null){
				api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))	//插入新节点
				api.removeChild(parentEle, oldVnode.el)  //移除旧节点
		 }
  ....  // some code
 return vnode
}

整个patch过程,先检查新旧节点是否相似,

  • 如果不相似,则根据vnode生成新dom节点插入到父元素里,并删除旧的节点
  • 如果相似,则先对data比较,包括class、style、event、props、attrs等,有不同就调用对应的update函数,然后对子节点children进行比较,children的比较用到了diff算法
function sameVnode(a,b){
	return (a.key ==== b.key && ( 
	(a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a,b))  ||
	(isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error))
	)
}

简单来说,sameVnode就是比较是否有相同的key和tag。

简化后的vnode大致包含以下属性:

{
	tag:'div',
	data:{},  //属性数据, 包括class style event props attrs等
	children: [],  //子节点数组
	text: undefined, //文本
	elem: undefined, // 真实dom
	key: undefined  //节点标识
}
patch过程(包括diff算法)
patchVnode(oldVnode, vnode){
    // 新节点引用旧节点的dom,因此对于新节点来说,vnode.elm 还是指向旧节点的真实dom
	const elm = vnode.elm = oldVnode.elm  // vnode中的elm存放的是(或者说指向的是)真实的dom结构
	let i, oldCh = oldVnode.children, ch = vnode.children
	
	//调用update钩子; 先对data比较,包括class、style、event、props、attrs等
	if(vnode.data){  
		updateAttrs(oldVnode, vnode);
		updateClass(oldVnode, vnode);
		updateEventListeners(oldVnode, vnode);
		updateProps(oldVnode, vnode);
	    updateStyle(oldVnode, vnode);
	}
	
	//判断是否为文本节点
	if(vnode.text == undefined){
		//vnode 不是文本节点
		if(isDef(oldCh) && isDef(ch)){
		 // oldCh 和 ch都存在
			if(oldCh !== ch) updateChidren(elm, oldCh, ch, insertedVnodeQueue)   //diff算法
		}else if (isDef(ch)){
		    // 只有ch存在,oldVnode没有children
			if(isDef(oldVnode.text)) api.setTextContent(elm, '')  // oldVnode如果是文本节点,直接至空
			addVnodes(elm, null, ch, 0, ch.length -1, insertedVnodeQueue)
		} else if (isDef(oldCh)){
		     //只有oldCh存在,vnode没有children
		     removeVnodes(elm, oldCh, 0, oldCh.length -1)
		} else if (isDef(oldVnode.text)){
		     // oldVnode和vNode都没有children,并且,oldVnode是文本节点
			api.setTextContent(elm,'')
		}
	}else if(oldVnode.text !== vnode.text){
		//vnode 是文本节点,并且文本内容已经发生了改变
		api.setTextContent(elm, vnode.text)
	}
}

先对data比较,包括class、style、event、props、attrs等以updateAttrs为例

function updateAttrs(oldVnode,vnode){
	let key,cur, old
	const elm = vnode.elm
	const oldAttrs = oldVnode.data.attrs ||{}
	const attrs = vnode.data.attrs || {}

	for(key in attrs){
		cur = attrs[key] //vnode中的attrs
		old = oldAttrs[key]
		if(old !== cur) {
			old这个属性与cur这个属性不一致了,则更新/添加
				elm.setAttribute(key,cur)
		}		
	}
	//删除节点中不存在的属性
	for(key in oldAttrs){
		if(!(key in attrs)){
			//旧属性中存在,而新属性中不存在
			elm.removeAttribute(key)
		}
	}
}

然后,当oldCh 和 ch(newCh)都存在时,比较子节点chidren,此过程就是diff

diff算法

在patch过程中,当oldVnode和vnode均有children子节点时,这时候就会用到diff算法来进行比较。
以下图为例,
在这里插入图片描述

比较的方法流程:

  • 第一步 头头比较,如果相似,旧头心头均后移,真实dom不变,进入下一次循环,如不相似,进入第二步
  • 第二步 尾尾比较,如果相似,旧尾新尾均前移,真是dom不变,进入下一次循环,如不相似,进入第三步
  • 第三步 头尾比较, 如果相似,将旧头插入到旧尾最后的位置。旧头后移,新尾前移。如不相似,进入下一次循环
  • 第四步 尾头比较,如果相似,将旧尾插入到旧头前面的位置。旧尾前移,新头后移。如不相似进入第五步
  • 第五步 若节点有key且再旧子节点数组中找到sameVnode(tag和key都一致),则将其dom移动到当前真实dom序列的头部,新头指针后移; 否则,vnode对应的dom(vnode[newStartIdx],elm)插入到当前真实dom序列的头部,新头指针后移

以上这个循环结束的标志是

  • 新的节点数组(newCh)被遍历完了(newStartIdx > newEndIdx)。则把多余的旧dom都删掉
  • 旧的节点数组(oldCh)被遍历完了(oldStartIdx > oldEndIdx)。则把多余的新dom都添加

在这里插入图片描述

//diff算法源码
function updateChildren(parentElm, oldCh, newCh,insertedVnodeQueue){			
	let oldStartIdx = 0;
	let newStartIdx = 0;
	let oldEndIdx = oldCh.length -1;
	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(isUndef(oldStartVnode)){
			oldStartVnode = oldCh[++oldStartIdx] //这个节点被移走了
		}else if(isUndef(oldEndVnode)){
			oldEndVnode = oldCh[--oldEndIdx]
		} else if(sameVnode(oldStartVnode, newStartVnode)){ 
			//头头相似
			patchVnode(oldStartVnode,newStartVnode)
			oldStartVnode = oldCh[++oldStartIdx]
			newStartVnode = newCh[++newStartIdx]			
		} else if(sameVnode(oldEndVnode,newEndVnode)){
		  // 尾尾相似
			pathVnode(oldEndVnode,newEndVnode)
			oldEndVnode = oldCh[--oldEndIdx]
			newEndVnode = newCh[--newEndIdx]
		} else if(sameVnode(oldStartVnode,newEndVnode)){
		    //头尾相似  Vnode moved right
			patchVnode(oldStartVnode,newEndVnode)
			api.insertBefore(parentElm,oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)) //把oldStartVnode插入到oldEndVnode后面
			oldStartVnode = oldCh[++oldStartIdx]
			newEndVnode = newCh[--newEndIdx]			
		} else if(sameVnode(oldEndVnode, newStartVnode)){
			//尾头相等 Vnode moved left
			patchVnode(oldEndVnode,newStartVnode)
			api.inserBefore(parentElm,oldEndVnode.elm,api.nextSibling(oldStartVnode.elm)) // 把oldEndVnode插入到oldStartVnode的前面
		} else {
			// 根据旧子节点的key,生成map映射
			if(isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)				
			//在旧子节点数组中,找到和newStartVnode相似节点的下标
			idxInOld = oldKeyToIdx[newStartVnode.key]
			if(isUndef(idxInOld)){
				api.insertBefore(parentElm,createElm(newStartVnode),oldStartVnode.elm)
				newStartVnode = newCh[++newStartIdx]
			}else {
 			   elmToMove = oldCh[idxInOld]
			   patchVnode(elmToMove,newStartVnode)
			   oldCh[idxInOld] = undefined
			   api.insertBefore(parentElm,elmToMove.elm, OldStartVnode.elm)
			   newStartVnode = newCh[++newStartIdx]
			}
		}
	}


}

注: isUndef函数
都说添加了 :key可以优化v-for的性能,到底是怎么回事呢?
因为v-for大部分情况下生成的都是相同的tag标签,如果没有key标识,那么相当于每次头头比较都能成功。如果你往v-for绑定的数组头部push数据,那么整个dom将全部刷新一遍。
在这里插入图片描述
有了key, 其实就是多了一项匹配查找。

参考:Vue源码解析:虚拟dom比较原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值