编译入口

当我们使用 Runtime + Compiler 的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js,看一下它对 $mount 函数的定义:

  1. const mount = Vue.prototype.$mount
  2. Vue.prototype.$mount = function (
  3. el?: string | Element,
  4. hydrating?: boolean
  5. ): Component {
  6. el = el && query(el)
  7. /* istanbul ignore if */
  8. if (el === document.body || el === document.documentElement) {
  9. process.env.NODE_ENV !== 'production' && warn(
  10. `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
  11. )
  12. return this
  13. }
  14. const options = this.$options
  15. // resolve template/el and convert to render function
  16. if (!options.render) {
  17. let template = options.template
  18. if (template) {
  19. if (typeof template === 'string') {
  20. if (template.charAt(0) === '#') {
  21. template = idToTemplate(template)
  22. /* istanbul ignore if */
  23. if (process.env.NODE_ENV !== 'production' && !template) {
  24. warn(
  25. `Template element not found or is empty: ${options.template}`,
  26. this
  27. )
  28. }
  29. }
  30. } else if (template.nodeType) {
  31. template = template.innerHTML
  32. } else {
  33. if (process.env.NODE_ENV !== 'production') {
  34. warn('invalid template option:' + template, this)
  35. }
  36. return this
  37. }
  38. } else if (el) {
  39. template = getOuterHTML(el)
  40. }
  41. if (template) {
  42. /* istanbul ignore if */
  43. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  44. mark('compile')
  45. }
  46. const { render, staticRenderFns } = compileToFunctions(template, {
  47. shouldDecodeNewlines,
  48. shouldDecodeNewlinesForHref,
  49. delimiters: options.delimiters,
  50. comments: options.comments
  51. }, this)
  52. options.render = render
  53. options.staticRenderFns = staticRenderFns
  54. /* istanbul ignore if */
  55. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  56. mark('compile end')
  57. measure(`vue ${this._name} compile`, 'compile', 'compile end')
  58. }
  59. }
  60. }
  61. return mount.call(this, el, hydrating)
  62. }

这段函数逻辑之前分析过,关于编译的入口就是在这里:

  1. const { render, staticRenderFns } = compileToFunctions(template, {
  2. shouldDecodeNewlines,
  3. shouldDecodeNewlinesForHref,
  4. delimiters: options.delimiters,
  5. comments: options.comments
  6. }, this)
  7. options.render = render
  8. options.staticRenderFns = staticRenderFns

compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns,它的定义在 src/platforms/web/compiler/index.js 中:

  1. import { baseOptions } from './options'
  2. import { createCompiler } from 'compiler/index'
  3. const { compile, compileToFunctions } = createCompiler(baseOptions)
  4. export { compile, compileToFunctions }

可以看到 compileToFunctions 方法实际上是 createCompiler 方法的返回值,该方法接收一个编译配置参数,接下来我们来看一下 createCompiler 方法的定义,在 src/compiler/index.js 中:

  1. // `createCompilerCreator` allows creating compilers that use alternative
  2. // parser/optimizer/codegen, e.g the SSR optimizing compiler.
  3. // Here we just export a default compiler using the default parts.
  4. export const createCompiler = createCompilerCreator(function baseCompile (
  5. template: string,
  6. options: CompilerOptions
  7. ): CompiledResult {
  8. const ast = parse(template.trim(), options)
  9. if (options.optimize !== false) {
  10. optimize(ast, options)
  11. }
  12. const code = generate(ast, options)
  13. return {
  14. ast,
  15. render: code.render,
  16. staticRenderFns: code.staticRenderFns
  17. }
  18. })

createCompiler 方法实际上是通过调用 createCompilerCreator 方法返回的,该方法传入的参数是一个函数,真正的编译过程都在这个 baseCompile 函数里执行,那么 createCompilerCreator 又是什么呢,它的定义在 src/compiler/create-compiler.js 中:

  1. export function createCompilerCreator (baseCompile: Function): Function {
  2. return function createCompiler (baseOptions: CompilerOptions) {
  3. function compile (
  4. template: string,
  5. options?: CompilerOptions
  6. ): CompiledResult {
  7. const finalOptions = Object.create(baseOptions)
  8. const errors = []
  9. const tips = []
  10. finalOptions.warn = (msg, tip) => {
  11. (tip ? tips : errors).push(msg)
  12. }
  13. if (options) {
  14. // merge custom modules
  15. if (options.modules) {
  16. finalOptions.modules =
  17. (baseOptions.modules || []).concat(options.modules)
  18. }
  19. // merge custom directives
  20. if (options.directives) {
  21. finalOptions.directives = extend(
  22. Object.create(baseOptions.directives || null),
  23. options.directives
  24. )
  25. }
  26. // copy other options
  27. for (const key in options) {
  28. if (key !== 'modules' && key !== 'directives') {
  29. finalOptions[key] = options[key]
  30. }
  31. }
  32. }
  33. const compiled = baseCompile(template, finalOptions)
  34. if (process.env.NODE_ENV !== 'production') {
  35. errors.push.apply(errors, detectErrors(compiled.ast))
  36. }
  37. compiled.errors = errors
  38. compiled.tips = tips
  39. return compiled
  40. }
  41. return {
  42. compile,
  43. compileToFunctions: createCompileToFunctionFn(compile)
  44. }
  45. }
  46. }

可以看到该方法返回了一个 createCompiler 的函数,它接收一个 baseOptions 的参数,返回的是一个对象,包括 compile 方法属性和 compileToFunctions 属性,这个 compileToFunctions 对应的就是 $mount 函数调用的 compileToFunctions 方法,它是调用 createCompileToFunctionFn 方法的返回值,我们接下来看一下 createCompileToFunctionFn 方法,它的定义在 src/compiler/to-function/js 中:

  1. export function createCompileToFunctionFn (compile: Function): Function {
  2. const cache = Object.create(null)
  3. return function compileToFunctions (
  4. template: string,
  5. options?: CompilerOptions,
  6. vm?: Component
  7. ): CompiledFunctionResult {
  8. options = extend({}, options)
  9. const warn = options.warn || baseWarn
  10. delete options.warn
  11. /* istanbul ignore if */
  12. if (process.env.NODE_ENV !== 'production') {
  13. // detect possible CSP restriction
  14. try {
  15. new Function('return 1')
  16. } catch (e) {
  17. if (e.toString().match(/unsafe-eval|CSP/)) {
  18. warn(
  19. 'It seems you are using the standalone build of Vue.js in an ' +
  20. 'environment with Content Security Policy that prohibits unsafe-eval. ' +
  21. 'The template compiler cannot work in this environment. Consider ' +
  22. 'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
  23. 'templates into render functions.'
  24. )
  25. }
  26. }
  27. }
  28. // check cache
  29. const key = options.delimiters
  30. ? String(options.delimiters) + template
  31. : template
  32. if (cache[key]) {
  33. return cache[key]
  34. }
  35. // compile
  36. const compiled = compile(template, options)
  37. // check compilation errors/tips
  38. if (process.env.NODE_ENV !== 'production') {
  39. if (compiled.errors && compiled.errors.length) {
  40. warn(
  41. `Error compiling template:\n\n${template}\n\n` +
  42. compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
  43. vm
  44. )
  45. }
  46. if (compiled.tips && compiled.tips.length) {
  47. compiled.tips.forEach(msg => tip(msg, vm))
  48. }
  49. }
  50. // turn code into functions
  51. const res = {}
  52. const fnGenErrors = []
  53. res.render = createFunction(compiled.render, fnGenErrors)
  54. res.staticRenderFns = compiled.staticRenderFns.map(code => {
  55. return createFunction(code, fnGenErrors)
  56. })
  57. // check function generation errors.
  58. // this should only happen if there is a bug in the compiler itself.
  59. // mostly for codegen development use
  60. /* istanbul ignore if */
  61. if (process.env.NODE_ENV !== 'production') {
  62. if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
  63. warn(
  64. `Failed to generate render function:\n\n` +
  65. fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
  66. vm
  67. )
  68. }
  69. }
  70. return (cache[key] = res)
  71. }
  72. }

至此我们总算找到了 compileToFunctions 的最终定义,它接收 3 个参数、编译模板 template,编译配置 options 和 Vue 实例 vm。核心的编译过程就一行代码:

  1. const compiled = compile(template, options)

compile 函数在执行 createCompileToFunctionFn 的时候作为参数传入,它是 createCompiler 函数中定义的 compile 函数,如下:

  1. function compile (
  2. template: string,
  3. options?: CompilerOptions
  4. ): CompiledResult {
  5. const finalOptions = Object.create(baseOptions)
  6. const errors = []
  7. const tips = []
  8. finalOptions.warn = (msg, tip) => {
  9. (tip ? tips : errors).push(msg)
  10. }
  11. if (options) {
  12. // merge custom modules
  13. if (options.modules) {
  14. finalOptions.modules =
  15. (baseOptions.modules || []).concat(options.modules)
  16. }
  17. // merge custom directives
  18. if (options.directives) {
  19. finalOptions.directives = extend(
  20. Object.create(baseOptions.directives || null),
  21. options.directives
  22. )
  23. }
  24. // copy other options
  25. for (const key in options) {
  26. if (key !== 'modules' && key !== 'directives') {
  27. finalOptions[key] = options[key]
  28. }
  29. }
  30. }
  31. const compiled = baseCompile(template, finalOptions)
  32. if (process.env.NODE_ENV !== 'production') {
  33. errors.push.apply(errors, detectErrors(compiled.ast))
  34. }
  35. compiled.errors = errors
  36. compiled.tips = tips
  37. return compiled
  38. }

compile 函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码:

  1. const compiled = baseCompile(template, finalOptions)

baseCompile 在执行 createCompilerCreator 方法时作为参数传入,如下:

  1. export const createCompiler = createCompilerCreator(function baseCompile (
  2. template: string,
  3. options: CompilerOptions
  4. ): CompiledResult {
  5. const ast = parse(template.trim(), options)
  6. optimize(ast, options)
  7. const code = generate(ast, options)
  8. return {
  9. ast,
  10. render: code.render,
  11. staticRenderFns: code.staticRenderFns
  12. }
  13. })

所以编译的入口我们终于找到了,它主要就是执行了如下几个逻辑:

  • 解析模板字符串生成 AST
  1. const ast = parse(template.trim(), options)
  • 优化语法树
  1. optimize(ast, options)
  • 生成代码
  1. const code = generate(ast, options)

那么接下来的章节我会带大家去逐步分析这几个过程。

总结

编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依赖的配置 baseOptions 会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions 的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。

原文: https://ustbhuangyi.github.io/vue-analysis/compile/entrance.html