[Vue源码学习]3-编译(上)

398 阅读3分钟

编译In Vue

我们前面分析了Vue中的实例化过程和响应式原理,接下来我们会补全Vue在实例化最初的一部分内容,就是编译

最初我们举例子都是通过直接编写render进行的,这个其实和我们开发中遇到的情况不太一样。虽然我们可以直接编写render函数用于渲染一些比较复杂但是有规律的内容,但是大部分情况下,我们都是直接编写template模板来进行开发的,这也更符合我们的开发习惯

提到template和render函数的关系,这里就补充一下

Vue.js主要由两个内容构成:runtime和compiler。我们一般用vue-cli完成打包的项目,内部的vue都是runtime only的,compiler的工作是把template转换成render函数,这个转换逻辑在我们npm run build的时候,通过webpack的vue-loader就已经完成了,所以vue在打包的时候不用带上compiler,这个也是推荐的做法

但是这个也不是绝对的,我们在业务场景中,为了方便控制组件的按需加载,直接把使用的template拼接在html内,通过使用带compiler的vue在运行时进行模板编译,来实现我们想要的效果,这样大大降低了按需加在的实现成本,但是,随之带来的是页面性能的牺牲(compiler解析十分耗时)

ok那回到这里,下面我们就回到实例化最初的地方,看看Vue是在哪一步进行的template 编译工作

源码分析

入口

回忆一下我们第一期分享的实例化内容,我们在初始化的时候会调用$mount函数进行开始把vue内容渲染到我们的页面上,在流程图内我们会判断是否使用了template,如果是有了那么就执行下面的内容:

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

const { render, staticRenderFns } =  compileToFunctions(template, {
    shouldDecodeNewlines,
    shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
  }, this)
options.render = render
options.staticRenderFns = staticRenderFns

compileToFunctions的作用就是把模板编译成render以及staticRenderFns,我们来看看他的实现逻辑

src/platforms/web/compiler/index.js

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

我们发现,其实compileToFunctions是createCompiler的返回值,然后看看createCompiler又是createCompilerCreator的返回值(禁止套娃)

这里比较有意思的是,createCompilerCreator传入的参数是一个function,所以整个调用链就像这样:

$mount => compileToFunctions => createCompiler => createCompilerCreator(function baseCompile() {})

根据类型系统,我们发现传入一个函数,返回一个函数,那么十有八九就是提前固化参数了,跟我们实例化时候讲到的的nodeOpts有点像,我这里也能发现,是提前固化了baseCompile的函数逻辑

那我们看看具体的createCompilerCreator的实现逻辑

src/compiler/create-compiler.js

export function createCompilerCreator (baseCompile: Function): Function {
    return function createCompiler (baseOptions: CompilerOptions) {
        // ...
        function compile (template, options) {
            if (options) {
                // ...
                const compiled = baseCompile(template, finalOptions)
                return compiled
            }
        }
        return {
          compile,
          compileToFunctions: createCompileToFunctionFn(compile)
        }
    }
}

我们可以发现,用上了我们提前固化的baseCompile函数,但是返回compileToFunctions的过程也没有想象中的顺利,又是一个套娃,compileToFunctions是createCompileToFunctionFn的返回值

这个是不是为了把我们固化的baseCompile固化到compile内,再把compile固化到compileToFunctions内所做的工作呢?

我们看看createCompileToFunctionFn的源代码

src/compiler/to-function.js

export function createCompileToFunctionFn (compile) {
    const cache = Object.create(null)
    
    return function compileToFunctions(template, options, vm) {
        // ...
        const compiled = compile(template, options)
        // ...
        return (cache[key] = res)
    }
}

其实归根结底就是compile函数的调用,这就是执行逻辑的所在,我们再回过头来,反向看看这个compile到底是啥,这个链条应该是这样的:

compile 
<= createCompileToFunctionFn的入参 
<= createCompiler传入变量function compile() {} 
<=  compile中调用baseCompile 
<= createCompilerCreator中传入function baseCompile() {}

所以编译入口就是我们最开始传入的baseCompile...我还是没太搞懂为啥这么做,先继续看看把

主要执行的有以下逻辑:

  • 解析模板字符串生成AST
  • 优化语法树
  • 生成代码

代码解释就是这些

// 1
const ast = parse(template.trim(), options)
// 2
optimize(ast, options)
// 3
const code = generate(ast, options)

我们终于找到了编译入口,之所以这么设计,和nodeOpts也是异曲同工之妙,还是为了固化baseOptions,把多平台的支持拆分开来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的