Vue编译内幕

360 阅读10分钟

内容简介

在《面试官:你能介绍下Vue的渲染机制吗》中介绍了Vue的渲染过程,Vue.prototype.$mount函数会调用compileToFunctions函数,该函数首先将模板转换为AST抽象语法树,再将AST编译成浏览器可执行的JS代码。我们知道Vue不仅支持了Web端的渲染,还支持了server rendering、weex,此外,uniapp也可以很好的支持Vue。那么Vue在设计编译器时如何保证其自身的高扩展性,本篇内容将以compileToFunctions为入口介绍Vue的编译过程,除了了解Vue是如何设计编译器,也可以学习到Vue是如何通过闭包的方式来支持不同端的compile。

代码结构

先了解下编译的目录全貌,Vue除了支持Web端的编译,还支持了server的编译渲染以及weex,和编译相关的核心代码放在src/compilers目录下,而各个端的编译实现代码包含在src/platforms目录下,例如web、weex目录,服务端的编译单独存放在src/server目录下,并且这三个目录下都包含compiler子目录,集中存放编译相关的代码。

src
--compiler #dir
--platforms #dir
----web #dir
------compiler #dir
------server #dir
----weex #dir
------compiler #dir
--server #dir
----optimizing-compiler #dir

运行时编译

src/platforms/web/compiler/entry-runtime-with-compiler.js中包含有$mount函数,该函数的主要作用是生成模板渲染函数。代码中调用了compileToFunctions函数,该函数包含三个参数,第一个参数为模板template,第二个参数为options,第三个参数为上下文环境(例如组件自身)。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  ...
  // compileToFunctions首先将模板转换为AST,然后在根据AST生成目标代码
  const { render, staticRenderFns } = compileToFunctions(template, {
      outputSourceRange: process.env.NODE_ENV !== 'production',
      shouldDecodeNewlines,
      shouldDecodeNewlinesForHref,
      delimiters: options.delimiters,
      comments: options.comments
    }, this)
    ...
}

options为CompilerOptions类型,附录部分有详细介绍,这里就不再阐述。接着看compileToFunctions函数,由src/platforms/web/compiler/index.js文件提供,该文件先从./options.js拉取基础配置baseOptions,然后调用createCompiler函数,该函数返回的结果包含compile、compileToFunctions两个和编译相关的函数,compile和compileToFunctions的区别在于,compile为函数字符串,而compileToFunctions由new Function(code)将字符串转换为了函数。

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

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

createCompiler定义在src/compiler/index.js文件中,该函数为createCompilerCreator函数的执行结果,createCompilerCreator可理解为编译函数(createCompiler)的创建者(Creator),函数定义为: createCompilerCreator (baseCompile: Function): Function,其作用是定义编译函数createCompiler,我们可以先不用关注createCompiler函数具体的定义。

// createCompilerCreator的形参为compile编译函数
// compile编译函数参数包含template、options,返回结果为包含{ ast, render, statisRernderFns }的对象
// compile函数使用可替换的parser/optimizer/codegen等等处理编译过程
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
  }
})

baseCompile为具体执行编译过程的函数,首先将template按options参数编译为AST,然后调用optimize对ast优化处理,例如某些纯文本或者不包含Vue指令的Dom元素,不需要重新绘制,可以通过optimize函数将该节点标记为静态static标示,避免重新渲染。最后调用generate将ast生成目标代码,baseCompile返回的结果包含ast、render、staticRenderFns三个属性,其中render、staticRenderrFns为generate执行结果,generate函数定义如下:

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // fix #11483, Root level <script> tags should not be rendered.
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

函数体首先实例化了CodegenState对象,该对象包含了辅助函数,其作用是将模板中定义的for、if、template、slot转换对应的代码表达。然后调用genElement函数,结合static定义的辅助函数将ast转换为可执行的代码字符串。最后返回的对象包含render、staticRenderFns属性,其render属性为使用with包含起来的代码模块,其上下文为this,因为render最终在Vue实体上执行,所有这里的this即为组件本身。

