3.3 编译过程 - 模板编译成 render 函数

通过文章前半段的学习,我们对Vue的挂载流程有了一个初略的认识。接下来将先从模板编译的过程展开。阅读源码时发现,模板的编译过程是相当复杂的,要在短篇幅内将整个编译的过程讲开是不切实际的,因此这节剩余内容只会对实现思路做简单的介绍。

3.3.1 template的三种写法

template模板的编写有三种方式,分别是:

  1. // 1. 熟悉的字符串模板
  2. var vm = new Vue({
  3. el: '#app',
  4. template: '<div>模板字符串</div>'
  5. })
  6. // 2. 选择符匹配元素的 innerHTML模板
  7. <div id="app">
  8. <div>test1</div>
  9. <script type="x-template" id="test">
  10. <p>test</p>
  11. </script>
  12. </div>
  13. var vm = new Vue({
  14. el: '#app',
  15. template: '#test'
  16. })
  17. // 3. dom元素匹配元素的innerHTML模板
  18. <div id="app">
  19. <div>test1</div>
  20. <span id="test"><div class="test2">test2</div></span>
  21. </div>
  22. var vm = new Vue({
  23. el: '#app',
  24. template: document.querySelector('#test')
  25. })

三种写法对应代码的三个不同分支。

  1. var template = options.template;
  2. if (template) {
  3. // 针对字符串模板和选择符匹配模板
  4. if (typeof template === 'string') {
  5. // 选择符匹配模板,以'#'为前缀的选择器
  6. if (template.charAt(0) === '#') {
  7. // 获取匹配元素的innerHTML
  8. template = idToTemplate(template);
  9. /* istanbul ignore if */
  10. if (!template) {
  11. warn(
  12. ("Template element not found or is empty: " + (options.template)),
  13. this
  14. );
  15. }
  16. }
  17. // 针对dom元素匹配
  18. } else if (template.nodeType) {
  19. // 获取匹配元素的innerHTML
  20. template = template.innerHTML;
  21. } else {
  22. // 其他类型则判定为非法传入
  23. {
  24. warn('invalid template option:' + template, this);
  25. }
  26. return this
  27. }
  28. } else if (el) {
  29. // 如果没有传入template模板,则默认以el元素所属的根节点作为基础模板
  30. template = getOuterHTML(el);
  31. }

其中X-Template模板的方式一般用于模板特别大的 demo 或极小型的应用,官方不建议在其他情形下使用,因为这会将模板和组件的其它定义分离开。

3.3.2 流程图解

vue源码中编译流程代码比较绕,涉及的函数处理逻辑比较多,实现流程中巧妙的运用了偏函数的技巧将配置项处理和编译核心逻辑抽取出来,为了理解这个设计思路,我画了一个逻辑图帮助理解。

3.3 编译过程 - 模板编译成 render 函数 - 图1

3.3.3 逻辑解析

即便有流程图,编译逻辑理解起来依然比较晦涩,接下来,结合代码分析每个环节的执行过程。

  1. var ref = compileToFunctions(template, {
  2. outputSourceRange: "development" !== 'production',
  3. shouldDecodeNewlines: shouldDecodeNewlines,
  4. shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
  5. delimiters: options.delimiters,
  6. comments: options.comments
  7. }, this);
  8. // 将compileToFunction方法暴露给Vue作为静态方法存在
  9. Vue.compile = compileToFunctions;

这是编译的入口,也是Vue对外暴露的编译方法。compileToFunctions需要传递三个参数:template模板,编译配置选项以及Vue实例。我们先大致了解一下配置中的几个默认选项

  • 1.delimiters 该选项可以改变纯文本插入分隔符,当不传递值时,vue默认的分隔符为 {{}},用户可通过该选项修改
  • 2.comments 当设为 true 时,将会保留且渲染模板中的 HTML注释。默认行为是舍弃它们。接着一步步寻找compileToFunctions根源
  1. var createCompiler = createCompilerCreator(function baseCompile (template,options) {
  2. //把模板解析成抽象的语法树
  3. var ast = parse(template.trim(), options);
  4. // 配置中有代码优化选项则会对Ast语法树进行优化
  5. if (options.optimize !== false) {
  6. optimize(ast, options);
  7. }
  8. var code = generate(ast, options);
  9. return {
  10. ast: ast,
  11. render: code.render,
  12. staticRenderFns: code.staticRenderFns
  13. }
  14. });

