前言
之前的系列我们有分析过模版编译成真实DOM的过程,有一个环节就是把模版编译成了render函数,这个过程我们称作编译;
Vue.js 提供了 2 个版本,一个是 Runtime + Compiler 的,一个是 Runtime only 的,前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpack 的 vue-loader 事先把模板编译成 render函数;
理解编译过程对理解Vue的指令以及内置组件有更好的帮助,由于编译过程相对复杂,我们只分析整体的流程,不要太扣细节,正所谓成大事者,不拘小节😄;
编译入口
$mount
源码:src/platforms/web/entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ...
/**
* 编译的入口
* 将template编译成render函数,staticRenderFns是编译优化,static静态不需要在VNode更新时进行patch,优化性能
*/
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
/* 将编译成的render赋值给options.render */
options.render = render
options.staticRenderFns = staticRenderFns
}
/* 最后执行一开始缓存下来的原型上的mount */
return mount.call(this, el, hydrating)
}
这段代码我们之前分析过,compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns
源码:src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
可以看到 compileToFunctions 方法实际上是 createCompiler 方法的返回值,该方法接收一个编译配置参数,接下来我们来看一下 createCompiler 方法的定义。
createCompiler
源码: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
}
})
createCompiler 方法实际上是通过调用 createCompilerCreator 方法返回的,该方法传入的参数是一个函数,真正的编译过程都在这个 baseCompile 函数里执行,baseCompile我们后边分析,那么 createCompilerCreator 又是什么呢
createCompilerCreator
它的定义在 src/compiler/create-compiler.js 中
export function createCompilerCreator (baseCompile: Function): Function {
/*
* createCompiler主要做了两件事:
* 1. 合并options,将平台自有的option与传入的option进行合并
* 2. baseCompile,进行模版的基础编译
*/
return function createCompiler (baseOptions: CompilerOptions) {
/*编译,将模板template编译成AST、render函数以及staticRenderFns函数*/
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
if (options) {
/*做下面这些merge的目的因为不同平台可以提供自己本身平台的一个baseOptions,
内部封装了平台自己的实现,然后把共同的部分抽离开来放在这层compiler中,
所以在这里需要merge一下*/
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
/* 合并指令 */
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
for (const key in options) {
/*合并其余的options,modules与directives已经在上面做了特殊处理了*/
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
/*基础模板编译,得到编译结果*/
const compiled = baseCompile(template.trim(), finalOptions)
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
可以看到createCompilerCreator返回了一个 createCompiler 的函数,它接收一个 baseOptions 的参数,返回的是一个对象,包括 compile 方法属性和 compileToFunctions 属性,这个 compileToFunctions 对应的就是 $mount 函数调用的 compileToFunctions 方法,它是调用 createCompileToFunctionFn 方法的返回值,我们接下来看一下 createCompileToFunctionFn 方法。
createCompileToFunctionFn
它的定义在 src/compiler/to-function/js 中:
export function createCompileToFunctionFn (compile: Function): Function {
/* 闭包内的缓存器 */
const cache = Object.create(null)
/*带缓存的编译器,同时staticRenderFns以及render函数会被转换成Funtion对象*/
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
const key = options.delimiters
? String(options.delimiters) + template
: template
/* 取缓存 */
if (cache[key]) {
return cache[key]
}
/* 编译的核心 */
const compiled = compile(template, options)
/*将render转换成Funtion对象*/
res.render = createFunction(compiled.render, fnGenErrors)
/*将staticRenderFns全部转化成Funtion对象 */
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
/*存放在缓存中,以免每次都重新编译*/
return (cache[key] = res)
}
}
可以看到,我们创建了一个在闭包内的空对象,每次将之前的编译结果缓存起来,下次再进来就会先取缓存,避免每次取重新编译。
至此我们总算找到了 compileToFunctions 的最终定义,它接收 3 个参数、编译模板 template,编译配置 options 和 Vue 实例 vm。核心的编译过程就一行代码:
const compiled = compile(template, options)
我们上边已经贴出了compile的源码,compile 函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码:
const compiled = baseCompile(template, finalOptions)
因此,兜兜转转我们又回到了baseCompile,它也正是编译的真正入口
baseCompile
源码:src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
/* 解析模板字符串生成 AST */
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
/**
* 优化语法树
* 优化的目标:生成模版AST,检测不需要进行DOM改变的静态子树,一🥚检测到这些静态子树,我们就能做以下事情:
* 1.把他们变成常数, 这样就不需要每次重新渲染的时候创建新的节点
* 2.在patch过程中直接跳过
*/
/* optimize主要作用是标记static静态节点,当更新的时候,会直接跳过静态节点,性能优化 */
optimize(ast, options)
}
/**
* 将AST转化成render funtion字符串的过程
* 根据AST生成所需的code,内部包含render与staticRenderFns
*/
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
它主要做了三件事
1.解析模板字符串生成 AST
const ast = parse(template.trim(), options)
2.优化语法树
optimize(ast, options)
3.生成代码
const code = generate(ast, options)
小结:编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依赖的配置 baseOptions 会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions 的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。
接下来我们分别解析parse、optimize、generate
parse
编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。
这个过程是比较复杂的,它会用到大量正则表达式对字符串解析。
举个🌰子:
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
经过 parse 过程后,生成的 AST 如下:
ast = {
'type': 1, // 节点类型
'tag': 'ul', // 标签名
'attrsList': [], // 属性列表
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow', // 指令
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined, // 父子关系
'plain': false,
'staticClass': 'list', // 静态class
'classBinding': 'bindCls',
'children': [{ // 子节点
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
]
]
}]
}
可以看到,生成的 AST 是一个树状结构,每一个节点都是一个 ast element,除了它自身的一些属性,还维护了它的父子关系,如 parent 指向它的父节点,children 指向它的所有子节点。
parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,来达到构造 AST 树的目的。
AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。
当 AST 树构造完毕,下一步就是 optimize 优化这颗树。
optimize
optimize就是对AST这棵树做优化,那么为什么要有优化过程,因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对。
optimize的作用就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点就将相应节点做静态标志,比如我们上边的例子经过optimize函数优化后变成了这样:
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'static': false,
'staticRoot': false,
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'static': false,
'staticRoot': false,
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
],
'static': false
]
}]
}
可以看到每个节点又多了static属性,我们通过 optimize 我们把整个 AST 树中的每一个 AST 元素节点标记了 static 和 staticRoot(静态根节点),它会影响我们接下来执行代码生成的过程。
generate
编译的最后一步就是把优化后的 AST 树转换成可执行的代码
/**
* 将AST转化成render funtion字符串的过程
* 根据AST生成所需的code,内部包含render与staticRenderFns
*/
const code = generate(ast, options)
接着上边的示例,经过编译,生成的render如下:
with(this){
return (isShow) ?
_c('ul', {
staticClass: "list",
class: bindCls
},
_l((data), function(item, index) {
return _c('li', {
on: {
"click": function($event) {
clickItem(index)
}
}
},
[_v(_s(item) + ":" + _s(index))])
})
) : _e()
}
这里的 _c 函数就是系列一(juejin.cn/post/684490…)讲到的createElement,作用是创建虚拟VNode,定义在 src/core/instance/render.js 中。
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
而 _l、_v 定义在 src/core/instance/render-helpers/index.js 中:
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
顾名思义,_c 就是执行 createElement 去创建 VNode,而 _l 对应 renderList 渲染列表;_v 对应 createTextVNode 创建文本 VNode;_e 对于 createEmptyVNode创建空的 VNode。看一下generate
源码: src/compiler/codegen/index.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
}
}
generate 函数首先通过 genElement(ast, state) 生成 code,再把 code 用 with(this){return ${code}}} 包裹起来。先来看一下 genElement
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
const data = el.plain ? undefined : genData(el, state)
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
基本就是判断当前 AST 元素节点的属性执行不同的代码生成函数,最后都是返回拼接好的code,有兴趣的可自行去查看每个方法的实现,这里就不一一详细介绍了;
总结
通过parse将模版template生成AST语法树,然后经过optimize方法的优化,标记了一些非响应式的静态节点,在patch的时候可跳过这些标记好的静态属性,优化算法,最后通过generate生成最终的code码,创建生成最终的标签节点。