• 编译入口
    • 总结

    编译入口

    当我们使用 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