通过几个问题深入分析Vue中的diff原理

遇到的问题

在使用Vue渲染“可删减”的列表时,错误的使用index作为key,导致列表视图出现错乱。

点击查看问题

  • 复现步骤:右侧有两行,在第一行的Input里输入1,在第二行Input里输入2,然后点第一行的“ד删除第一行
  • 期待结果:删除第一行后,应该变成“请输入 dog 的个数:2”
  • 实际结果:删除第一行后,变成了“请输入 dog 的个数:1”

为什么cat变成了dog,但是<input />里的1没有变成2呢?

这个问题一下子很难解释,下面我们通过几个小问题,一步一步来分析。

如果我们使用正确的值做为key,那么这个问题其实根本就没有意义。但是,如果我们参透了其中的出错原因,这将给我们带来极大的提升。

为什么会触发组件update

查看使用index作为key例子

  • 测试1:打开浏览器控制台,然后删除第一行,查看日志,思考为什么。
  • 测试2:先重置页面,然后删除最后一行,查看日志,思考为什么。

测试1的结果

你会发现,删除第一行后,watchupdated钩子都执行了,这个结果其实给了我们第一个提示:

删除第一行这句话本质上其实是:删除vue实例数据中list的第一项,并不是删除dom的第一个节点!

对于在dom中的这三个节点,其实是做了如下的变化:

VDOM的diff算法

之所以会这种方式进行dom更新,这决定于vdom的diff算法,我们通过阅读vue源码中src/core/vdom/patch.js这个文件来一探究竟。具体的来说,就是其中的updateChildren方法:

不要被这么多变量吓到,其实主要是三组变量,每组四个:

  • 第一组是四个指针,分别指向oldCh和newCh的头和尾
  • 第二组是四个vnode,分别是四个指针所指的节点
  • 第三组是四个辅助变量(413行),用来移动vnode

在我们的例子里,大概是这样:

继续往下看:

又是一坨代码,但也不要被吓到,你会发现if、else if里的逻辑都是差不多的,仔细读两遍,你就会发现其实大概就是:

  1. 空节点跳过处理

  2. 指针1对应的vnode跟指针3对应的node比,如果是same的就patchVnode进行更新,如果不是same的就往下走

  3. 指针2对应的vnode跟指针4对应的node比,如果是same的就patchVnode进行更新,如果不是same的就往下走

  4. 指针1对应的vnode跟指针4对应的node比,如果是same的就patchVnode进行更新,如果不是same的就往下走

  5. 指针2对应的vnode跟指针3对应的node比,如果是same的就patchVnode进行更新,如果不是same的就往下走

  6. 最终,如果到了这一步还不是same的,那就用key最终确认一次

    1. 先构造一个key to index的map
    2. 判断当前old vnode的key是否在map里
    3. 如果不在,就直接createElm
    4. 如果在,并且是same的,那就patchVnode,然后更新节点内容、顺序
    5. 如果在,但是不是same的,那就视作新element,执行createElm
  7. 当while循环退出时,如果指针1和指针2还没重合,那就代表此时指针1和指针2区域内的vnode是待删除的,所以直接removeVnodes。而如果是指针3和指针4还没重合,那就代表指针3和指针4之间的vnode是待添加的,所以直接addVnodes。至此整个过程结束。

怎么用上面的过程解释“测试1”的结果

再看一下这个图:

按照上面的diff算法,我们会先判断cat和dog是否是same的,其中sameNode方法如下:

也就是说,只要key相同,并且tag、isComment、isDef(data)、sameInputType都是true,那么diff算法就认为是同一个vnode,在这里旧的cat节点和新的dog节点,它们的key都是0,显然符合这个条件。

所以,代码会进入到这里:

此时会使用patchVnode方法来"patch"旧的这个cat节点,怎么patch呢?

简单地说,就是使用新的props,让这个cat节点进行re-render,re-render的过程中必然也做一些诸如:触发watch,调用updated声明周期钩子之类的事情。 记住这句话,以后会用到!

接下来,dog变成pig,也是同样的道理。

最后,左边oldCh的pig节点哪去了呢?

其实到了这里,while循环就已经退出了,看上一小节的第7步,此时pig节点会直接被remove掉。

关于patchVnode的细节在这里没有写,需要自己去看,关键的地方是在src/core/vdom/patch.js的545,552,572行

需要强调一点

可能有人会疑惑,即使我不知道diff算法的细节,在我们删除第一行时,也就是删掉list的第一项时,会触发视图更新,视图更新了,那cat节点肯定就会变成dog,这应该是理所当然的啊。

这里需要强调的是,使用diff算法时,"合适"的原有的节点是会被复用的!cat之所以变成dog,不是因为新建了一个dog节点,而是cat节点被复用,然后使用新的props,通过re-render实现了视图的更新!

测试2为什么不触发log的打印

到这里,我们就已经解释了:为什么“测试1”会触发watchupdated的打印了。

那么为什么测试2不会触发上述打印呢?其实原因很简单,因为patchVnode提前return了,没有触发re-render:

回到最开始的问题

如下图,到这里我们应该已经理解为什么删除第一行后,cat会变成dog。但是,为什么<input />里的1没有变成2呢?

一个简单的解释

我们之前说过:patchVnode的结果,其实就是使用新的props,让这个cat节点进行re-render。

这里是re-render,它的执行不是unmount一个节点,然后再mount一个新的节点,而是直接使用新的props来receive(更新)一个节点,节点的instance并没有重置,所以re-render的过程中,data压根就没变。

receive这个词出自:React实现原理

一些练手的问题 [可选]

使用空、常量1、index、unique的稳定值、random的随机值来作为key,依次预测视图如何表现、控制台如何打印:

练手问题

这样就结束了吗?

有一个更深层次的问题:这是一个feature还是一个bug?

我又用React写了一个同样的例子:点击查看React版本的问题

你会发现,不管是React还是Vue都会存在这个问题,这肯定不是一个bug,那么这两个框架为什么要这么设计呢?

如果感兴趣,请关注下一篇文章:《思考如何自己写一个React框架》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值