今天学习Vue模板编译原理

事前准备

首先例行编译vue的sourcemap:

  • 克隆或下载 vue官方源码
  • npm i -g rollup
  • 修改 package.json 里的 dev ,添加 sourcemap
    1. rollup -w -c scripts/config.js
    2. --sourcemap
    3. --environment TARGET:web-full-dev
    我们准备如下内容,方便我们调试
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Vue源码剖析</title>
  5. <script src="/dist/vue.js"></script>
  6. </head>
  7. <body>
  8. <div id="demo">
  9. <!-- 静态节点,不需要比对 -->
  10. <h1>Vue模板<span>编译</span></h1>
  11. <p v-if="foo">{{foo}}</p>
  12. <comp></comp>
  13. </div>
  14. <script>
  15. Vue.component("comp", {
  16. template: "<div>I am comp</div>",
  17. });
  18. // 创建实例
  19. const app = new Vue({
  20. el: "#demo",
  21. data: { foo: "foo" },
  22. });
  23. // 输出render函数
  24. console.log(app.$options.render);
  25. </script>
  26. </body>
  27. </html>

思路大纲

  1. 模板从哪里来? render > template > el
  2. 准备 src/compiler/index.js#compilerToFunction 用来把模板变成render函数 const render= compileToFunctions(template);
  3. 这里要用到ast,就是用js来描述html语法,相比后面的vdom,前者更为底层
  4. 解析为ast的过程是一堆正则,头疼,最终得到ast
  5. 处理静态节点标记
  6. 准备生成代码generate,最终目的拿到 h 渲染函数,具体细节是字符串拼装
  7. 字符串转函数,同时拥有指定的作用域 用到了with 和 new Function

    1. export function compileToFunctions(template) {
    2. parseHTML(template);
    3. let code = generate(root);
    4. let render = `with(this){return ${code}}`;
    5. let renderFn = new Function(render);
    6. return renderFn
    7. }

    以上是模板编译,接下来考虑挂载技术实现

  8. 准备 src/lifecycle.js 中的 mountComponent ,利用 _render 得到虚拟DOM,这里就到 renderMixin 方法给Vue拓展 _render 方法了

  9. 这里就是对 _v 创建文本, _c 创建元素 _s _render返回vnode
  10. vnode用来描述虚拟DOM,和 sanbbdom一样的概念
  11. _update 把虚拟DOM变为真实DOM,替换操作 用到了patch方法
  12. 第一次patch,oldVNode为空节点,是直接挂载

以上就是vue初渲染流程。

源码分析

entry-runtime-with-compiler.js

因为我们使用的 vue 打包编译器+运行时。通过之前的介绍,可以进入

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

因为我们是走的 template 模板,会看到 类似 if(template) 的判断代码。
如图所示已经拿到了模板内容,接下来就是如何解析了。
我们在 compileToFunctions 打断点,进入。
截屏2020-05-16 上午10.57.07.png

to-function.js

我们进入 compileToFunctions 内部,发现返回了一个function,这里对 options 做了修改,不重要。
最终看到,把 template传给 compile 函数。
截屏2020-05-16 上午11.04.35.png

进入这个函数。

create-compiler.js

返现还是包装了一层 function,跳过,定位到这一行:

  1. const compiled = baseCompile(template.trim(),finalOptions)

发现,之前的操作其实是对options做修正,终于要进入编译了。我们进入这个函数

compiler/index.js

终于对 template这个字符串进行操作了。整个编译分三步: 解析 + 优化 + 生成。

解析

于是就有了这行代码:

  1. const ast = parse(template.rom().options)

截屏2020-05-16 上午11.15.26.png

这里忽略 parse的具体实现,最终我们拿到了 ast,如上图所示:这是一个对象,通过空间换时间,我们对常用的属性做了解析。

优化

我们的template里有一些静态的内容,比如静态节点,后续vue不应当关心这些内容,因为不会发生变化。
我们会在截图的 Line17 做了 optimize 做了优化。
优化的结果是,多了 static 标记。后续看到 static 如果是 true,干脆就不关注了。

image.png
之前看vue作者再b站开直播,讲到vue3会对这一块做更细致的操作。

生成

通过 generate 操作,我们就拿到了 render函数。

image.png

这样 渲染函数拿到了,就可以执行后续的操作了。注意这里是 字符串。

to-function

拿到 字符串渲染函数了,后续如何render呢,我们退回 to-funciton 这个文件,会看到

  1. res.render = createFunction(compiled.render,funGenErrors)

刚才的截图已知, compiled.render 是一个字符串。我们关注 createFunction 是如何操作的。
定位到 function createFunction 我们发现核心代码:

  1. return new Function(code)

直接把 字符串变为Function。

这样整个渲染函数的流程就走通了。

整体流程如下: