Vue3编译过程分析
进入到这个话题之前我们先得回顾下编译器的概念。
编译器(compiler)
编译器的概念
是一种计算机程序,它会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言(目标语言)。(wiki)
编译器的工作流程
一个现代编译器的主要工作流程如下:
源代码(source code)→ 预处理器(preprocessor)→ 编译器(compiler)→ 汇编程序(assembler)→ 目标代码(object code)→ 链接器(linker)→ 可执行文件(executables),最后打包好的文件就可以给电脑去判读运行了。(wiki)
Vue中的编译器
我们平时使用Vue编译后的程序是运行在浏览器中。也就是产出的目标代码是能够被浏览器解释运行的。
所以我们在Vue3中只用跟踪源代码=>编译器=>目标代码这一层就ok了。
Vue种的编译器的作用就是将模板字符串编译成为 JavaScript 渲染函数的代码。然后渲染函数渲染的到的代码就是直接能被浏览器解释运行的。
那么问题来了,在Vue中的编译过程是什么时候执行的?
回答这个问题需要知道当前所使用的的Vue版本是完整版还是运行时版本。这一部分在Vue的官方中也有相应的介绍。
vue.js 运行时 + 编译器的 UMD 版本,也叫完整版。
如果你需要在客户端编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器,即完整版。
vue.runtime.js 运行时版本,相比完整版体积要小大约 30%,也是我们平时开发的时候使用的版本。编译的过程发生在构建时。
当使用 vue-loader 或 vueify 的时候,*.vue 文件内部的模板会在构建时预编译成 JavaScript。你在最终打好的包里实际上是不需要编译器的,所以只用运行时版本即可。
编译过程
我们先通过几个步骤把Vue3的源码跑起来
1.Clone Vue3的源码到本地。地址:github.com/vuejs/vue-n…
2.修改package.json的scripts命令,加上sourcemap方便调试。
"dev": "node scripts/dev.js --sourcemap"
3.运行npm run dev,生成packages/vue/dist/vue.global.js。
4.我们在packages/vue/example中增加一个html文件,这里我建了一个test.html。引入了我们刚刚生成的vue.global.js。这个文件是一个是包含编译器和运行时的“完整”构建版本,因此它支持动态编译模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3 compile</title>
</head>
<body>
<div id="app">{{msg}}</div>
<script src="../dist/vue.global.js"></script>
<script>
const { createApp } = Vue
createApp({
data() {
return {
msg: 'Vue3 compile'
}
}
}).mount('#app')
</script>
</body>
</html>
5.打开test.html。看到msg正常显示,这个是时候我们基础的调试环境就ok了。下一步通过断点去内部探究一下编译的过程。
整体流程
我们知道Vue中编译器的作用就是将模板字符串编译成为 JavaScript 渲染函数的代码。直接在打包文件中的index.ts找到compile方法的返回结果。
在源代码中index.ts第47行。我们先打个断点,然后刷新test.html。
我们可以看到右边的Call Stack中。走到compile这一步所经历的主要几个方法。
1.app.mount获取template
const { mount } = app;
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector);
if (!container) return;
const component = app._component;
if (
!isFunction(component) &&
!component.render &&
!component.template
) {
component.template = container.innerHTML;
// 2.x compat check
if (__COMPAT__ && __DEV__) {
...
// 省略非核心流程
...
}
}
// 这里获取template
container.innerHTML = "";
const proxy = mount(container, false, container instanceof SVGElement);
if (container instanceof Element) {
container.removeAttribute("v-cloak");
container.setAttribute("data-v-app", "");
}
return proxy;
};
return app;
2.compile
compile将传入的template编译成render函数,其实执行的是baseCompile
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
// 其实执行的是baseCompile
return baseCompile(
template,
extend({}, parserOptions, options, {
nodeTransforms: [
// ignore <script> and <tag>
// this is not put inside DOMNodeTransforms because that list is used
// by compiler-ssr to generate vnode fallback branches
ignoreSideEffectTags,
...DOMNodeTransforms,
...(options.nodeTransforms || []),
],
directiveTransforms: extend(
{},
DOMDirectiveTransforms,
options.directiveTransforms || {}
),
transformHoist: __BROWSER__ ? null : stringifyStatic,
})
);
}
3.通过parse获取AST
接着往下看baseCompile
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
...
// 获取AST
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)
...
}
4.transfom
解析AST中的属性,样式,指令等。完善抽象语法树
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
...
// 获取AST
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)
// 解析AST中的属性,样式,指令等。完善抽象语法树
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
// 最后一步通过generate生成渲染函数
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
5.generate
最后一步通过generate生成渲染函数。我们在test.html中写的template最终被编译成了成了下边的渲染函数被返回。
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString } = _Vue
return _toDisplayString(msg)
}
}
6.总结
整个编译过程概括就是template=>ast=>transform=>ast=>generate=>render。这次我们已经基本了解整个编译过程。
在这个过程中,也有一些值得去深入研究的部分。比如词法分析,AST的生成过程(句法分析),transform中对v-指令的解析过程等等,有兴趣的同学可以在相关步骤点进去深入研究。
同时Vue3也在编译过程中做了不少的优化处理。也是值得我们去研究和学习的,比如静态节点提升,补丁标记和缓存事件等。这也是下一步学习和分析的内容。
谢谢观看。