Vue2.0 —— 关于虚拟节点和 Diff算法的浅析

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析

《工欲善其事,必先利其器》

既然点进来了,麻烦你看下去,希望你有不一样的收获。

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析_第1张图片

一、前言

大家好,我是 vk。今天我们需要来研究一下,diff算法虚拟节点。因为篇幅实在是太长了。而且,在实现 diff算法 的过程中,我也没有添加过多的代码注释,而是根据自己的思路一点点实现的。所以在阅读本文之前,我会先推荐你去读一遍 yk菌 的高赞文章。

同样,在文末,我也会把源码上传至码云,需要的小伙伴可以去下载查看。好了,废话不多说,我们马上开始!!

要了解 Vue 为什么会应用虚拟节点这门技术,以及,diff算法,就要先了解一些前置的储备知识。
本文我会大部分以图片的形式讲解,我们可以结合图解,更好的理解 Vue 的源码。另外,本文还会涉及一些抽象语法树的知识,大家如果还没看过的话可以去翻一下我以前的文章~

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析_第2张图片

二、虚拟节点的由来 —— 历程

在学习 Vue 源码的过程中,我的心中一直存在一个疑问:就是为什么不在 AST 抽象语法树做 diff 算法,而是要把它变成虚拟节点再进行比较呢?

首先来说,Vue 拥有三个最核心的部分:compilerreactivityruntime。它们分别代表着不同的任务模块:

  1. compiler 的目的是将 template 模板提取为一份,有规律的数据结构,即 AST 抽象语法树
  2. reactivity 的职责是实现数据的响应式,v3 使用 proxyv2 使用 Object.defineProperty
  3. runtime 表示运行时部分功能模块的实现,包含 render 成虚拟节点、diff算法 和操作真实节点、指令API等。

本质上,任务模块1(compiler)是为了适配 SFC 才会出现的。言外之意,我们完全可以抛开 compiler 的开发模式,只采用 runtime 运行时就可以进行开发的模式,主要利用 Vue 提供的render函数来实现,如下:

render(h) {
    let bool = false;
    return (
        <div>
            { bool ? <button>按钮1</button> : <button>按钮2</button>}
        </div>
    );
}

我相信,在你初学 Vue 的时候,肯定在某篇文章或者官网看见这段代码,你会想到什么?没错,这就是 JSX 的开发模式!

篇幅走到这里,我们了解了以下几点:
  1. Vue 的主要任务板块是什么;
  2. 以及,什么是 JSX 的开发模式。
但是,你有没有思考过几个问题?
  1. JSX 是否也会被转化为 Vnode
  2. 如果会,那为什么 SFCJSX 都会被转化为 VNode
  3. ASTVnode 的区别是什么?

我相信你已经注意到 render 函数的参数 h 了。实际上,不管是 Vue SFC 还是 JSX 开发模式所编写的代码,最后都会被 render 函数编译为 h 函数,从而转化为虚拟节点(VNode)。

我们也可以自己尝试写一下 h 函数:

render(h){
    return h('div',{ style : "color: red" }, "你好")
}

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析_第3张图片

乍一看好像还挺简单?可是如果有很多个结构层次需要书写的时候,render 往往不是最优的选择(这里不包括 JSX 开发模式,单纯指 renderh 函数)。相反,它会变得很繁琐。

因此,compiler 开发模式(SFC)和 runtime 开发模式(JSX)就诞生了。

现在我们知道了,尽管是 runtime 开发模式(JSX),它最终也是会被编译转化为 Vnode
但是,灵魂拷问来了:为什么都要被转化为 Vnode 呢?

这是因为,JavaScript 的引擎是单线程的,因为对于一个单页面前端框架来说(SPA),如果频繁的直接操控和渲染 DOM 时候,势必会带来很多不必要的性能消耗,而且用户体验也及其差劲。因此引发的思考就是:能否利用 JavaScript 执行速度快的优势,先通过这个手段把我们需要操作的过程和节点分析、提炼然后整合起来,最后再一次性的挂载到 DOM 上?

答案当然是可以的。在这之前,前端界就已经有过这种虚拟节点的库,叫做 Snabbdom。这个库可以说是虚拟节点技术的鼻祖,大家可以去了解一下。(documentFragment 也可以了解一下)

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析_第4张图片

但这还不算完。

为了追求更高的性能,如果每次挂载就把旧的 DOM 直接完全替换掉,那这种方法显然又会把一些不必要更新的节点也给重新渲染了一遍。这就相当于变相的浪费了性能,与一开始选择采用 VNode 的初衷不符。

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析_第5张图片

因此我们的祖师爷们又开始新一轮的头脑风暴,想在 VNode 节点挂载上树之前,让他跟旧节点做一次比较,把需要变化上树的节点更新了,其他不变的不用更新。但这种节点树之间的比较,需要一种非常精妙的算法,才可以优雅的实现,否则就是为 VNode 画蛇添足了。

最后,虚拟节点(VNode)就伴随着 diff算法 一同问世,这给当时的前端界带来了不小的轰动。这也被我们今天学习的 Vue 框架所应用且发扬光大。