回到src/compiler/index.js文件中的createCompilerCreator函数,其定义为createCompilerCreator (baseCompile: Function): Function,为什么参数baseCompile为一个函数?源代码共有两处调用了createCompilerCreator函数,分别为src/compiler/create-compiler.jssrc/server/optimizing-compiler.js文件。由于client端和server端的AST、code生成存在差异,所有分别在这两个文件中定义了不同的baseCompile来处理具体的AST、code生成。

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

通过代码可以看出,server端定义了自己的generate和optimize函数,而parse函数和client端一样,都是使用的src/compiler/parser/index文件中定义的函数。

不同端编译

entry-runtime-with-compiler.js文为运行时编译入口,那么还有其他哪些编译入口?Vue在scripts/config.js文件中定义了几个和编译渲染相关的配置,每项配置包含entry、dest、format、env等属性。

const builds = {
    // Web编译(CommonJS)
  'web-compiler': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
  },
  // Web编译(浏览器使用, umd格式)
  'web-compiler-browser': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/browser.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'VueTemplateCompiler',
    plugins: [node(), cjs()]
  },
  // web服务端渲染(CommonJS),开发环境
  'web-server-renderer-dev': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.dev.js'),
    format: 'cjs',
    env: 'development',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  // web服务端渲染(CommonJS),正式环境
  'web-server-renderer-prod': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.prod.js'),
    format: 'cjs',
    env: 'production',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  // Web服务端渲染(umd),开发环境
  'web-server-renderer-basic': {
    entry: resolve('web/entry-server-basic-renderer.js'),
    dest: resolve('packages/vue-server-renderer/basic.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'renderVueComponentToString',
    plugins: [node(), cjs()]
  }
}

通过dest可发现,web-compiler、web-compiler-browser结果都被打包到vue-template-compiler,这两项主要用于web客户端的研发、正式环境的编译;而web-server-renderer-dev、web-server-renderer-prod、web-server-renderer-basic结果都被打包到vue-server-renderer,分别为服务端开发(cjs)、正式(cjs)、开发(umd)环境的渲染。查看每项配置的dest属性, 一共包含web/entry-compiler.jsweb/entry-server-renderer.jsweb/entry-server-basic-renderer.js三个入口文件。

web-compiler: web/entry-compiler.js
web-compiler-browser: web/entry-compiler.js
web-server-renderer-dev: web/entry-server-renderer.js
web-server-renderer-prod:web/entry-server-renderer.js
web-server-renderer-basic: web/entry-server-basic-renderer.js

除了以上三个入口文件,还有上面介绍的web运行时入口文件web/entry-runtime-with-compiler.js,以及weex入口文件weex/entry-compiler.js文件,一共包含五个编译入口文件。五个编译入口文件归纳到Web、weex、Web-server三个不同的端,所有的入口文件经过一系列流程最终都会调用到compiler/create-compiler.js文件定义的createCompilerCreator,也即上文说的编译函数创建器(crreateComipler)的创建者(Creator)。

编译流程.jpg 分析返回过程,可概括为三个阶段,每个阶段返回的结果都为函数,分别返回createCompilerCreater、createCompiler、compileToFuntions|compile函数。

核心函数

createCompilerCreator

函数定义

ceateComiplerCreator大体结构如下,可以看出几乎和上面说的三个阶段返回一一对应,ceateComiplerCreator没做其他处理,直接返回createCompiler函数,也就是上文所说的编译函数创建器。而编译函数创建器本身又返回compile(编译)、compileToFunctions(编译结果(字符串)转函数)。

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (template: string,options?: CompilerOptions): CompiledResult {
    }

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

核心代码主要集中在compile函数体,其逻辑可分为options处理、执行编译两部分。options处理代码如下:

const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []

let warn = (msg, range, 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]
    }
  }
}

finalOptions.warn = warn

