带你简单理解diff算法

关于virtual dom

咱们知道不论是vue仍是react当中,都是利用virtual dom(下面简称vd)来表示真实的dom,由于操做真实的dom的代价是昂贵的,即便是查找dom节点的操做都是昂贵的,因此在优化的方法当中,就有缓存dom的查找结果的一个优化,那么既然真实dom的操做是昂贵的,因此若是咱们在使用diff算法来比较两个dom之间的差别的时候,就要遍历全部的dom来进行对比,若是是按照真实的dom来进行diff算法的比较的话,那么就至关消耗性能了,所以vd应运而生。那么怎么将真实的dom和vd对应起来呢?咱们知道,dom不外乎三个特性:
一、标签名
二、各类属性
三、孩子节点
所以,若是要用vd来表示dom的话咱们就能够这样定义。vue

class VNode {
    constructor(tagName, attributes, children) {
        this.tagName = tagName
        this.attributes = attributes
        this.children = children
    }
}
复制代码

好比有这样的domnode

<div id="div" class="classVal">
    <span>child</span>
</div>
复制代码

那么vd就是这样的react

{
    tagName: 'div',
    attributes: {
        'id': 'div',
        'class': 'classVal'
    },
    children: [{
        tagName: 'span',
        attributes: null,
        children: ['child']
    }]
}
复制代码

固然,这里vd的定义少了TEXT节点,因此咱们加上TEXT节点,TEXT节点直接返回里面的innerText/textContent,就像上面的children: ['child'],咱们定义一个叫h的函数,用来建立vd,包括TEXT节点,它接受四个参数,分别以下:git

tagName: 标签名  
text: 若是是TEXT节点,那么就是TEXT的内容,即innerText/textContent
attributes: dom属性的价值对  
children: dom的孩子vd
复制代码
function h(tagName, text, attributes, children) {
    // 判断到是TEXT节点,直接返回TEXT里面的内容
    if(text) {
        return text
    }
    return new VNode(tagName, attributes, children)
}
复制代码

好了,VD大概就是这样子表示,那么咱们若是根据vd还原成真实的dom呢,其实很简单,就是根据一一对应关系还原呗:github

function createElement(vnode) {
    var el = null;
    // 文本元素
    if(typeof vnode === "string") {
        el = document.createTextNode(vnode);
        return el;
    }
    // 还原dom
    el = document.createElement(vnode.tagName);
    // 还原attribute
    for(var key in attributes) {
        el.setAttribute(key, attributes[key]);
    }
    // 还原孩子节点
    var children = vnode.children.map(createElement);
    children.forEach(function(child) {
        el.appendChild(child);
    });
    return el;
}
复制代码

关于vd的理解差很少就这样,若是有须要补充的或者指正的,望不吝赐教。算法

diff算法

有了vd后,咱们要怎么比较两个dom树之间的不一样呢,固然不能无脑的使用innerHTML对整块树更新(backbone就是这样),而是针对更改的地方进行更新或者替换,那么咱们就须要依赖diff来找出两棵树之间的不一样。
传统的diff算法,是须要跨级对比两个树之间的不一样,时间复杂度为O(n^3),这样的对比是没法接受的,因此react提出了一个简单粗暴的diff算法,只对比同级元素,这样算法复杂度就变成了O(n)了,虽然不能作到最优的更新,可是时间复杂度大大减小,是一种平衡的算法,下面会提到。缓存

image

那么怎么理解它是只对比同级和具体它是怎么对比的呢?
基于diff算法的同级对比,咱们先讲下对比的过程当中,它主要分为四种类型的对比,分别为:
一、新建create: 新的vd中有这个节点,旧的没有
二、删除remove: 新的vd中没有这个节点,旧的有
三、替换replace: 新的vd的tagName和旧的tagName不一样
四、更新update: 除了上面三点外的不一样,具体是比较attributes先,而后再比较children
写成代码就是这样:bash

