编译一:入口

135 阅读3分钟

Vue 在实现挂载的过程中,如果用户传入的是模板 template,那么会执行编译操作,即将模板编译生成 render 函数,以便后续可以将 Vue 实例通过 render 函数转换成虚拟 DOM,进而通过 patch 将虚拟 DOM 转换成真实 DOM,完成挂载。那么,涉及到的编译过程则是本文要分析的。

编译入口

沿着主线将其实现逻辑整理成一张逻辑图如下:

compile-entrance.png

$mount 函数定义过程中,有一段逻辑是用来处理模板编译的,具体代码实现如下:

// src/platform/web/entry-runtime-with-compiler.js    
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
}

那么,根据编译代码实现,一步一步地来分析模板编译入口究竟是怎样的?

首先,调用函数 compileToFunctions ,传入的参数有三个:编译模板 template、编译配置 options 和 Vue 实例 vm,最终返回对象,包含两个属性:renderstaticRenderFns

compileToFunctions 代码实现如下:

// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions) 

可见,函数 compileToFunctions 是调用 createCompiler 返回的,传入参数 baseOptions,具体如下:

// src/platforms/web/compiler/options.js
export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}

那么 createCompiler 又是如何实现的呢?具体实现如下:

// src/compiler/index.js

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

调用函数 createCompilerCreator ,传入函数 baseCompile 作为参数,需要注意的是函数 baseCompile 是整个编译过程的核心逻辑,最终返回函数 createCompiler

那么,再来看下函数 createCompilerCreator 是如何实现的?

// src/complier/create-compiler.js

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }

      if (options) {
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\s*/)[0].length

          warn = (msg, range, tip) => {
            const data: WarningMessage = { msg }
            if (range) {
              if (range.start != null) {
                data.start = range.start + leadingSpaceLength
              }
              if (range.end != null) {
                data.end = range.end + leadingSpaceLength
              }
            }
            (tip ? tips : errors).push(data)
          }
        }
        // merge custom modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      finalOptions.warn = warn

      const compiled = baseCompile(template.trim(), finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        detectErrors(compiled.ast, warn)
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

从代码实现可看出,最终返回函数 createCompiler;那么,当执行函数 createCompiler 时,其结果是返回对象,包含两个属性:compilecompileToFunctions。而对于 compileToFunctions 则是由调用函数 createCompileToFunctionFn 返回,传入的参数为 compilecreateCompileToFunctionFn 具体实现如下:

// src/compiler/to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }

    // check cache
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile
    const compiled = compile(template, options)

    // check compilation errors/tips
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        if (options.outputSourceRange) {
          compiled.errors.forEach(e => {
            warn(
              `Error compiling template:\n\n${e.msg}\n\n` +
              generateCodeFrame(template, e.start, e.end),
              vm
            )
          })
        } else {
          warn(
            `Error compiling template:\n\n${template}\n\n` +
            compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
            vm
          )
        }
      }
      if (compiled.tips && compiled.tips.length) {
        if (options.outputSourceRange) {
          compiled.tips.forEach(e => tip(e.msg, vm))
        } else {
          compiled.tips.forEach(msg => tip(msg, vm))
        }
      }
    }

    // turn code into functions
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }

    return (cache[key] = res)
  }
}

从代码实现中不难看出,最终返回的是 compileToFunctions;也就是说,在 $mount 挂载过程中,处理模板编译逻辑调用的函数 compileToFunctions 的实现逻辑正在这里,接着来其是如何实现的。

函数接收三个参数:

  • template:编译模板;
  • options:编译配置;
  • vm:Vue 实例。

函数实现逻辑主要有以下几点:

  • 判断是否有缓存,有的话则直接返回,无需编译;
  • 调用函数 compile 执行编译核心逻辑;
  • 检查编译产物是否存在错误;
  • 调用函数 createFuncition 将编译产物转换成 render 函数
  • 设置属性 staticRenderFns
  • 缓存编译结果,并将其返回。

对于编译核心逻辑 compile,其作为参数传入,定义在 src/complier/create-compiler.js,具体实现如上;接着来分析其核心实现。

函数 compile 接收两个参数:

  • template:编译模板;
  • options:编译配置。

其实现逻辑主要有以下几点:

  • 合并编译配置 options
  • 调用函数 baseCompile 执行编译核心逻辑。

baseCompile 是在调用函数 createCompilerCreator 函数时传入,其定义在 src/compiler/index.js 文件中,具体实现如下:

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

由此可见,代码实现主要有三步:

  • 解析模板,生成 AST(parse);
  • 优化 AST(optimize);
  • 生成代码(generate)

这三步也是整个模板编译过程中的核心逻辑,后续会一一分析。

回到函数 compileToFunctions,来看下如何把编译产物转换成函数,即 createFunction 的实现:

// src/compiler/to-function.js
function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

至此,模板编译入口已分析完,

参考链接