# vue源码 - 编译器之解析

Vue 源码解读(4)—— 异步更新 最后说到刷新 watcher 队列,执行每个 watcher.run 方法,由 watcher.run 调用 watcher.get,从而执行 watcher.getter 方法,进入实际的更新阶段。这个流程如果不熟悉,建议大家再去读一下这篇文章。

当更新一个渲染 watcher 时,执行的是 updateComponent 方法:

// /src/core/instance/lifecycle.js
const updateComponent = () => {
  // 执行 vm._render() 函数,得到 虚拟 DOM,并将 vnode 传递给 _update 方法,接下来就该到 patch 阶段了
  vm._update(vm._render(), hydrating)
}
1
2
3
4
5

可以看到每次更新前都需要先执行一下 vm._render() 方法,vm._render 就是大家经常听到的 render 函数,由两种方式得到:

  • 用户自己提供,在编写组件时,用 render 选项代替模版
  • 由编译器编译组件模版生成 render 选项

今天我们就来深入研究编译器,看看它是怎么将我们平时编写的类 html 模版编译成 render 函数的。

编译器的核心由三部分组成:

  • 解析,将类 html 模版转换为 AST 对象
  • 优化,也叫静态标记,遍历 AST 对象,标记每个节点是否为静态节点,以及标记出静态根节点
  • 生成渲染函数,将 AST 对象生成渲染函数

# 1、目标

深入理解 Vue 编译器的解析过程,理解如何将类 html 模版字符串转换成 AST 对象。

主线:“解析类 HTML 字符串模版,生成 AST 对象”

# 2、入口 $mount

编译器的入口位置在 /src/platforms/web/entry-runtime-with-compiler.js,有两种方式找到这个入口

  • 初始化的最后一步是执行 $mount 进行挂载,在全量的 Vue 包中这一步就会进入编译阶段。
  • 通过 rollup 的配置文件一步步的去找

/src/platforms/web/entry-runtime-with-compiler.js

/**
 * 编译器的入口
 * 运行时的 Vue.js 包就没有这部分的代码,通过 打包器 结合 vue-loader + vue-compiler-utils 进行预编译,将模版编译成 render 函数
 * 
 * 就做了一件事情,得到组件的渲染函数,将其设置到 this.$options 上
 */
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 挂载点
  el = el && query(el)

  // 挂载点不能是 body 或者 html
  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  // 配置项
  const options = this.$options
  // resolve template/el and convert to render function
  /**
   * 如果用户提供了 render 配置项,则直接跳过编译阶段,否则进入编译阶段
   *   解析 template 和 el,并转换为 render 函数
   *   优先级:render > template > el
   */
  if (!options.render) {
    let template = options.template
    if (template) {
      // 处理 template 选项
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // { template: '#app' },template 是一个 id 选择器,则获取该元素的 innerHtml 作为模版
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        // template 是一个正常的元素,获取其 innerHtml 作为模版
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 设置了 el 选项,获取 el 选择器的 outerHtml 作为模版
      template = getOuterHTML(el)
    }
    if (template) {
      // 模版就绪,进入编译阶段
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      // 编译模版,得到 动态渲染函数和静态渲染函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        // 在非生产环境下,编译时记录标签属性在模版字符串中开始和结束的位置索引
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        // 界定符,默认 {{}}
        delimiters: options.delimiters,
        // 是否保留注释
        comments: options.comments
      }, this)
      // 将两个渲染函数放到 this.$options 上
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // 执行挂载
  return mount.call(this, el, hydrating)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

其余章节更新中...

# 总结

  1. 简单说一下 Vue 的编译器都做了什么?

Vue 的编译器做了三件事情:

  • 将组件的 html 模版解析成 AST 对象
  • 优化,遍历 AST,为每个节点做静态标记,标记其是否为静态节点,然后进一步标记出静态根节点,这样在后续更新的过程中就可以跳过这些静态节点了;标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
  • 从 AST 生成运行时的渲染函数,即大家说的 render,其实还有一个,就是 staticRenderFns 数组,里面存放了所有的静态节点的渲染函数
  1. 详细说一说编译器的解析过程,它是怎么将 html 字符串模版变成 AST 对象的?
  • 遍历 HTML 模版字符串,通过正则表达式匹配 "<"
  • 跳过某些不需要处理的标签,比如:注释标签、条件注释标签、Doctype。

    备注:整个解析过程的核心是处理开始标签和结束标签

  • 解析开始标签
    • 得到一个对象,包括 标签名(tagName)、所有的属性(attrs)、标签在 html 模版字符串中的索引位置
    • 进一步处理上一步得到的 attrs 属性,将其变成 [{ name: attrName, value: attrVal, start: xx, end: xx }, ...] 的形式
    • 通过标签名、属性对象和当前元素的父元素生成 AST 对象,其实就是一个 普通的 JS 对象,通过 key、value 的形式记录了该元素的一些信息
    • 接下来进一步处理开始标签上的一些指令,比如 v-pre、v-for、v-if、v-once,并将处理结果放到 AST 对象上
    • 处理结束将 ast 对象存放到 stack 数组
    • 处理完成后会截断 html 字符串,将已经处理掉的字符串截掉
  • 解析闭合标签
    • 如果匹配到结束标签,就从 stack 数组中拿出最后一个元素,它和当前匹配到的结束标签是一对。
    • 再次处理开始标签上的属性,这些属性和前面处理的不一样,比如:key、ref、scopedSlot、样式等,并将处理结果放到元素的 AST 对象上
    • 然后将当前元素和父元素产生联系,给当前元素的 ast 对象设置 parent 属性,然后将自己放到父元素的 ast 对象的 children 数组中
  • 最后遍历完整个 html 模版字符串以后,返回 ast 对象

# 参考

https://juejin.cn/post/6959019076983209992

https://juejin.cn/post/6959019174215548935

上次更新: 4/10/2022, 10:26:50 AM