Vue2.0 源码阅读:模板渲染

Vue 2.0 中模板渲染与 Vue 1.0 完全不同,1.0 中采用的 ,而 2.0 中借鉴 React 的 Virtual DOM。基于 Virtual DOM,2.0 还可以支持服务端渲染(SSR),也支持 JSX 语法(改良版的 render function)。

基础概念

在开始阅读源码之前,先了解一些必备的基础概念:AST 数据结构,VNode 数据结构, 的问题,render function。

AST 数据结构

的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。Vue 源码中借鉴 jQuery 作者 的 对模板进行解析,得到的就是 AST 代码。

我们看一下 Vue 2.0 源码中 的定义:

Vue2.0 源码阅读:模板渲染

我们看到 ASTNode 有三种形式:ASTElement,ASTText,ASTExpression。用属性 type 区分。

注意:为了避免文章过长,我在以上的代码中注释了 ASTElement 中的许多属性,点击上方 的链接可查看完整代码。

VNode 数据结构

VNode 是 VDOM 中的概念,是真实 DOM 元素的简化版,与真实 DOM 元素是一一对应的关系。

我们看一下 Vue 2.0 源码中 的定义:

Vue2.0 源码阅读:模板渲染

本文中我们关注代码中后面带注释的属性,后面的 render function 的生成跟这些属性相关。可在实际的 Vue 项目中加一个断点,查看实际的 VNode 中这些属性的值。

document. 的问题

我们为什么不直接使用原生 DOM 元素,而是使用真实 DOM 元素的简化版 VNode,最大的原因就是 document. 这个方法创建的真实 DOM 元素会带来性能上的损失。我们来看一个 document. 方法的例子:

Vue2.0 源码阅读:模板渲染

打开 console 运行一下上面的代码,你会发现打印出来的属性多达 228 个,而这些属性有 90% 多对我们来说都是无用的。VNode 就是简化版的真实 DOM 元素,只将我们需要的属性拿过来,并新增了一些在 diff 过程中需要使用的属性,例如 isStatic。

render function

render function 顾名思义就是渲染函数,这个函数是通过编译模板文件得到的,其运行结果是 VNode。render function 与 JSX 类似,Vue 2.0 中除了 Template 也支持 JSX 的写法。大家可以使用 方法编译下面这段模板:

Vue2.0 源码阅读:模板渲染

方法会返回一个对象,对象中有 render 和 staticRenderFns 两个值。看一下生成的 render function :

Vue2.0 源码阅读:模板渲染

除了 render function,还有一个 staticRenderFns 数组,这个数组中的函数与 VDOM 中的 diff 算法优化相关,我们会在编译阶段给后面不会发生变化的 VNode 节点打上 static 为 true 的标签,那些被标记为静态节点的 VNode 就会单独生成 staticRenderFns 函数:

Vue2.0 源码阅读:模板渲染

要看懂上面的 render function,只需要了解 _c,_m,_v,_s 这几个函数的定义,其中 _c 是 ,_m 是 renderStatic,_v 是 createTextVNode,_s 是 toString。除了这个 4 个函数,还有另外 10 个函数,我们可以在源码 中可以查看这些函数的定义。

模板渲染的过程

有了上面这些基本概念的认知,接下来通过源码了解模板渲染的过程。

生命周期

阅读源码之前,我们首先介绍一下相关源码的目录。

src

|— compile 模板编译的代码,1.0 和 2.0 版本在模板编译这一块改动非常大

|— core/instance 生命周期,初始化入口

|— core/vdom 虚拟DOM

|— entries 编译入口文件

本文中涉及到模板渲染的代码以上目录中都有分布,其中我们重点讲解的 compile 和 patch 分别在 src/compile 和 src/core/vdom 目录中。

核心函数介绍

在上一篇博客中我们简单讲了 Vue 的生命周期,在 _init 函数的最后一步就是 $mount 方法。这个方法就是模板渲染的入口。我们看一下下面这张图:

Vue2.0 源码阅读:模板渲染

上图中展示了模板渲染过程中涉及到的核心函数。我们可以通过 WebStrom 查看源码(按住 control 键单击方法名可以直接跳转,源码阅读神器),或者在浏览器中打断点一步一步查看代码运行的过程。

函数(src/entries/web-runtime-with-compiler.js),主要是获取 template,然后进入 compileToFunctions函数。

函数(src/platforms/web/compiler/index.js),主要将 template 编译成 render 函数。首先读缓存,没有缓存就调用 compile方法拿到 render function 的字符串形式,在通过 new Function 的方式生成 render function(基础概念中的 render function):

Vue2.0 源码阅读:模板渲染

函数(src/compiler/index.js)就是将 template 编译成 render 函数的字符串形式,后面一小节我们会详细讲到。

回到上面的 $mount方法,源码最后又调用了 函数(src/core/instance/lifecycle.js):

Vue2.0 源码阅读:模板渲染