首先根据createCompiler函数传入的baseOptions创建options对象,这里的baseOptions选项就是附录中的CompilerOptions定义,除了提供mustUseProp、whitesapce等属性,CompilerOptions也提供如isUnaryTag、isReservedTag等判断标签类型的函数。

紧接着定义了warn函数,用于存储在编译过程发现的错误或警告信息。然后判断options是否为空,options为compile函数的第二个参数,其类型也为CompilerOptions。如果options包含modules、directives属性,会对这两个属性做特殊处理,通过concat函数将baseOptions定义的modules、options定义的modules联合到一个数组。而directives为对象{[key]:value}形式,因此通过extend将options的directives和baseOptions的directives统一放到finalOptions的directives对象中。那么modules和directives中具体包含什么?

modules

Web编译相关的modules包含在web/compiler/modules/index.js文件,具体内容如下,可以看出主要处理模板中定义的class、style、model三种类型的语法。

import klass from './class'
import style from './style'
import model from './model'

export default [
  klass,
  style,
  model
]

klass、style对象包含staticKeys、transformNode、genData三个属性,而model只包含preTransformNode属性。接下来我们就结合这几个属性对klass、style、model作详细介绍。

// klass、style格式
export default {
  staticKeys: ['*'],
  transformNode,
  genData
}

// model格式
export default {
  preTransformNode
}

首先看web/compiler/module/class.js文件,transformNode函数主要从元素attributes中读取class以及:class属性,并将其值分别存储在staticClass、classBinding属性上。为什么要处理class属性?因为Vue允许像<div class="{{ val }}">这样的定义,所以需要特殊处理,但现在Vue已经不建议使用这种形式定义class,可直接使用<div :class="val">替换。

// 处理元素中定义的:class='{}'、class="{{}}"属性
function transformNode (el: ASTElement, options: CompilerOptions) {
  // 从attributes中读取class值
  const staticClass = getAndRemoveAttr(el, 'class')
  if (staticClass) {
    // 将静态class存放在staticClass属性上,后续编译做单独处理
    el.staticClass = JSON.stringify(staticClass.replace(/\s+/g, ' ').trim())
  }
  // 从attributes读取:class值
  const classBinding = getBindingAttr(el, 'class', false /* getStatic */)
  if (classBinding) {
    // 将:class值存放在classBinding属性上,后续编译做单独处理
    el.classBinding = classBinding
  }
}

// 根据staticClass、classBinding生成数据格式
function genData (el: ASTElement): string {
  let data = ''
  if (el.staticClass) {
    data += `staticClass:${el.staticClass},`
  }
  if (el.classBinding) {
    data += `class:${el.classBinding},`
  }
  return data
}

export default {
  staticKeys: ['staticClass'],
  transformNode,
  genData
}

genData函数用于将所有class统一转换为staticClass:value或class:value形式,并且用逗号分割。最后返回的staticKeys结果为包含staticClass的数组,具体有什么用,在介绍compile函数时再说明。

web/compiler/module/style.js的定义和class类似,主要处理style、:style属性的定义,并将其存放到ASTElement的staticStyle、styleBinding属性上。

directives

web编译相关的directives包含在web/compiler/directives/index.js, 代码如下,主要包含text、html以及model,model用来处理select、checkbox、radio等元素的change事件。

import model from './model'
import text from './text'
import html from './html'

export default {
  model,
  text,
  html
}

html要处理的事物比较简单,为元素设置innerHTML内容,其中_s函数的作用类似于toString()函数,addProp函数为el的props附加新的属性innerHTML。

export default function html (el: ASTElement, dir: ASTDirective) {
  if (dir.value) {
    addProp(el, 'innerHTML', `_s(${dir.value})`, dir)
  }
}

text处理纯文本内容,如果元素的内容为文本内容,则直接设置textContent即可。

export default function text (el: ASTElement, dir: ASTDirective) {
  if (dir.value) {
    addProp(el, 'textContent', `_s(${dir.value})`, dir)
  }
}

