虚拟DOM与diff算法

diff算法

新虚拟dom和老虚拟dom进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的dom上

在这里插入图片描述

虚拟DOM(虚拟DOM->真实DOM是通过模板编译)

真实DOM计算机处理起来比较复杂->转换
用js对象描述DOM的层次解构。DOM中的一切属性都在虚拟DOM中有对应的属性
为什么要有虚拟DOM,因为diff是发生在虚拟DOM上的

在这里插入图片描述

snabbdom

snabbdom是速度的意思,它是著名的虚拟DOM库,是diff算法的鼻祖,vue源码借鉴它

npm i snabbdom

搭建环境

在这里插入图片描述
创建www/index.html文件
在根目录下创建webpack.config.js文件

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        publicPath: 'dist/'
    },
    devServer: {
        port: 8080,
        contentBase: 'www'
    }
}

虚拟dom如何被渲染函数(h函数)产生?

h函数用来产生虚拟节点
在这里插入图片描述
在这里插入图片描述
创建src/index.js文件
// 创建patch函数,他是diff算法的核心

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

const patch = init([classModule, propsModule, styleModule, eventListenersModule])

const myVnode = h('a', { props: { href: 'https://www.baidu.com/' } }, '百度')

const container = document.getElementById('container')
patch(container, myVnode)

const vnode2 = h('a', {}, '百度')
patch(myVnode, vnode2)

在这里插入图片描述
vnode.js

// 将输入的参数转换成对象
export default function(sel,data,children,text,elm){
const key = data.key
	return {sel,data,children,text,elm,key}
}

h.js

import vnode from './vnode.js'
// 写一个低配版的h函数,这个函数必须接受3个参数,缺一不可
// 重载功能较弱,调用的时候必须是下面三种之一
// 1.h('div', {}, '文字')
// 2.h('div', {}, [])
// 3.h('div', {}, h())
export default function(sel,data,c){
  // 检查参数的个数
  if(arguments.length != 3) throw new Error('必须传入3个参数')
  if(typeof c == 'string' || typeof c == 'number' ){
		// 说明现在调用h函数是形态1
		return vnode(sel, data, undefined, c, undefined)
  }else if(Array.isArray(c)){
	// 说明现在调用h函数是形态2
	let children = [];
	// 遍历c, 收集children 
	for(let i = 0; i < c.length; i++){
		// 检查c[i]必须是对象
		if(!(typeof c == 'object' && c.hasOwnProperty('sel'))) throw new Error('传入的数组参数中有项不是h函数')
		children.push(c[i])
	}
	// 循环结束了,说明children收集完毕了,此时返回虚拟节点,他有children属性
	return vnode(sel, data, children, c, undefined)
  }else if(typeof c == 'object' && c.hasOwnProperty('sel')){
	// 说明现在调用h函数是形态3
	let children = [c]
	return vnode(sel, data, children, c, undefined)
  }else{
	throw new Error('传入的第三个参数类型不对')
  }
}

diff算法原理

虚拟DOM如何通过diff变为真正的DOM,是覆盖在diff算法里面的
在这里插入图片描述
在这里插入图片描述

patch.js

import vnode from './vnode.js'
import createElement from './createElement.js'
import patchVnod from './patchVnod.js'
export default function (oldVnode,newVnode){
	// 判定传入的第一个参数,是DOM节点还是虚拟节点?
	if(oldVnode.sel=='' || oldVonde.sel==undefined){
		// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
		oldVnode = vnode(oldVnode. tagName.toLowerCase(), {}, [], undefined, oldVnode)
	}
	// 判定oldVnode和newVnode是不是同一个节点
	if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel){
		console.log('是同一个节点')
		patchVnod(oldVnode,newVnode)
	}else{
		console.log('不是同一个节点,暴力插入新的,删除旧的')
		let newVnodeElm = createElement(newVnode)
		// 插入到老节点之前
		if(oldVnode.elm && newVnodeElm){
			oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
		}
		// 删除老节点
		oldVnode.elm.parentNode.removeChild(oldVnode.elm)
	}
}

createElement.js

// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入
export default function(vnode){
	console.log('目的是把虚拟节点', vnode, '真正变为DOM')
	// 创建一个DOM节点,这个节点现在还是孤儿节点
	let domNode = document.createElement(vnode.sel)
	// 有子节点还是有文本?
	if(vnode.text!=''&&(vnode.children==undefined||vnode.children.length==0)){
		// 它内部是文字
		domNode.innerText = vnode.text;
	}else if(Array.isArray(vnode.children) && vnode.children.length > 0){
		// 它内部是子节点,就要递归创建节点
		for(let i = 0;i<vnode.children.length;i++){
			// 得到当前这个children
			let ch = vnode.children[i]
			// 创建出它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点。
			let chDOM = createElement(ch);
			// 上树
			domNode.appendChild(chDOM)
		}
	}
	// 补充elm属性
	vnode.elm = domNode;
	// 返回elm, elm属性是一个纯DOM对象
	return vnode.elm;
}

在这里插入图片描述
patchVnod.js

