前言
dom-diff算法是为了在更新dom的时候尽可能地减少dom操作,本文主要阐述vue2.x中的dom-diff算法,它的主要原理就是比对新老两个虚拟节点。其中虚拟节点的格式如下:
function vnode(tag,data,key,children,text) {
return {
tag,
data,
key,
children,
text
}
}
两个函数
在讲述dom-diff前先说明两个函数,分别是根据虚拟节点创建真实节点和更新节点属性。
- 根据虚拟节点创建真实节点
// 创建真实dom
export function createElm(vnode) {
let {tag,children,key,data,text} = vnode;
if(typeof tag == 'string'){ // 创建元素 放到vnode.el上
vnode.el = document.createElement(tag);
// 更新属性
updateProperties(vnode);
children.forEach(child => { // 遍历儿子 将子节点渲染后的结果添加到父节点中
vnode.el.appendChild(createElm(child));
});
}else { // 创建文件,放到vnode.el上
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
- 根据虚拟节点更新节点属性
/ 更新属性
function updateProperties(vnode,oldProps={}) {
let newProps = vnode.data || {};// 新属性
let el = vnode.el;
// 老的有 新的没有 需要删除
for (let key in oldProps) {
if (!newProps[key]) {
el.removeAttribute(key); // 移除真实dom属性
}
}
// 样式处理 老的 style={color:red} 新的 style={background:red}
let newStyle = newProps.style || {};
let oldStyle = oldProps.style || {};
// 老的样式中 新的没有 删除老的样式
for (let key in oldStyle) {
if (!newStyle[key]) {
el.style[key] = "";
}
}
// 新的有 直接用新的
for (let key in newProps) {
if (key == 'style') {
for(let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
}else if(key == 'class'){
el.className = newProps.class;
}else {
el.setAttribute(key,newProps[key])
}
}
}
新旧节点比对
在vue中新旧虚拟节点比对的函数是patch,其比对过程如下:
export function patch(oldVnode,vnode) {
// 默认初始化时 是直接用虚拟节点创建出真实节点 替换老节点
if (oldVnode.nodeType === 1) { // 真实节点
// 将虚拟节点转化成真实节点
let el = createElm(vnode); // 产生真实的dom
let parentElm = oldVnode.parentNode; // 获取老的app的父亲-》 body
parentElm.insertBefore(el,oldVnode.nextSibling); // 当前真实元素的后面 如果直接appendChild会导致插入在script脚本后面
parentElm.removeChild(oldVnode); // 删除老的节点
return el;
}else{
// 在更新时 进行新虚拟节点和老虚拟节点做对比 将不同的地方更新真实的DOM
// 1.比较两个元素的标签,标签不一样直接替换即可
if (oldVnode.tag !== vnode.tag) {
return oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el);
}
// 2.标签可能一样
// 都是文本标签 tag都是undefined
if (!oldVnode.tag) { // 文本比对
if(oldVnode.text !== vnode.text){
return oldVnode.el.textContent = vnode.text
}
}
// 3.标签一样 并且需要开始比对标签的属性和儿子
// 标签一样直接复用
let el = vnode.el = oldVnode.el
// 更新属性 用新的虚拟节点比对老的,更新节点
updateProperties(vnode,oldVnode.data);// 新老属性做对比
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 子元素比对 分为以下几种情况:
if (oldChildren.length>0 && newChildren.length>0) {
// 老的有儿子 新的也有儿子
updateChildren(oldChildren,newChildren,el)
} else if(oldChildren.length>0){
// 老的有儿子 新的没有儿子
el.innerHTML=""
} else if (newChildren.length>0) {
// 老的没儿子 新的有儿子
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child))
}
}
}
}
updateChildren函数
updateChildren函数是dom-diff算法的核心,具体实现逻辑如下:
function isSameVnode(oldVnode,newVnode) {
return (oldVnode.tag == newVnode.tag) && (oldVnode.key == newVnode.key)
}
// vue中的diff算法
function updateChildren(oldChildren,newChildren,parent) {
// DOM中常见的操作:把节点插入到当前儿子的头部、尾部、儿子倒序正序
// vue2中采用的是双指针
// 做一个循环 同时循环新老虚拟节点 哪个先节点 循环就停止
let oldStartIndex = 0;
let oldStartVnode = oldChildren[0];
let oldEndIndex = oldChildren.length-1;
let oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0;
let newStartVnode = newChildren[0];
let newEndIndex = newChildren.length-1;
let newEndVnode = newChildren[newEndIndex];
function makeIndexByKey(children) {
let map = {}
children.forEach((item,index)=>{
if (item.key) {
map[item.key] = index
}
})
return map;
}
let map = makeIndexByKey(oldChildren);
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (!oldStartVnode) { // 指针指向null 跳过这次处理
oldStartVnode = oldChildren[++oldStartIndex]
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
}else if(isSameVnode(oldStartVnode,newStartVnode)){ // 同意元素
patch(oldStartVnode,newStartVnode) // 更新属性 递归更新子节点
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
}else if (isSameVnode(oldEndVnode,newEndVnode)) {
patch(oldEndVnode,newEndVnode) // 更新属性 递归更新子节点
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
}else if (isSameVnode(oldStartVnode,newEndVnode)) { // 老的头和新的尾
patch(oldStartVnode,newEndVnode)
// 将当前元素插入到尾部的下一个元素
parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling)
oldStartVnode = oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex]
}else if (isSameVnode(oldEndVnode,newStartVnode)) { // 老的尾和新的头
patch(oldEndVnode,newStartVnode)
// 将当前元素插入到尾部的下一个元素
parent.insertBefore(oldEndVnode.el,oldStartVnode.el)
oldEndVnode = oldChildren[--oldEndIndex]
newStartVnode = newChildren[++newStartIndex]
}else {
// 儿子之间没有关系 暴力比对
let moveIndex = map[newStartVnode.key]; // 新虚拟节点开头的值
if (moveIndex == undefined) { // 不需要移动 说明没有复用的key
parent.insertBefore(createElm(newStartVnode),oldStartVnode.el)
} else {
let moveVnode = oldChildren[moveIndex]; // 对应老的虚拟节点
oldChildren[moveIndex] = null;
parent.insertBefore(moveVnode.el,oldStartVnode.el);
patch(moveVnode,newStartVnode); // 比较属性和儿子
}
newStartVnode = newChildren[++newStartIndex]
}
}
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
// parent.appendChild(createElm(newChildren[i]));
let ele = newChildren[newEndIndex+1] == null ? null : newChildren[newEndIndex+1].el
// 向后插入 ele = null
// 向前插入 ele 就是当前元素前面插入
parent.insertBefore(createElm(newChildren[i]),ele)
}
}
// 老节点未处理的都删除掉,如果是null,跳过即可
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
let child = oldChildren[i];
if (child !== null) {
parent.removeChild(child);
}
}
}
}
总结
vue中的DOM-DIFF算法主要分为两个部分:patch和updateChildren。两个过程的处理逻辑如下图所示。