createCompilerCreator角色定位为创建编译器的创建者。他传递了一个基础的编译器baseCompile作为参数,baseCompile是真正执行编译功能的地方,他传递template模板和基础的配置选项作为参数。实现的功能有两个

  • 1.把模板解析成抽象的语法树,简称AST,代码中对应parse部分
  • 2.可选:优化AST语法树,执行optimize方法
  • 3.根据不同平台将AST语法树生成需要的代码,对应的generate函数具体看看createCompilerCreator的实现方式。
  1. function createCompilerCreator (baseCompile) {
  2. return function createCompiler (baseOptions) {
  3. // 内部定义compile方法
  4. function compile (template, options) {
  5. ···
  6. // 将剔除空格后的模板以及合并选项后的配置作为参数传递给baseCompile方法,其中finalOptions为baseOptions和用户options的合并
  7. var compiled = baseCompile(template.trim(), finalOptions);
  8. {
  9. detectErrors(compiled.ast, warn);
  10. }
  11. compiled.errors = errors;
  12. compiled.tips = tips;
  13. return compiled
  14. }
  15. return {
  16. compile: compile,
  17. compileToFunctions: createCompileToFunctionFn(compile)
  18. }
  19. }
  20. }

createCompilerCreator函数只有一个作用,利用偏函数将baseCompile基础编译方法缓存,并返回一个编译器函数,该函数内部定义了真正执行编译的compile方法,并最终将compilecompileToFunctons作为两个对象属性返回,这也是compileToFunctions的来源。而内部compile的作用,是为了将基础的配置baseOptions和用户自定义的配置options进行合并,(baseOptions是跟外部平台相关的配置),最终返回合并配置后的baseCompile编译方法。

compileToFunctions来源于createCompileToFunctionFn函数的返回值,该函数会将编译的方法compile作为参数传入。

  1. function createCompileToFunctionFn (compile) {
  2. var cache = Object.create(null);
  3. return function compileToFunctions (template,options,vm) {
  4. options = extend({}, options);
  5. ···
  6. // 缓存的作用:避免重复编译同个模板造成性能的浪费
  7. if (cache[key]) {
  8. return cache[key]
  9. }
  10. // 执行编译方法
  11. var compiled = compile(template, options);
  12. ···
  13. // turn code into functions
  14. var res = {};
  15. var fnGenErrors = [];
  16. // 编译出的函数体字符串作为参数传递给createFunction,返回最终的render函数
  17. res.render = createFunction(compiled.render, fnGenErrors);
  18. // 渲染优化相关
  19. res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
  20. return createFunction(code, fnGenErrors)
  21. });
  22. ···
  23. return (cache[key] = res)
  24. }
  25. }

最终,我们找到了compileToFunctions真正的执行过程var compiled = compile(template, options);,并将编译后的函数体字符串通过creatFunction转化为render函数返回。

  1. function createFunction (code, errors) {
  2. try {
  3. return new Function(code)
  4. } catch (err) {
  5. errors.push({ err: err, code: code });
  6. return noop
  7. }
  8. }

其中函数体字符串类似于"with(this){return _m(0)}",最终的render渲染函数为function(){with(this){return _m(0)}}

至此,Vue中关于编译过程的思路也梳理清楚了,编译逻辑之所以绕,主要是因为Vue在不同平台有不同的编译过程,而每个编译过程的baseOptions选项会有所不同,同时在同一个平台下又不希望每次编译时传入相同的baseOptions参数,因此在createCompilerCreator初始化编译器时便传入参数,并利用偏函数将配置进行缓存。同时剥离出编译相关的合并配置,这些都是Vue在编译这块非常巧妙的设计。