3-2-27-Vue.js 源码阅读-模板编译-过程

Vue 模板编译-过程

编译的入口文件是 src/compiler/create-compiler.js 中的 createCompilerCreator 生成的方法 complieToFunctions 。

下面就简单介绍一下编译的过程吧。

在这里插入图片描述

用于生成 render 函数的方法 compileToFunctions 是通过调用方法 createCompileToFunctionFn 生成的。

export function createCompileToFunctionFn (compile: Function): Function {
  // 定义 cache 通过闭包缓存编译后的结果
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 防止污染 Vue 中的 options clone 一份
    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
    // 1. 读取缓存中的 CompiledFunctionResult 对象,如果有直接返回
    // options.delimiters 是插值表达式使用的符号,默认是花括号,可以自定义
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile
    // 2. 把模板编译成编译对象(render, staticRenderFns),字符串形式的 js 代码
    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 = []

    // 3. 把字符串形式的js代码转换成js方法(通过 new Function)
    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
        )
      }
    }

    // 4. 缓存并返回res对象(render,staticRenderFns方法)
    return (cache[key] = res)
  }
}

这个函数的核心就是先去找缓存中编译的结果,如果有的话直接返回,没有开始编译,并且把字符串形式的js代码转换成函数的形式,最后缓存并且返回。

字符串格式的js代码是由 createFunction 通过 new Function 生成的。

这里的核心就是字符串格式的js代码的生成过程。它是由 compile 方法生成的。

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
  // 通过 baseCompile 把模板编译成 render 函数
  // 返回的是一个对象,对象中有两个成员 render 和 staticRenderFns
  // render 中存放的是字符串形式的 js 代码
  const compiled = baseCompile(template.trim(), finalOptions)
  if (process.env.NODE_ENV !== 'production') {
    detectErrors(compiled.ast, warn)
  }
  compiled.errors = errors
  compiled.tips = tips
  return compiled
}

compie 的核心作用就是合并选项调用 baseCompile 进行编译,最后记录错误并返回编译好的对象。

接着介绍 baseCompile 中编译模板的过程,它是模板编译的核心函数。

baseCompile 里面就只有三个步骤

  • 生成抽象语法树
  • 优化抽象语法树
  • 把优化好的AST对象转换成字符串形式的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 {
  // 把模板转换成 ast 抽象语法树
  // 抽象语法树,用来以树形的方式描述代码解构
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 优化抽象语法树
    optimize(ast, options)
  }
  // 把抽象语法树生成字符串形式的js代码
  const code = generate(ast, options)
  return {
    ast,
    // 渲染函数
    render: code.render,
    // 静态渲染函数,生成静态 Vnode 树
    staticRenderFns: code.staticRenderFns
  }
})
  • 什么是抽象语法树(AST)

    • 抽象语法树简称AST(Abstract Syntax Tree)
    • 使用对象的形式描述树形的代码结构
    • 此处的抽象语法树是用来描述树形结构的HTML字符串
  • 为什么要使用抽象语法树

    • 模板字符串转换成AST后,可以通过AST对模板做优化处理
    • 标记模板中的静态内容,在 patch 的时候直接跳过静态内容
    • 在 patch 的过程中静态内容不需要对比和重新渲染

使用 AST explorer 可以生成各种模板生成的 AST ,非常方便了解 AST 。

https://astexplorer.net/

在 baseCompile 中生成 AST 是由 parse 函数生成的。

parse 函数接受两个参数一个是模板字符串,一个是合并后的选项。返回 AST。

  • 首先去解析 options 中的成员
  • parseHTML 解析 HTML 模板,生成 AST(这个函数 Vue 借鉴了一个开源库 simplehtmlparser.js)
    • start(解析开始标签)
    • end(解析结束标签)
    • chars(解析文本内容)
    • comment(解析注释标签)

生成 AST 后会调用 optimize 方法对 AST 进行优化。

优化的目的是为了标记抽象语法树中的静态节点,这些节点不会重新渲染,在 patch 中直接跳过这些节点。

optimize 将标记静态节点的过程分为了两步

  • 标记静态节点(调用 markStatic)
  • 标记静态根节点(调用 markStaticRoots)

AST对象优化完成之后,接下来是 baseCompile 的最后一步,将优化好的 AST 对象通过 generate 转换成字符串形式的js代码。

generate 接收两个参数一个是优化好的 AST 对象,另一个是选项对象,但会字符串js代码。

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值