model函数的定义如下,7-10行定义变量,value为v-model绑定的值,modifiers为修饰符,如Vue为input类型元素提供了lazy、number、trim修饰符,使用方式如<input v-model.number="age" type="number">。type变量为input元素的type类型。

export default function model (
  el: ASTElement,
  dir: ASTDirective,
  _warn: Function
): ?boolean {
  warn = _warn
  const value = dir.value
  // 修饰符,如lazy、number、trim等
  const modifiers = dir.modifiers
  // 元素标签
  const tag = el.tag
  // 元素类型,如 input标签的type,checkbox、radio等等
  const type = el.attrsMap.type

  if (el.component) {
    // 处理自定义组件使用v-model的情况,为el绑定model属性
    // 绑定的model格式为{ value, expression, callback }
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } else if (tag === 'select') {
    genSelect(el, value, modifiers)
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers)
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers)
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  }

  // ensure runtime directive metadata
  return true
}

15至33行代码处理不同元素的绑定和change事件,genComponentModel处理自定义组件的v-model绑定,例如<A v-model="data.value" />,最终会给el.model赋值{ value, expression, callback },其中callback的格式如下所示,当数据发生变化会回调callback函数,然后通过$set设置到数据对象上。

function ($$v) { $set(data, 'value', $$v) }

再以处理select类型的genSelect函数为例,代码实现如下,首先判断是否有加number修改器,然后定义selectedVal获取选中value值的函数字符串,通过call函数调用Array的filter将状态为selected的option筛选出来。接下来拼凑code代码,其作用和genComponentModel中的回调类似,当select元素触发change事件,也会调用$set函数来更新绑定数据的值。

function genSelect (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  const selectedVal = `Array.prototype.filter` +
    `.call($event.target.options,function(o){return o.selected})` +
    `.map(function(o){var val = "_value" in o ? o._value : o.value;` +
    `return ${number ? '_n(val)' : 'val'}})`

  const assignment = '$event.target.multiple ? $$selectedVal : $$selectedVal[0]'
  let code = `var $$selectedVal = ${selectedVal};`
  code = `${code} ${genAssignmentCode(value, assignment)}`
  addHandler(el, 'change', code, null, true)
}

compile

现在回到createCompilerCreator函数体中定义的compile函数,其逻辑可分为options处理、执行编译两部分,options处理我们已经了解了,接下来看compile函数剩余的代码,首先调用baseCompile执行编译,baseCompile是通过createCompilerCreator函数传参带过来的,定义在src/compiler/index.js文件中。如果编译过程发生有异常或者有警告信息,这些信息将设置到compiled的errors、tips属性上。在上文"Web端编译入口"已经介绍过baseCompile函数定义,为了加深理解,接下来再回顾下。

const compiled = baseCompile(template.trim(), finalOptions)
compiled.errors = errors
compiled.tips = tips
return compiled

baseCompile

src/compiler/index.js文件定义了ceateCompiler,调用createCompilerCreator定义编译过程,到目前我们知道该函数的主要作用是预处理options参数,提供modules定义的model、class、style处理,以及directives定义的html、text、model处理。

baseCompile函数流程,先将options传给parse函数,使用提供的属性选项,将template转换为AST抽象语法树,所有在template定义的内容都转换到ASTElement的属性上,AST共计差有80多个属性。然后调用optimize优化ast,最后再调用generate函数将AST转换为可执行的JS代码。

// createCompilerCreator的形参为compile编译函数
// compile编译函数参数包含template、options,返回结果为包含{ ast, render, statisRernderFns }的对象
// compile函数使用可替换的parser/optimizer/codegen处理编译过程
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
  }
})

总结

Vue除了支持Web端SPA应用,还支持服务端SSR渲染。本地开发可以使用entry-runtime-with-compiler.js提供的运行时编译,Vue2.0还提供了vue-template-compiler,作为一个独立的包,支持将*.vue进行预编译,也即是预先执行template->ast->render,生成渲染函数,可提升运行时渲染效率。除此之外,Vue还提供了weex-template-compiler独立包,支持weex模板的编译。

