Vue3编译过程分析

1,520 阅读4分钟

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。

compile.png 我们可以看到右边的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也在编译过程中做了不少的优化处理。也是值得我们去研究和学习的,比如静态节点提升,补丁标记和缓存事件等。这也是下一步学习和分析的内容。

谢谢观看。