在这个函数中出现了熟悉的 new Watcher,这一部分在上一篇博客中详细介绍过,主要是将模板与数据建立联系,所以说 Watcher 是模板渲染和数据之间的纽带。

至此,模板解析完成,拿到了 render function,也通过 Watcher 与将之数据联系在一起。

compile

上文中提到 函数(src/compiler/index.js)就是将 template 编译成 render function 的字符串形式。这一小节我们就详细讲解这个函数:

Vue2.0 源码阅读:模板渲染

这个函数主要有三个步骤组成:parse,optimize 和 generate,最终输出一个包含 ast,render 和 staticRenderFns 的对象。

函数(src/compiler/parser/index.js)采用了 jQuery 作者 的 ,将 template字符串解析成 AST。感兴趣的同学可以深入代码去了解原理。

函数(src/compiler/optimizer.js)主要功能就是标记静态节点,为后面 patch 过程中对比新旧 VNode 树形结构做优化。被标记为 static 的节点在后面的 diff 算法中会被直接忽略,不做详细的比较。

Vue2.0 源码阅读:模板渲染

函数(src/compiler/codegen/index.js)主要功能就是根据 AST 结构拼接生成 render function 的字符串。

Vue2.0 源码阅读:模板渲染

其中 函数(src/compiler/codegen/index.js)是会根据 AST 的属性调用不同的方法生成字符串返回。

Vue2.0 源码阅读:模板渲染

以上就是 compile 函数中三个核心步骤的介绍,compile 之后我们得到了 render function 的字符串形式,后面通过 new Function 得到真正的渲染函数。数据发现变化后,会执行 Watcher 中的 函数(src/core/instance/lifecycle.js),_update 函数会执行这个渲染函数,输出一个新的 VNode 树形结构的数据。然后在调用 __patch__ 函数,拿这个新的 VNode 与旧的 VNode 进行对比,只有发生了变化的节点才会被更新到真实 DOM 树上。

Vue2.0 源码阅读:模板渲染

patch

上一节我们提到了 __patch__ 函数最终会进入 。patch.js 就是新旧 VNode 对比的 diff 函数,diff 算法来源于 ,是 VDOM 思想的核心。对两个树结构进行完整的 diff 和 patch,复杂度增长为 O(n^3),而 snabbdom 的算法根据 DOM 操作跨层级增删节点较少的特点进行调整,将代码复杂度降到 O(n),算法比较如下图,它只会在同层级进行, 不会跨层级比较。

Vue2.0 源码阅读:模板渲染

函数(src/core/vdom/patch.js)的源码:

Vue2.0 源码阅读:模板渲染

函数(src/core/vdom/patch.js)与 函数(src/core/vdom/patch.js)形成的递归调用是 diff 算法的核心。

patchNode 核心代码:

Vue2.0 源码阅读:模板渲染

updateChildren 函数是 diff 算法高效的核心,代码较长且密集,但是算法简单。遍历两个节点数组,维护四个变量 oldStartIdx,oldEndIdx,newStartIdx,newEndIdx。算法步骤如下:

  1. 对比 oldStartVnode 和 newStartVnode,两者 elm 相对位置不变,若值得比较,则 patchVnode;

  2. 否则对比 oldEndVnode 和 newEndVnode,两者 elm 相对位置不变,若值得比较,则 patchVnode;

  3. 否则对比 oldStartVnode 和 newEndVnode,若值得比较说明 oldStartVnode.elm 向右移动了,那么 patchVnode,然后执行 api.insertBefore() 调整它的位置;

  4. 否则对比 oldEndVnode 和 newStartVnode,若值得比较说明 oldVnode.elm 向左移动了,那么 patchVnode,然后执行 api.insertBefore() 调整它的位置;

  5. 如果上面四种条件都不满足,则利用 vnode.key。先使用 createKeyToOldIdx 生成一个旧节点数组的索引表,如果新节点的 key 不存在这个表中说明是新节点,则添加;如果在则 patchVnode,然后在做一些调整免得影响后面的遍历;

  6. oldStartIdx > oldEndIdx 或者 newStartIdx > newOldStartIdx 的时候停止遍历;

  7. 如果 oldStartIdx > oldEndIdx 说明旧节点数组先遍历完,这时将剩余的新节点直接新建添加;

  8. 否则如果 newStartIdx > newEndIdx 说明新节点数组先遍历完,这时将剩余的旧节点直接删除。

核心代码:

Vue2.0 源码阅读:模板渲染

Vue2.0 源码阅读:模板渲染

总结

模板渲染是 Vue 2.0 与 1.0 最大的不同。本文梳理了模板渲染的过程,重点讲解了其中的 compile 和 patch 函数

compile 函数主要是将 template 转换为 AST,优化 AST,再将 AST 转换为 render function;

render function 与数据通过 Watcher 产生关联;

在数据发生变化时调用 patch 函数,执行此 render 函数,生成新 VNode,与旧 VNode 进行 diff,最终更新 DOM 树。

转载于:https://www.sohu.com/a/140752535_472869