import updateChildren from './updateChildren.js'
// 对比同一个虚拟节点
export default function(oldVnode,newVnode){
		// 判定新旧vnode是否是同一个对象
		if(oldVnode === newVnode) return;
		// 判断新vnode有没有text属性
		if(newVnode.text != undefined && (newVnode == undefined || newVnode.children.length == 0)){
			console.log('新vnode有text属性');
			if(newVnode.text != oldVnode.text){
			// 如果新虚拟节点中的text和老的虚拟节点text不同,那么直接让新的text写入老的elm中即可。如果老的elm中是children,那么也会立即消失
				oldVnode.elm.innerText = newVnode.text;
			}
		} else{
			// 新vnode没有text属性,有children 
			console.log('新vnode没有text属性')
			// 判断老的有没有children 
			if(oldVnode.children != undefined && oldVnode.children.length >0 ){
				// 老的children,此时是最复杂的情况。就是新老都有children。
			updateChildren(oldVnode.elm,oldVnode.children,newVnode.children)
			}else{
				// 老的没有children, 新的有chilfdren
				// 清空老的节点内容
				oldVnode.elm.innerHTML = ''
				// 遍历新的vnode的子节点,创建DOM,上树
				for(let i = 0 ;i <newVnode.children.length;i++){
					let dom = createElement(vnode.children);
					oldVnode.elm.appendChild(dom)
				}
			}
		}
}

四种命中查找(从上到下依次查找):

  1. 新前与旧前
  2. 新后与旧后
  3. 新后与旧前(此种发生了,涉及移动节点,那么新前指向的节点,移动的旧后之后)
  4. 新前与旧后(此种发生了,涉及移动节点,那么新前指向的节点,移动的旧前之前)

命中一种就不再进行命中判断了
如果都没有命中,就需要用循环来寻找了

在这里插入图片描述
新增的情况
在这里插入图片描述
删除的情况
在这里插入图片描述

多删除的情况(真实DOM会移动位置,虚拟DOM变为undefined)
在这里插入图片描述

复杂的情况
在这里插入图片描述

在这里插入图片描述
updateChildren.js

import patchVnode from './patchVnode.js'
// 判断是否是同一个虚拟节点
function checkSameVnode(a,b){
	return a.sel == b.sel && a.key == b.key
}
export default function updateChildren(parentElm, oldCh, newCh){
	// 旧前
	let oldStartIdx = 0;
	// 新前
	let newStartIdx = 0;
	// 旧后
	let oldEndIdx = oldCh.length - 1; 
	// 新后
	let newEndIdx = newCh.length - 1;
	// 旧前节点
	let oldStartVnode = oldCh[oldStartIdx];
	// 旧后节点
	let oldEndVnode = oldCh[oldEndIdx];
	// 新前节点
	let newStartVnode = newCh[newStartIdx];
	// 新后节点
	let newEndVnode = newCh[newEndIdx];
	let keyMap = null;
	// 开始大while
	while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
		if(checkSameVnode(oldStartVnode, newStartVnode)){
			console.log('1新前和旧前命中,对比递归')
			pathVnode(oldStartVnode, newStartVnode);
			oldStartVnode = oldCh[++oldStartIdx];
			newStartVnode = newCh[++newStartId];
		}else if(checkSameVnode(oldEndVnode, newEndVnode)) {
			console.log('2新后和旧后命中,对比递归')
			pathVnode(oldEndVnode, newEndVnode);
			oldEndVnode = oldCh[--oldEndIdx];
			newEndVnode = newCh[++newEndId];
		}else if(checkSameVnode(oldStartVnode, newEndVnode)){
			console.log('3新后和旧前命中,对比递归')
			pathVnode(oldStartVnode, newEndVnode);
			// 此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
			// 如何移动节点?插入到一个已经在DOM树上的节点,它就会被移动
			parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling);
			oldStartVnode = oldCh[++oldStartIdx];
			newEndVnode = newCh[--newEndId];
		}else if(checkSameVnode(oldEndVnode, newStartVnode)){
			console.log('4新前和旧后命中,对比递归')
			pathVnode(oldEndVnode, newStartVnode);
			// 移动新前指向这个节点到老节点旧前的前面
			parentElm.insertBefore(oldEndVnode.elm,oldStartnode.elm);
			oldEndVnode = oldCh[--oldStartIdx];
			newStartVnode = newCh[++newEndId];
		}else{
			// 都没有匹配到
			// 寻找key的map
			if(!keyMap){
				keyMap = {}
				for(let i = oldStartIdx;i<=oldEndIdx;i++){
					const key = oldCh[i].key;
					if(key != undefined){
						keyMap[key] = i
					}
				}
			}
			// 寻找当前这项在keyMap中映射的位置序号
			const idxInOld = keyMap[newStartVnode.key];
			if(idxInOld === undefined){
				// 判断,如果idxInOld是undefined表示它全新的项
				// 被加入的项(就是newStartVnode这项)现在不是真正的DOM节点
				parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)
			}else{
				// 如果不是undefined,不是全新的项,而是要移动
				const elmToMove = old[idxInOld];
				patchVnode(elmToMove. newStartVnode);
				// 把这项设置为undefined,表示处理完这项了
				oldCh[idxInOld] = undefined;
				// 移动,调用insertBefore实现移动
				parentElm.insertBefore(elmToMove.elm,oldStartVnode.elm);
			}
			// 指针下移,只移动新的头
			newStartVnode = newCh[++newStartIdx]
		}
	}
	// 继续看看有没有剩的。循环结束了start还是比old小
	if(newStartIdx <= newEndIdx){
		// 遍历新的newCh,添加到老的没有处理的之前
		for(let i = newStartIdx; i<= newEndIdx;i++){
			// insertBefore方法可以自动识别null,如果是null会自动拍到队尾去。和appendChild一致。
			// newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
			parentElm.insertBefore(createElement(newCh[i],oldCh[oldStartIdx].elm))
		}else if(oldStartIdx<=oldEndIdx){
			console.log('old还有剩余节点没有处理')
			// 批量删除oldStart和oldEnd指针之间的项
			for(let i = oldStartIdx; i<= oldEndIdx; i++){
				if(oldCh[i]){	
					parentElm.removeChild(oldCh[i].elm)
				}
			}
		}
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值