编译流程总结.jpg 上图为运行时编译流程,我们就以运行时编译为例,对编译流程做总结,一共涉及五个文件中的$mount、compileToFunctions、createCompiler、createComilerCreator、ceateCompileToFunctionFn函数。

可以看出$mount需要的其实就是src/compiler/to-function.js文件中createCompileToFuncionFn函数返回的compileToFunctions,那么为什么经过3个中间函数的处理?其目的就是为了让编译过程具备高扩展性。

在第二个文件src/platforms/web/compiler/index.js中定义了web编译需要的基础选项baseOptions,然后传递给createCompiler函数。第三个文件src/compiler/index.js定义了基础编译函数baseCompile,传递给createCompilerCreator函数。第四个文件src/compiler/create-compiler.js定义了编译函数compile,compile将把用户定义的options和baseOptions合并,然后调用baseCompile执行编译,返回编译结果,但compile函数不会立即执行,而是传递给第五个文件src/compiler/to-function.js。第五个文件定义了createCompileToFuncionFn函数,该函数定义了用户需要的compileToFunctions,compileToFunctions在执行compile之前,先检查cached中是否有缓存编译结果,有则直接返回编译结果,否则执行compile函数,所以compileToFunctions的主要作用就是实现缓存。

本篇我们只介绍了编译过程,baseCompile函数中有调用parse将模板生成AST、调用optimize对AST进行优化,调用generate将AST生成可执行的代码。下一篇我将对parse、optimize以及generate的执行细节做介绍,敬请期待!

附录

CompilerOptions说明:

declare type CompilerOptions = {
  warn?: Function; // 允许在不同环境中自定义警告,例如node 
  modules?: Array<ModuleOptions>; // 平台特有的模块配置,例如class、style等 
  directives?: { [key: string]: Function }; // 平台特有的指令directives配置
  staticKeys?: string; // 生成AST使用的属性,主要用于优化 
  isUnaryTag?: (tag: string) => ?boolean; // 校验tag是否为一元标签
  canBeLeftOpenTag?: (tag: string) => ?boolean; // 可以直接进行闭合的标签 
  isReservedTag?: (tag: string) => ?boolean; // 平台保留标签
  preserveWhitespace?: boolean; // 保留元素之间的空白,该选项以过期
  whitespace?: 'preserve' | 'condense'; // 空白处理方式,保留或压缩
  optimize?: boolean; // 优化静态内容 // optimize static content?

  // web平台特有选项
  mustUseProp?: (tag: string, type: ?string, name: string) => boolean; // 检查一个属性是否应该绑定为元素的property
  isPreTag?: (attr: string) => ?boolean; // 检查标签是否需要保留空白 // check if a tag needs to preserve whitespace
  getTagNamespace?: (tag: string) => ?string; 
  expectHTML?: boolean; // 非web平台构建,设置为false
  isFromDOM?: boolean;
  shouldDecodeTags?: boolean; // 浏览器兼容问题, 检查不同浏览器在生成标签时,是否有特殊编码
  shouldDecodeNewlines?:  boolean; // 浏览器兼容问题, 检查不同浏览器在生成标签时,是否有特殊编码
  shouldDecodeNewlinesForHref?: boolean; // 浏览器兼容问题, 检查不同浏览器在生成标签时,是否有特殊编码
  outputSourceRange?: boolean;

  // 用户自定义配置 runtime user-configurable
  delimiters?: [string, string]; // 模板分隔符
  comments?: boolean; // 保留模板中的注释

  // ssr编译优化 
  scopeId?: string; 
};

参考

  1. Vue技术内幕, caibaojian.com/vue-design/…
  2. umd、commonjs区别,juejin.cn/post/684490…
  3. browserify介绍,browserify.org/

写在最后

如果大家有其他问题可直接留言,一起探讨!最近我会持续更新Vue源码介绍、前端算法系列,感兴趣的可以持续关注