概念
diff算法的产生主要是直接操作dom很浪费性能,dom-diff可以在每次渲染的时候进行比对,比如有一个列表里面有三个元素,过一会又产生了一条新的数据,如果可以复用的元素就直接复用而不去操作相同的元素,diff算法的特点是平级比较
dom-diff操作的是虚拟节点,虚拟节点的属性很少(tag,data,children,key)
真实dom,每次操作真实dom就会重新创建一下非常多的属性,大大的消耗了性能
diff算法的实现
上面我们也了解了diff算法就是使用旧的虚拟节点和新的虚拟节点进行比较,那么我们看一下其中的比较规则吧。
那么我们现在使用vue源码中的三个方法生成两个虚拟节点。
//compileToFunction 解析html生成render函数
import {compileToFunction} from './compiler/index.js';
//patch 虚拟节点对比
//createElm 创建真是dom
import { patch,createElm } from './vdom/patch';
// 1.创建第一个虚拟节点
let vm1 = new Vue({data:{name:'meng'}});
let render1 = compileToFunction('<div>{{name}}</div>')
let oldVnode = render1.call(vm1)
// 2.创建第二个虚拟节点
let vm2 = new Vue({data:{name:'li'}});
let render2 = compileToFunction('<p>{{name}}</p>');
let newVnode = render2.call(vm2);
// 3.通过第一个虚拟节点做首次渲染
let el = createElm(oldVnode)
document.body.appendChild(el);
// 4.调用patch方法进行对比操作
patch(oldVnode,newVnode);
生成之后的虚拟节点,如下图
console.log(oldVnode,newVnode)
1.标签对比
如果标签不一样直接让新的替换老的
if(oldVnode.tag !== vnode.tag){
oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
}
2.标签内的文字对比
// 如果标签不存在那就是文本 如果文本不一致那就让新的替换老的内容 同时复用老的标签 只是内容替换 节省了创建标签的性能
if(!oldVnode.tag){
if(oldVnode.text !== vnode.text){
oldVnode.el.textContent = vnode.text;
}
}
3.对比属性
// 复用标签,并且更新属性
let el = vnode.el = oldVnode.el;
updateProperties(vnode,oldVnode.data);
function updateProperties(vnode,oldProps={}) {
let newProps = vnode.data || {};
let el = vnode.el;
// 比对样式
let newStyle = newProps.style || {};
let oldStyle = oldProps.style || {};
//如果新的属性不存在 直接将旧清空
for(let key in oldStyle){
if(!newStyle[key]){
el.style[key] = ''
}
}
// 删除多余属性
for(let key in oldProps){
if(!newProps[key]){
el.removeAttribute(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]);
}
}
}
4.子元素的对比
// 比较孩子节点
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比对儿子
if(oldChildren.length > 0 && newChildren.length > 0){
// 老的有儿子新的没有清空即可
}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));
}
}
新老都有需要比对儿子,这一步比较复杂一些 我们拎出来看一看
diff中做了几步优化
1 头和头对比
2 尾和尾对比
3 头移尾
4 尾移头
5 乱序对比
下面我们看看依次看一下diff中的几种对比孩子元素的优化策略
每次我们进行数据绑定的时候都会绑定一个key
function isSameVnode(oldVnode,newVnode){
// 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
1 头和头对比
指定双指针依次从头开始对比
代码实现
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];//新的指针截止元素
//当新的和老的虚拟节点指针没有重合之前
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 如果标签和key相同
if(isSameVnode(oldStartVnode,newStartVnode)){
//进行上面1,2,3,4对比
patch(oldStartVnode,newStartVnode);
//对比完成之后指针向后依次对比,对比之后的结果是直接将内容为`D`的替换为`E`
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
}
2.尾和尾对比
理解了上面的对比方式,这个对比方式只是指针对换了一下,当发现新的头和旧的头不一致时,diff算法有改变了一下对比策略,就是尾与尾对比,头对比是指针从开始指到结尾,尾对比是从结尾指到开始
代码实现,接着上面的while循环
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
else if(isSameVnode(oldEndVnode,newEndVnode)){
patch(oldEndVnode,newEndVnode); // 比较孩子
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
}
上面两种会出现的情况
头与头对比,尾与尾对比可能新的元素比旧的元素多,我们需要处理一下,那么可能问了,那老的元素比新的元素多不应该也处理一下么,这种情况我们再patch
方法中已经处理过了
头与头对比情况
尾与尾的对比情况
代码处理
当while循环完成之后
//如果跳出了while循环那就是老的开始指针和老的结束指针重合了,那么这种情况下新的开始指针和新的结束指针并没有重合 就会走到下面的逻辑里
if(newStartIndex <= newEndIndex){
//循环新的剩下的数据
for(let i = newStartIndex ; i<=newEndIndex ;i++){
创建新的元素 依次将剩下的插入到对应位置
let ele = newChildren[newEndIndex+1] == null? null:newChildren[newEndIndex+1].el;
parent.insertBefore(createElm(newChildren[i]),ele);
}
}
3.头移尾
如果oldVnode和vNode都不符合上面两种情况
那么diff又进行了头尾相比,如果新的尾和旧的头相同,那么旧的头进行位移。
代码实现
// 头移动到尾部
else if(isSameVnode(oldStartVnode,newEndVnode)){
patch(oldStartVnode,newEndVnode);
//将老的头插入到老的尾之后
parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex]
}
4.尾移头
如果oldVnode和vNode都不符合上面两种情况
那么diff又进行了头尾相比,如果新的头和旧的尾相同,那么将旧的尾进行位移。对比
调换位置之后
代码实现
//如果旧的尾和新的头相等
else if(isSameVnode(oldEndVnode,newStartVnode)){
patch(oldEndVnode,newStartVnode);
//将旧的尾插入到新的头前面
parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex]
}
5.乱序对比
如果新的和老的都不符合上述的条件,那么就属于乱序了,diff算法也进行了相对的操作。
先对老的虚拟dom创建一个映射表,如下图,创建出的映射表如下:
map = {C:0,A:1,B:2,D:3}
第一步:
老的指针位置 新的指针位置
C Z
map.Z不存在 插入到老的开始指针前面
老的指针还是C 新的指针向前走一步Y
第二步
老的指针位置 新的指针位置
C Y
map.Y不存在 插入到老的开始指针前面
新的指针向前走一步还是C 老的指针还是C
第三步
老的指针位置 新的指针位置
C C
map.C存在 C这个节点不需要动 老的开始指针向前移一步
新的指针向前走一步N 老的指针向前走一步A(头和头对比向前走一步)
第四步
老的指针位置 新的指针位置
A N
map.N不存在 插入到老的开始指针前面
新的指针向前走一步M 老的指针A
第四步
老的指针位置 新的指针位置
A M
map.M不存在 插入到老的开始指针前面
新的指针向前走一步H 老的指针A
第四步
老的指针位置 新的指针位置
A H
map.H不存在 插入到老的开始指针前面
老的指针A 新的已经在上一次重合
终止while循环
第五步 将老的开始指针 --- 结束指针的内容删除,最终形成
Z,Y,C,N,M,H
代码实现
对所有孩子进行编号
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index
});
return map;
}
let map = makeIndexByKey(oldChildren);
用新的元素去老的中进行查找,如果找到则移动,找不到则直接插入
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中没有将新元素插入
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的话做移动操作
let moveVnode = oldChildren[moveIndex];
oldChildren[moveIndex] = undefined;
parent.insertBefore(moveVnode.el, oldStartVnode.el);
patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]
如果有剩余则直接删除
if(oldStartIndex <= oldEndIndex){
for(let i = oldStartIndex; i<=oldEndIndex;i++){
let child = oldChildren[i];
if(child != undefined){
parent.removeChild(child.el)
}
}
}
更新操作
Vue.prototype._update = function (vnode) {
const vm = this;
const prevVnode = vm._vnode; // 保留上一次的vnode
vm._vnode = vnode;
//如果是第一次渲染
if(!prevVnode){
vm.$el = patch(vm.$el,vnode); // 需要用虚拟节点创建出真实节点 替换掉 真实的$el
// 我要通过虚拟节点 渲染出真实的dom
}else{
//如果上次是更新操作 需要进行diff对比
vm.$el = patch(prevVnode,vnode); // 更新时做diff操作
}
}
新的虚拟dom和旧的虚拟dom对比,操作的是真实dom,所以当老的虚拟dom不存在时我们将新的dom插进去 不会改变原来的虚拟dom 所以我们并没有在乱序对比中操作老的虚拟dom的指针
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。