Vue 是如何进行模板编译的?(一)

前言

如果以简单使用者的角度来学习 vue,当然了解其相关的概念及使用方式足够,但如果是深度用户的话,或多或少会踩到不少框架及底层设计方式带来的不少的坑。比如,我们理所当然的认为,更新了状态,视图层会自动相应的更新,但现实是假如状态中使用到了数组,则我们经常会遇到更新了数组的值后,视图并没有按预期去自动更新,而需要我们手动去使用 this.$set 来强制更新视图,这是因为 vue 设计时使用的监听状态变化方式使用了 Object.defineProperty(obj, prop, descriptor),这种方式监听数组会有问题,针对数组状态,vue 进行了特别的处理,改写了数组对象的函数对可能改变数组的方法进行监听处理,但当前处理方式会出现监听遗漏的情况如 splice 等函数;废话不多说,下面一步步来了解vue是如何进行模板编译。

通用的 Vue 文件格式

<template>
<!-- 模版部分-->
</template>

<script>
// 生命周期函数
</script>

<style lang="scss" scoped rel="stylesheet/scss">
<!-- css 样式或者 scss / less 等样式预处理语言-->
</style>

编译时主要是对 template 里面的内容进行编译。script 可以直接使用,style 也是可以直接抽离出来使用或者进行样式预处理。

第一步是将 模板字符串 转换成 element ASTs(parser 解析器)

下面是最简单的例子:

<div>
  <p>{{name}}</p>
</div>

上面的模版转化为下面 element ASTs

{
  tag: "div"
  type: 1,
  // staticRoot: false,
  // static: false,
  plain: true,
  parent: undefined,
  attrsList: [],
  attrsMap: {},
  children: [
      {
      tag: "p"
      type: 1,
      staticRoot: false,
      // static: false,
      plain: true,
      parent: {tag: "div", ...},
      attrsList: [],
      attrsMap: {},
      children: [{
          type: 2,
          text: "{{name}}",
          // static: false,
          expression: "_s(name)"
      }]
    }
  ]
}

详细的 element ASTs (元素抽象语法树)请查看:https://github.com/vuejs/vue/blob/dev/flow/compiler.js

第二步是对 AST 进行静态节点标记,主要用来做虚拟 DOM 的渲染优化(optimizer 优化器)

主要是循环把上面的代码的 staticRoot 及 static 设为 true / false

{
  tag: "div"
  type: 1,
  staticRoot: false,
  static: false,
  plain: true,
  parent: undefined,
  attrsList: [],
  attrsMap: {},
  children: [
      {
      tag: "p"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: {tag: "div", ...},
      attrsList: [],
      attrsMap: {},
      children: [{
          type: 2,
          text: "{{name}}",
          static: false,
          expression: "_s(name)"
      }]
    }
  ]
}

第三步是 使用 element ASTs 生成 render 函数代码字符串(codegen 代码生成器)

主要是把上面的 element ASTs 转成下面的 render 函数代码串

{
  render: `with(this){return _c('div',[_c('p',[_v(_s(name))])])}`
}

格式化后如下(_c 其实已经跟大名顶顶的各通用 virtual dom 中创建 vnode 节点 h 函数结构非常像了,在这里,_c 是 createElement,执行后也是创建 vnode 节点):

with(this){
  return _c(
    'div',
    [
      _c(
        'p',
        [
          _v(_s(name))
        ]
      )
    ]
  )
}

这些缩写函数定义如下:

/*处理 v-once 的渲染函数*/
  Vue.prototype._o = markOnce
  /*将字符串转化为数字,如果转换失败会返回原字符串*/
  Vue.prototype._n = toNumber
  /*将 val 转化成字符串*/
  Vue.prototype._s = toString
  /*处理 v-for 列表渲染*/
  Vue.prototype._l = renderList
  /*处理 slot 的渲染*/
  Vue.prototype._t = renderSlot
  /*检测两个变量是否相等*/
  Vue.prototype._q = looseEqual
  /*检测 arr 数组中是否包含与 val 变量相等的项*/
  Vue.prototype._i = looseIndexOf
  /*处理 static 树的渲染*/
  Vue.prototype._m = renderStatic
  /*处理 filters*/
  Vue.prototype._f = resolveFilter
  /*从 config 配置中检查 eventKeyCode 是否存在*/
  Vue.prototype._k = checkKeyCodes
  /*合并 v-bind 指令到 VNode 中*/
  Vue.prototype._b = bindObjectProps
  /*创建一个文本节点*/
  Vue.prototype._v = createTextVNode
  /*创建一个空 VNode 节点*/
  Vue.prototype._e = createEmptyVNode
  /*处理 ScopedSlots*/
  Vue.prototype._u = resolveScopedSlots

  /*创建 VNode 节点*/
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

最后

本文主要是通过一些简单的模板编译例子来介绍模板编译的过程及步骤,其实并没有涉及代码实现部分。但通过本文我们可以了解到 vue 进行模板编译到底在做什么。结合前面的一些 virtual dom 及 mvvm 文章,如果串联起来我们可以对 mvvm 框架有一定的想象空间了。但实现起来复杂度远远没有这么简单,下一篇,我们将对 vue 模板编译的原理进行详细探索哈。

 

参考引用:
https://segmentfault.com/a/1190000013763590
https://www.cnblogs.com/answershuto/p/7638755.html

作者: 博主

Talk is cheap, show me the code!

发表评论

邮箱地址不会被公开。

Captcha Code