内容简介
在《面试官:你能介绍下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.js
、src/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.js
、web/entry-server-renderer.js
、web/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)。
分析返回过程,可概括为三个阶段,每个阶段返回的结果都为函数,分别返回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模板的编译。
上图为运行时编译流程,我们就以运行时编译为例,对编译流程做总结,一共涉及五个文件中的$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;
};
参考
- Vue技术内幕, caibaojian.com/vue-design/…
- umd、commonjs区别,juejin.cn/post/684490…
- browserify介绍,browserify.org/
写在最后
如果大家有其他问题可直接留言,一起探讨!最近我会持续更新Vue源码介绍、前端算法系列,感兴趣的可以持续关注。