diff(newVnode, oldVNode) {
    
    if(!newVNode) {
        // 新节点中没有,说明是删除旧节点的
        return {
            type: 'remove'
        }
    } else if(!oldVNode) {
        // 新节点中有旧节点没有的,说明是删除
        return {
            type: 'create',
            newVNode
        }
    } else if(isDiff(newVNode, oldVNode)) {
        // 只要对比出两个节点的tagName不一样,说明是替换
        return {
            type: 'replace',
            newVNode
        }
    } else {
        // 其余状况是更新节点,要对比两个节点的attributes和孩子节点
        return {
            type: 'update',
            attributes: diffAttributes(newVNode, oldVNode),
            children: diffChildren(newVNode, oldVNode)
        }
    }
}

// 对比孩子节点,其实就是遍历全部的孩子节点,而后调用diff对比
function diffChildren(newVnode, oldVNode) {
    var patches = []
    // 这里要获取两个节点中的最大孩子数,而后再进行对比 
    var len = Math.max(newVnode.children.length, oldVNode.children.length);
    for(let i = 0; i <len; i++) {
        patches[i] = diff(newVnode.children[i], oldVnode.children[i])
    }
    return patches
}

// 对比attribute,只有两种状况,要不就是值改变/新建,要不就是删除值,对比dom只有setAttribute和removeAttribute就知道了
function diffAttributes(newVnode, oldVNode) {
    var patches = []
    // 获取新旧节点的全部attributes
    var attrs = Object.assign({}, oldVNode.attributes, newVNode.attributes)
    for(let key in attrs) {
        let value = attrs[key]
        // 只要新节点的属性值和久节点的属性值不一样,就判断为新建,不论是更新和真正的新建都是调用setAttribute来更新
        if(oldVNode.attributes[key] !== value) {
            patches.push({
                type: 'create',
                key,
                value: newVnode.attributes[key]
            })
        } else if(!newVNode.attributes[key]) {
            patches.push({
                key,
                type: 'remove'
            })
        }
    }
    return patches
}

// 判断两个节点是否不一样
function isDiff(newVNode, oldVNode) {
    // 正常状况下,只对比tagName,可是text节点对比没有tagName,因此要考虑text节点
    return (typeof newVNode === 'string' && newVNode !== oldVNode) 
    || (typeof oldVNode === 'string' && newVNode !== oldVNode) 
    || newVNode.tagName !== oldVNode.tagName
}
复制代码

结合代码,你们对比下面的图,图里面remove没有列出来,remove和create差很少缘由,相信你们知道什么状况下是remove。 mvc

image
上图,按传统的方法只是在span和p直接插入了一个div,可是diff算法不是这么来更新的,它只对比同一级别的,即会以为旧节点的p和新节点的div才是同一级,他们的tagName不一样,因此定义为replace,接着把旧节点的div当作和新节点的p是同一级,依旧是replace,最后旧节点没有div,因此create了。能够看到,其实这个更新代价仍是比较大的,可是比对的过程却简单和快速,所以是一种相对平衡的算法。

完整的代码你们能够看下个人git地址:github.com/VikiLee/XLM…app

总结

咱们更新dom的时候,尽可能不要整棵树进行更新,须要作到细颗粒的更新,要作到细颗粒地更新就必须知道两棵树直接的不一样,因此须要使用diff算法来进行对比,可是传统的diff算法虽然能作到细颗粒准确地更新,可是它须要花销大量的时间来进行比对,因此有来react的改版的diff算法,只比较同一级的元素,这样能够作到快速的比对,为O(n),即便这样,在对比两棵树的时候,咱们仍是须要遍历全部的节点,咱们知道dom的操做是昂贵的,即便是查找,也是昂贵的一个过程,特别是在节点不少的donm树下,因此虚拟dom应运而生,虚拟dom避开了直接操做dom的缺点,而是直接对比内存中vd,使得对比速度进一步获得质地提高。

相关文章
相关标签/搜索