八股文不用背-vue的diff算法

272 阅读5分钟

diff算法

背景

  1. vue将虚拟树(后面用vDom来描述,是一个树)渲染调用了创建元素的方法(creatElement),然后渲染出元素
  2. vDom中的节点是虚拟节点(后面用Vnode形容),在vue源码中使用h函数来生成vNode,接受三个参数
    1. 渲染什么标签,比如div、span,使用一个sel(selector)指代
    2. 一个对象,代表该标签的属性,其中还有个属性key,比如我们v-for渲染列表时必须要写的key
    3. 有孩子就传数组children,如果没有孩子,传string或者number代表该节点渲染的文本内容
  3. vNode在创建真实元素时,会将该真实元素记录在该VNode,这样,我需要修改该元素的text时,能直接通过vNode.ele来直接修改。
  4. 当我们修改了data中的值,然后产生新的vDom时,本来我们可以直接渲染新的vDom,但这样有很多没必要的更新,比如旧vDom有100个vNode,我们只修改了其中某一个vNode,然后产生了新vDom,如果我们直接渲染vDom的话,真实的渲染树中99个节点都被重新渲染了,浪费了很多很多资源。

patch

那么很自然,我们就需要比较旧vDom和新vDom(即树的比较-递归算法),找出需要更新的vNode,递归过程如下:

  1. 无旧vNode,有新vNode,很明显是新建,通过新vNode创建真实元素,然后将该真实元素插入旧父vNode.ele..children上(因为是递归遍历,且是从父往子遍历,所以能拿到对应节点的父节点),然后直接将新vNode插入到旧父vNode上。
  2. 有旧vNode,无新vNode,很明显删除,将旧父vNode.ele.children中的对应节点删除,然后删除该旧vNode
  3. 有旧vNode,有新vNode,得分情况
    1. 新旧vNode的sel和key不一样,说明是整个vNode替换,将旧父vNode.ele.children中对应节点换成通过新vNode创建出来的真实节点,然后将新vNode替换掉旧vNode
    2. 新旧vNode的sel和key一样,还得分情况
      1. 旧vNode无children(说明是文本节点),新vNode无children(同左),那么就看两者的text是否相同咯,相同就什么都不干,不同就将旧vNode.ele.text换成新的

      2. 旧vNode有children,新vNode无children,直接将旧vNode.ele.text赋值成新的,然后将旧vNode的children置空

      3. 旧vNode有children,新vNode有children,来咯来咯,重点来咯,一般来说就vNode的children中和新vNode的children中应该是有大部分重复的,他们可能只是变了个位置,比如ul渲染了1,2,3,4,5,然后更新成2,3,6,1,4,5,旧的12345都是没必要重新创建的,直接利用旧vNode即可,但是相对位置需要改变,还可能涉及插入,删除等。

diff

旧vNode.children中的vNode都是已经有真实的元素的了,即vNode.ele,而新vNode.children中的vNode是没有创建真实元素的,我们想干的是尽可能用旧vNode.ele,而不是通过新vNode来创建新的真实元素,这样就能节省很多资源。 如果对本身有哪些操作不理解的,大概率是为了实现上面这个目的才干的操作。

对于上面的例子,其实我们是可以通过O(n^2)(甚至是O(n))的复杂度来实现一个新的children。
即通过新数组[2,3,6,1,4,5],从旧数组[1,2,3,4,5]中找对应的item,然后组成一个新的数组[2,3,6,1,4,5],其中的12345来自旧数组,但是需要一个额外的空间。

旧vNode.children里的vNode的顺序是[2,3,6,1,4,5],这对了,但是children的vNode.ele真实元素的顺序依旧是[1,2,3,4,5],我们要怎么处理呢?方式和上面一样,但是还是需要一个额外空间。

而vue的diff算法是在旧vNode和新vNode上动手的,即不需要额外的空间,而且时间复杂度是O(n),来我们走一遍:

下面的几张图片来自这篇文章,想要手写vDom和diff算法的很推荐看这篇文章。

首先我们有旧前、旧后、新前、新后这四个索引,初始值分别是0、旧children.length-1、0、新children.length-1。如图:

image.png

旧前<=旧后&&新前<=新后 的前提下不断进行如下判断

  1. 如果:旧前 = 新前,那么旧前所指不需要动,旧前后移,新前后移


    a0ira-0mf1y.gif
    \


  2. 否则如果:旧后 = 新后,说明旧后所指不需要动,旧后前移,新后前移


    aodg1-5er0x.gif
    \


  3. 否则如果:旧前 = 新后,说明要将旧前插入到旧后之后,(旧后vNode.ele.insertAfter(旧前vNode.ele),旧后vNode.insertAfter(旧前vNode),旧前所指设置为undefined),旧前后移,新后前移。


    ay3tm-bknwp.gif
    \


  4. 否则如果:旧后 = 新前,说明要将旧后插入到旧前之前,(旧前vNode.ele.insertBefore(旧后vNode.ele),旧前vNode.insertBefore(旧后vNode),旧后所指设置为undefined),旧后前移,新前后移




afv8g-6x25m.gif


  1. 否则:在旧前和旧后之间找到新前的节点,然后将该节点插入到旧前之前,然后设置该节点原来的位置为undefined,新前后移;如果没能在旧前和旧后之间找到新的节点,说明是新增,在旧前插入新增的节点


    a1kue-69qpu.gif

在跳出上面的循环之后,将旧前和旧后之间的节点全删除,然后旧vDom就是新的vDom,选择性更新dom树就完成了。

看到这里的看官,麻烦点个赞赞吧