篇幅走到这里,我相信应该会为你扫除了心中的一部分阴霾。现在我们剩下最后一个问题:
ASTVNode 到底有什么区别?为什么不在 AST 编译阶段就开启 diff算法

为此我画了一张图,可以清晰的看到,Vue SFC 的解析过程:

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析_第6张图片

其实这个 Vue SFC 确实是方便了绝大部分程序员,但是对于开发者来说,就要多一道手续,那就是通过把 template 先解析成 AST 抽象语法树,然后再通过 render 函数的编译,render 函数再提炼成 VNode

我们接着来看一张对比图,分别po出了,ASTh 函数转化完的结果:

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析_第7张图片

如上,以上两个流程图分别代表了,AST 抽象语法树 提取 template 的流程,以及,JSX 通过 render(即:h函数)转化成 VNode 的流程。其实我们可以看见,这两部分的代码功能似乎都差不多,都具备有把节点的文本和属性分开提炼的特点,只是前者被提取为 有规律性的数据结构,后者则是利用h函数转化为 VNode

所以文章到这里,上面的问题我们也解决了,那就是:
AST 抽象语法树和虚拟节点 VNode 的职责不同。前者注重模板编译,后者注重性能的提升。所以, diff算法 也就最适合在虚拟节点 VNode 阶段开启。

虚拟 DOM 非常有趣,他允许我们以函数的形式来表达程序视图,但现有的解决方式基本都过于臃肿、性能不佳、功能缺乏、API 偏向于 OOP 或者缺少一些我所需要的功能。但也有框架没有采用虚拟节点的技术,那就是 Svelte,大家有兴趣的也可以去了解一下。

三、前端框架视图更新的王牌 —— Diff 算法

回来说 diff算法,归根结底它是一种算法。只是它不同于普通的算法,这个算法总共运用了,递归、和4个指针好几种策略。

为了更好的理解这个算法,我画了一张图,从头到尾帮大家梳理了一下整个 patchVnode 的过程:

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析_第8张图片
(看不清楚图的小伙伴可以右键打开新标签页看哦~)

左边 h 函数我们上面讲过,将 AST 或者 JSX 转化成虚拟节点的函数;右边的 patchVnode 才是真正实现 diff算法 的地方。

然后那4个匹配策略其实也很简单,一句话概括:任何一种策略命中的都需要递归调用一下 patchVnode,用于比较新老节点是否有差异,然后将指针一个个的往下(新前老前策略)或者往上(新后老后策略)走。第3种策略如果命中,因为是新节点的后面命中,说明新节点永远在老节点的后面,因此不光要比较差异,还要将命中的老节点提升到节点树的顶部,策略4命中则反之。

最后还要整体循环遍历一下,看是否有需要上树或者删除的节点即可。

四、Vue 和 React 算法的差异 —— 不同点

  1. React

React 中假如 ChildComponent 里还有十层嵌套子元素,那么所有层次都会递归的重新 render(在不进行手动优化的情况下),这是性能上的灾难。(因此,React 创造了 Fiber,创造了异步渲染,其实本质上是弥补被自己搞砸了的性能)。

他们能用收集依赖的这套体系吗?不能,因为他们遵从 Immutable 的设计思想,永远不在原对象上修改属性,那么基于 Object.definePropertyProxy 的响应式依赖收集机制就无从下手了(你永远返回一个新的对象,我哪知道你修改了旧对象的哪部分?)

同时,由于没有响应式的收集依赖,React 只能递归的把所有子组件都重新 render 一遍(除了 memoshouldComponentUpdate 这些优化手段),然后再通过 diff算法 决定要更新哪部分的视图,这个递归的过程叫做 reconciler,听起来很酷,但是性能很灾难。

  1. Vue

Vue 其实每个组件都有自己的渲染 Watcher,它掌管了当前组件的视图更新,但是并不会掌管 ChildComponent 的更新。例如组件1种渲染了组件2,某个时刻组件1的某个片段需要更新,Vue 不会去深入到子组件的更新。

当然了,没有最好的框架,只有最好的程序员,如果这位看官你不赞同我的观点,那你就当看个笑话就好了~ 毕竟各花入各眼嘛~

五、总结

Vue2.0 —— 关于虚拟节点和 Diff算法的浅析_第9张图片
ok,经过上面这么大篇幅的介绍,我们总算是知道了:

  1. Vue 为什么使用虚拟节点以及为什么采用 AST 抽象语法树;
  2. Vue SFC 是如何编译的以及 JSX 是如何编译的,两者各有什么优点。
  3. diff算法 的匹配策略和虚拟节点是如何生成的;
  4. ReactVue 对于 diff算法 的性能差异。

其中,diff算法 的所有代码我托管在了码云上面,有需要的小伙伴可以下载来看看。—— 码云地址

感谢你的阅读,希望你的未来一片光明!

参考文献:

  1. Vue中的AST和VNode有什么区别? —— 知乎社区
  2. 为什么说 Vue 的响应式更新精确到组件级别?(原理深度解析) —— 掘金社区

你可能感兴趣的:(前端算法,vue,javascript,算法,vue.js,前端)