源码分析:Vue 编译(compile)流程 编译入口解析

891 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Vue 编译(Compile)入口

Vue中的数据渲染到页面的整个流程,我们之前介绍过,不清楚可以点击这里,其中的编译流程,是将我们的template模板编译生成render函数。那为什么需要编译呢,因为template模板中有很多指令(比如@click,v-for等),原生js是识别不了的。接下来我们来分析一下Vue编译的过程:

在我们之前分析的mount过程中有这样一段逻辑,compileToFunctions就是执行编译的函数,将我们定义的template作为参数传入,并生成render函数,下面我们来看下compileToFunctions:

if (template) {
  ......

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

  ......
}

compileToFunctions

compileToFunctions定义在src/platform/web/compiler/index.js中:

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

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

可以看到,compileToFunctions是通过调用createCompiler生成的,参数baseOptions是web平台的一些配置项,继续看createCompiler函数,定义在src/compiler/index.js中:

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'

// `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 {
  // 解析成ast树
  const ast = parse(template.trim(), options)
  // 优化ast树
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 生成代码
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

可以看到这里的createCompiler又是通过createCompilerCreator这个函数去生成的,createCompilerCreator这个函数的参数是一个函数baseCompile,这里面的逻辑是编译过程的核心,包含ast树的生成、ast树的优化、代码生成,这三个流程我们之后再去详细的分析。我们继续看createCompilerCreator这个函数,定义在src/compiler/create-compiler.js:

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    // 这个函数的作用是将一些配置项合并一下,再执行传入的核心编译函数baseCompile
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      // web平台的基础配置,利用原型继承
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []
      finalOptions.warn = (msg, tip) => {
        (tip ? tips : errors).push(msg)
      }

      // 配置合并
      if (options) {
        // 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]
          }
        }
      }

      // 核心编译函数
      const compiled = baseCompile(template, finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        errors.push.apply(errors, detectErrors(compiled.ast))
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

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

在这里,我们看到,compileToFunctions最终是调用了createCompileToFunctionFn这个函数,我们继续看createCompileToFunctionFn,定义在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 */
    // 判断是否支持new Function语法
    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 这儿执行传入的compile函数
    const compiled = compile(template, options)

    // check compilation errors/tips
    // 检查编译的错误等
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        warn(
          `Error compiling template:\n\n${template}\n\n` +
          compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
          vm
        )
      }
      if (compiled.tips && compiled.tips.length) {
        compiled.tips.forEach(msg => tip(msg, vm))
      }
    }

    // turn code into functions
    // 这儿将编译生成的字符串代码转换成render函数,通过createFunction函数
    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)
  }
}
function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

可以看到我们绕了很多圈,最终找到了compileToFunctions这个函数,现在我们来捋一下这个执行流程。

  • 1.进入compileToFunctions函数: 执行compileToFunctions这个函数,这个函数首先会验证当前环境是否支持new Function语法,再利用闭包做了一个编译器缓存,然后执行传入的compile函数。

  • 2.进入compile函数 compile函数将平台的配置拿到做了一个赋值,再结合当前传入的options参数,做了一些合并配置的操作,然后会执行传入的baseCompile函数,

  • 3.进入baseCompile函数 baseCompile函数是编译的核心函数,这个地方会根据传入的template生成ast树、优化ast树、生成字符串代码,完成编译后,

  • 4.完成baseCompile函数的执行,进入compile函数 对编译返回结果compiled对象挂载了errors、tips两个属性,

  • 5.完成compile函数,进入compileToFunctions函数 进入compileToFunctions这个函数,检查编译过程有没有错误,再将生成的字符串代码利用 new Function语法,转换成匿名函数。

最终的这个匿名函数就是最终生成的render函数。关于模板编译的结果可以参考这个网址template-explorer.vuejs.org/

到此为止,我们清楚了编译的入口了,下一节我们继续去分析编译的核心流程!

点击去往下一节