• 3.3.3 逻辑解析

    3.3.3 逻辑解析

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

    1. Vue.prototype.$mount = function () {
    2. ···
    3. if(!options.render) {
    4. var template = options.template;
    5. if (template) {
    6. var ref = compileToFunctions(template, {
    7. outputSourceRange: "development" !== 'production',
    8. shouldDecodeNewlines: shouldDecodeNewlines,
    9. shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
    10. delimiters: options.delimiters,
    11. comments: options.comments
    12. }, this);
    13. var render = ref.render;
    14. }
    15. ...
    16. }
    17. }

    compileToFunctions有三个参数,一个是template模板,另一个是编译的配置信息,并且这个方法是对外暴露的编译方法,用户可以自定义配置信息进行模板的编译。最后一个参数是Vue实例。

    1. // 将compileToFunction方法暴露给Vue作为静态方法存在
    2. Vue.compile = compileToFunctions;

    Vue的官方文档中,Vue.compile只允许传递一个template模板参数,这是否意味着用户无法决定某些编译的行为?显然不是的,我们看回代码,有两个选项配置可以提供给用户,用户只需要在实例化Vue时传递选项改变配置,他们分别是:

    1.delimiters: 该选项可以改变纯文本插入分隔符,当不传递值时,Vue默认的分隔符为 {{}}。如果我们想使用其他模板,可以通过delimiters修改。

    2.comments : 当设为 true 时,将会保留且渲染模板中的 HTML注释。默认行为是舍弃它们。

    注意,由于这两个选项是在完整版的编译流程读取的配置,所以在运行时版本配置这两个选项是无效的

    接着我们一步步寻找compileToFunctions的根源。

    首先我们需要有一个认知,不同平台对Vue的编译过程是不一样的,也就是说基础的编译方法会随着平台的不同有区别,编译阶段的配置选项也因为平台的不同呈现差异。但是设计者又不希望在相同平台下编译不同模板时,每次都要传入相同的配置选项。这才有了源码中较为复杂的编译实现。

    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. });
    15. var ref$1 = createCompiler(baseOptions);
    16. var compile = ref$1.compile;
    17. var compileToFunctions = ref$1.compileToFunctions;

    这部分代码是在Vue引入阶段定义的,createCompilerCreator在传递了一个baseCompile函数作为参数后,返回了一个编译器的生成器,也就是createCompiler,有了这个生成器,当将编译配置选项baseOptions传入后,这个编译器生成器便生成了一个指定环境指定配置下的编译器,而其中编译执行函数就是返回对象的compileToFunctions

    这里的baseCompile是真正执行编译功能的地方,也就是前面说到的特定平台的编译方法。它在源码初始化时就已经作为参数的形式保存在内存变量中。我们先看看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. }
    7. return {
    8. compile: compile,
    9. compileToFunctions: createCompileToFunctionFn(compile)
    10. }
    11. }
    12. }

    createCompilerCreator函数只有一个作用,利用偏函数的思想将baseCompile这一基础的编译方法缓存,并返回一个编程器生成器,当执行var ref$1 = createCompiler(baseOptions);时,createCompiler会将内部定义的compilecompileToFunctions返回。

    我们继续关注compileToFunctions的由来,它是createCompileToFunctionFn函数以compile为参数返回的方法,接着看createCompileToFunctionFn的实现逻辑。

    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. res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
    19. return createFunction(code, fnGenErrors)
    20. });
    21. ···
    22. return (cache[key] = res)
    23. }
    24. }

    createCompileToFunctionFn利用了闭包的概念,将编译过的模板进行缓存,cache会将之前编译过的结果保留下来,利用缓存可以避免重复编译引起的浪费性能。createCompileToFunctionFn最终会将compileToFunctions方法返回。

    接下来,我们分析一下compileToFunctions的实现逻辑。在判断不使用缓存的编译结果后,compileToFunctions会执行compile方法,这个方法是前面分析createCompiler时,返回的内部compile方法,所以我们需要先看看compile的实现。

    1. function createCompiler (baseOptions) {
    2. function compile (template, options) {
    3. var finalOptions = Object.create(baseOptions);
    4. var errors = [];
    5. var tips = [];
    6. var warn = function (msg, range, tip) {
    7. (tip ? tips : errors).push(msg);
    8. };
    9. // 选项合并
    10. if (options) {
    11. ···
    12. // 这里会将用户传递的配置和系统自带编译配置进行合并
    13. }
    14. finalOptions.warn = warn;
    15. // 将剔除空格后的模板以及合并选项后的配置作为参数传递给baseCompile方法
    16. var compiled = baseCompile(template.trim(), finalOptions);
    17. {
    18. detectErrors(compiled.ast, warn);
    19. }
    20. compiled.errors = errors;
    21. compiled.tips = tips;
    22. return compiled
    23. }
    24. return {
    25. compile: compile,
    26. compileToFunctions: createCompileToFunctionFn(compile)
    27. }
    28. }

    我们看到compile真正执行的方法,是一开始在创建编译器生成器时,传入的基础编译方法baseCompilebaseCompile真正执行的时候,会将用户传递的编译配置和系统自带的编译配置选项合并,这也是开头提到编译器设计思想的精髓。

    执行完compile会返回一个对象,ast顾名思义是模板解析成的抽象语法树,render是最终生成的with语句,staticRenderFns是以数组形式存在的静态render

    1. {
    2. ast: ast,
    3. render: code.render,
    4. staticRenderFns: code.staticRenderFns
    5. }

    createCompileToFunctionFn最终会返回另外两个包装过的属性render, staticRenderFns,他们的核心是with语句封装成执行函数。

    1. // 编译出的函数体字符串作为参数传递给createFunction,返回最终的render函数
    2. res.render = createFunction(compiled.render, fnGenErrors);
    3. res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
    4. return createFunction(code, fnGenErrors)
    5. });
    6. function createFunction (code, errors) {
    7. try {
    8. return new Function(code)
    9. } catch (err) {
    10. errors.push({ err: err, code: code });
    11. return noop
    12. }
    13. }

    至此,Vue中关于编译器的设计思路也基本梳理清楚了,一开始看代码的时候,总觉得编译逻辑的设计特别的绕,分析完代码后发现,这正是作者思路巧妙的地方。Vue在不同平台上有不同的编译过程,而每个编译过程的baseOptions选项会有所不同,同时也提供了一些选项供用户去配置,整个设计思想深刻的应用了偏函数的设计思想,而偏函数又是闭包的应用。作者利用偏函数将不同平台的编译方式进行缓存,同时剥离出编译相关的选项合并,这些方式都是值得我们日常学习的。

    编译的核心是parse,generate过程,这两个过程笔者并没有分析,原因是抽象语法树的解析分支较多,需要结合实际的代码场景才更好理解。这两部分的代码会在后面介绍到具体逻辑功能章节时再次提及。