Vue 虚拟dom & diff算法

diff算法

vdom因为是纯粹的JS对象,所以操作它会很高效,但是vdom的变更最终会转换成DOM操作,为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。

Vuediff算法仅在同级的vnode间做diff,递归地进行同级vnodediff,最终实现整个DOM树的更新。

oldStart+oldEndnewStart+newEnd这样2对指针,分别对应oldVdomnewVdom的起点和终点。起止点之前的节点是待处理的节点,Vue不断对vnode进行处理同时移动指针直到其中任意一对起点和终点相遇。处理过的节点Vue会在oldVdomnewVdom中同时将它标记为已处理。

Vue通过diff算法来比较新老vdom的差异,计算出需要操作dom的最少次数,实际中并以下措施来提升diff的性能。

  • 1、优先处理特殊场景:处理头尾同类型节点。
  • 2、原地复用:尽可能复用DOM,尽可能不发生DOM的移动。Vue在判断更新前后指针是否指向同一个节点,其实不要求它们真实引用同一个DOM节点,实际上它仅判断指向的是否是同类节点(比如2个不同的div,在DOM上它们是不一样的,但是它们属于同类节点),如果是同类节点,那么Vue会直接复用DOM,这样的好处是不需要移动DOM
整体视图
  • 处理头尾同类型节点,即oldStartnewStart指向同类节点的情况 / oldEndnewEnd指向同类节点的情况,直接移动指针。

  • 处理头尾、尾头同类型节点,即oldStartnewEnd,以及oldEndnewStart指向同类节点的情况,互换位置。

  • 处理新增的节点:在oldVdom中找不到节点,说明它是新增的,那么就创建一个新的节点,插入DOM树,插到什么位置?插到oldStart指向的节点前面,然后将newStart后移1位标记为已处理。oldStart不需要移动,因为oldVdom中没有这个节点。

  • 处理更新的节点:newStart来到的位置,在oldVdom中能找到它而且不在指针位置(查找oldVdomoldStartoldEnd区间内的节点),说明它的位置移动了,那么需要在DOM树中移动它,移到哪里?移到oldStart指向的节点前面,与此同时将节点标记为已处理(因为最后oldVdom中此时还是存在这个节点的,之后指针会游到该节点的位置,如果被标记过已经处理了,则是需要出现在新DOM中的节点,不要删除它,而是之前就以前移动过它了),跟前面几种情况有点不同,newVdom中该节点在指针下,可以移动newStart进行标记,而在oldVdom中该节点不在指针处,所以采用设置为undefined的方式来标记

  • 处理删除的节点:如果newStart跨过了newEnd,它们相遇啦!而这个时候,oldStartoldEnd还没有相遇,说明这2个指针之间的节点是此次更新中被删掉的节点。而如果有之前被标记过的节点,则不做处理,只删除此时没有标记过的节点。

在应用中也可能会遇到oldVdom的起止点相遇了,但是newVdom的起止点没有相遇的情况,这个时候需要对newVdom中的未处理节点进行处理,这类节点属于更新中被加入的节点,需要将他们插入到DOM树中。

第一部分是一个循环,循环内部是一个分支逻辑,每次循环只会进入其中的一个分支,每次循环会处理一个节点,处理之后将节点标记为已处理(oldVdomnewVdom都要进行标记,如果节点只出现在其中某一个vdom中,则另一个vdom中不需要进行标记),标记的方法有2种,当节点正好在vdom的指针处,移动指针将它排除到未处理列表之外即可,否则就要采用其他方法,Vue的做法是将节点设置为undefined

循环结束之后,可能newVdom或者oldVdom中还有未处理的节点,如果是newVdom中有未处理节点,则这些节点是新增节点,做新增处理。如果是oldVdom中有这类节点,则这些是需要删除的节点,相应在DOM树中删除之

整个过程是逐步找到更新前后vdom的差异,然后将差异反应到DOM树上(也就是patch),特别要提一下Vuepatch是即时的,并不是打包所有修改最后一起操作DOMReact则是将更新放入队列后集中处理)

虚拟dom

1、dom很慢

当创建一个元素比如div,有以下几项内容需要实现: HTML elementElementGlobalEventHandler。简单的说,就是插入一个dom元素的时候,这个元素上本身或者继承很多属性如 widthheightoffsetHeightstyletitle,另外还需要注册这个元素的诸多方法,比如onfucosonclick等等。 这还只是一个元素,如果元素比较多的时候,还涉及到嵌套,那么元素的属性和方法等等就会很多,效率很低。

尤其是在js操作DOM的过程中,不仅有dom本身的繁重,js的操作也需要浪费时间,我们认为jsDOM之间有一座桥,如果你频繁的在桥两边走动,显然效率是很低的。

虚拟dom就是解决这个问题的! 虽然它解决不了dom自身的繁重(虚拟dom只实现了真实dom的重要的属性和事件,但是最终渲染在页面的真实dom依然是繁重的),但是虚拟dom可以对js操作dom这一部分内容进行优化。

