# 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
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
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
其余章节更新中...
# 总结
- 简单说一下 Vue 的编译器都做了什么?
Vue 的编译器做了三件事情:
- 将组件的 html 模版解析成 AST 对象
- 优化,遍历 AST,为每个节点做静态标记,标记其是否为静态节点,然后进一步标记出静态根节点,这样在后续更新的过程中就可以跳过这些静态节点了;标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
- 从 AST 生成运行时的渲染函数,即大家说的 render,其实还有一个,就是 staticRenderFns 数组,里面存放了所有的静态节点的渲染函数
- 详细说一说编译器的解析过程,它是怎么将 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