Vue.js 核心系列 - 编译

从模板到真实 DOM 渲染的过程,中间有一个环节是把模板编译成 render 函数,这个过程我们把它称作编译。

Vue.js 提供了 2 个版本,一个是 Runtime + Compiler 的,一个是 Runtime only 的,前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpack 的 vue-loader 事先把模板编译成 render 函数。

对编译过程的了解会让我们对 Vue 的指令、内置组件等有更好的理解。不过由于编译的过程是一个相对复杂的过程,我们只要求理解整体的流程、输入和输出即可,对于细节我们不必抠太细。

核心编译流程

我们将整个 Vue 的编译流程简化为如下三个核心步骤,分别是:

parse -> optimize -> codeGen

  • Parse 的目标是把 template 模板字符串转换成 AST 树(它是一种用 JavaScript 对象的形式来描述整个模板)。整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

    parse 流程(来自:Vue.js 技术揭秘)

  • optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变,这对运行时模板的更新起到极大的优化作用。

  • codeGen 中 genElement 针对不同情况,如静态节点 genStatic,生成 _m(xxxx) 的字符串

    • 通过 createFunction 核心调用 new Function() 传入字符串生成 render 函数,生成 render 函数所要的字符串使用了 with 语句,目的是在不造成性能损失的情況下,减少变量的长度

    • With 语句在变量寻址的过程中会根据作用域链逐层查找,这样使得性能变差,所以最好在调用语句中只应该包含这个指定对象的属性,如:

      1
      2
      3
      4
      5
      6
      var obj = { id: 1, name: ‘a’ };
      with(obj) {
      console.log(id) // 1
      console.log(name) // a
      console.log(qq) // 这时候会逐层查找,非常耗性能,如果没有找到则抛出错误
      }
  • Render 函数调用的的最终结果是创建 vNode 包含各种类型 vNode,最后通过 patch 调用原生方法创建真实的 DOM

扩展

  • 插槽(solt),有两种形式,普通插槽和作用域插槽,发生在 codegen 阶段,

    • 它们有一个很大的差别是数据作用域

    • 普通插槽是在父组件编译和渲染阶段生成 vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes。

    • 而对于作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnode 的 data 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例

参考

Vue.js 核心揭秘 - 编译

如果觉得有用,您的支持将鼓励我继续创作!