2、设计

隔离dom并不仅仅是因为dom慢,而也是为了把界面和业务完全隔离,操作数据的只关心数据,操作界面的只关心界面。

即我提供一个Component,然后你只管给我数据,界面的事情完全不用你操心,我保证会把界面变成你想要的样子。所以说着力点就在于View层。只要你给的数据是[1, 2, 3],我保证显示的是[1, 2, 3]。没有什么删除一个Element,添加一个Element这样的事情。NO。你要我显示什么就给我一个完整的列表。

3、实现

首先,vdom并没有完全实现dom,即vdom和真正地dom是不一样的,vdom最主要的还是保留了Element之间的层次关系和一些基本属性。因为真实dom实在是太复杂,一个空的Element都复杂得能让你崩溃,并且几乎所有内容我根本不关心好吗。所以vdom里每一个Element实际上只有几个属性,即最重要的,最为有用的,并且没有那么多乱七八糟的引用,比如一些注册的属性和函数啊,这些都是默认的,创建vdom进行diff的过程中大家都一致,是不需要进行比对的。所以哪怕是直接把vdom删了,根据新传进来的数据重新创建一个新的vdom出来都非常非常非常快。。

所以,引入了vdom之后,你给我一个数据,我根据这个数据生成一个全新的vdom,然后跟我上一次生成的vdom去 diff,得到一个Patch,然后把这个Patch打到浏览器的dom上去。完事。并且这里的patch显然不是完整的vdom,而是新的vdom和上一次的vdom经过diff后的差异化的部分

假设在任意时候有,vdom1 == dom1 (组织结构相同, 显然vdom和真实dom是不可能完全相等的)。当有新数据来的时候,我生成vdom2,然后去和vdom1diff,得到一个Patch(差异化的结果)。然后将这个Patch去应用到dom1上,得到dom2。如果一切正常,那么有vdom2 == dom2(同样是结构上的相等)。此时vdom2就会与下一次vdom3进行比较。

1.用JavaScript对象来表示DOM树的结构; 然后用这个树构建一个真正的DOM树,插入到文档中。
2.当状态变更的时候,重新构造一个新的对象树,然后用这个新的树和旧的树作对比,记录两个树的差异。
3.把2所记录的差异应用在步骤一所构建的真正的DOM树上,视图就更新了。

差异化实现

差异类型

1.替换原来的节点,如把上面的div换成了section。
2.移动、删除、新增子节点, 例如上面div的子节点,把p和ul顺序互换。
3.修改了节点的属性。
4.对于文本节点,文本内容可能会改变。

所以,我们可以定义下面的几种类型:

var REPLACE = 0;
var REORDER = 1;
var PROPS = 2;
var TEXT = 3;

对于节点替换,很简单,判断新旧节点的tagName是不是一样的,如果不一样的说明需要替换掉。 如div换成了section,就记录下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}]

除此之外,还新增了属性id为container,就记录下:

pathches[0] = [
    {
        type: REPLACE,
        node: newNode 
    }, 
    { 
        type: PROPS,
        props: {
            id: 'container'
        }
    }
]

如果是文本节点发生了变化,那么就记录下:

pathches[2] = [
    {
    type:  TEXT,
    content: 'virtual DOM2'
    }
]

列表对比算法

a b c d e f g h i => a b c h d f g i j

现在对节点进行了删除、插入、移动的操作。新增j节点,删除e节点,移动h节点,现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合),优化操作次数,我们能够获取到某个父节点的子节点的操作,就可以记录下来:

patches[0] = [{
  type: REORDER,
  moves: [{remove or insert}, {remove or insert}, ...]
}]

把差异引用到真正的DOM树上

因为步骤一所构建的 JavaScript 对象树(Vdom)和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行 DOM 操作。

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
    var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异

    var len = node.childNodes ? node.childNodes.length: 0
    for (var i = 0; i < len; i++) { // 深度遍历子节点
        var child = node.childNodes[i]
        walker.index++
        dfsWalk(child, walker, patches)
    }

    if (currentPatches) {
        applyPatches(node, currentPatches) // 对当前节点进行DOM操作
    }
}

function applyPatches (node, currentPatches) {
    currentPatches.forEach(function (currentPatch) {
        switch (currentPatch.type) {
            case REPLACE:
                node.parentNode.replaceChild(currentPatch.node.render(), node)
                break
            case REORDER:
                reorderChildren(node, currentPatch.moves)
                break
            case PROPS:
                setProps(node, currentPatch.props)
                break
            case TEXT:
                node.textContent = currentPatch.content
                break
            default:
                throw new Error('Unknown patch type ' + currentPatch.type)
        }
    })
}

参考1 参考2

你可能感兴趣的:(Vue 虚拟dom & diff算法)