我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
代码生成器是模板编译的最后一步,所做的工作是根据 AST(抽象语法树)生成代码字符串,这个代码字符串就是 render 函数的代码内容,代码字符串可以通过 new Function(code) 生成真正的 render 函数。
render 函数的作用是生成 vnode(虚拟 DOM),vnode 可以理解成真实 DOM 的抽象表示,通过 vnode 可以在页面上渲染出对应的真实 DOM,使用 vnode 的好处是:Vue 可以根据新旧 vnode 之间的差异计算出哪些真实 DOM 需要更新,然后只更新需要更新的真实 DOM,这样可以最小程度的操作真实 DOM,提高性能。
接下来,以一个简单的模板字符串为例,看看其所生成的 AST 和代码字符串是什么样子的。
模板字符串:
let template = `
<div class="container">
<h1 v-if="isShow">我是标题</h1>
<ul>
<li v-for="item in names">{{item}}</li>
</ul>
</div>
`
抽象语法树:
let ast = {
attrsList: [],
attrsMap: {class: "container"},
children:[
{
attrsList: [],
attrsMap: {v-if: "isShow"},
if: "isShow",
children:[
{type: 3, text: "我是标题", static: true}
],
ifConditions:[
{exp: "isShow"}
],
ifProcessed: true,
plain: true,
static: false,
staticRoot: false,
tag: "h1",
type: 1
},
{
attrsList: [],
attrsMap: {},
children: [
{
alias: "item",
attrsList: [],
attrsMap: {v-for: "item in names"},
children: [
{type: 2, expression: "_s(item)", text: "{{item}}", static: false}
],
for: "names",
forProcessed: true,
plain: true,
static: false,
staticRoot: false,
tag: "li",
type: 1
}
],
plain: true,
static: false,
staticRoot: false,
tag: "ul",
type: 1
}
],
parent: undefined,
plain: false,
static: false,
staticClass: ""container"",
staticRoot: false,
tag: "div",
type: 1
}
模板字符串:
let code = `
with (this) {
return _c(
'div',
{staticClass: 'container'},
[
(isShow)?_c('h1', [_v('我是标题')]):_e(),
_c('ul',_l(
(names),
function(item){
return _c('li',[_v(_s(item))])
}
))
]
)
}
`
1,render 函数是如何生成虚拟 DOM 的?
render 函数的内部调用 _c、_v、_s 之类的辅助函数生成虚拟 DOM。
1-1,这些辅助函数被定义在哪里?
_c 被定义在 Vue 的实例上。
export function initRender (vm: Component) {
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
}
从上面代码可知:_c 函数的内部是通过调用 createElement 函数实现生成虚拟 DOM 的功能的,而且 _c 被定义到了 Vue 的实例上面。
其他一些辅助函数被定义在 Vue 的原型对象中,看下面的源码:
export function renderMixin (Vue: Class<Component>) {
installRenderHelpers(Vue.prototype)
}
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 的其他辅助函数是定义在 Vue 的原型对象中的。
1-2,render 函数内部是如何调用到这些辅助函数的?
从上一小节可知:创建 vnode 的辅助函数被定义在了 Vue 的实例和原型对象中。这也就说明,可以通过 Vue 实例获取到这些辅助函数。
接下来需要了解的是,with 关键字的作用,借助 with,可以改变 with 后面代码块的作用域,上面 render 函数的内部借助 with,将代码块中的作用域指向 this。接下来,主要看看这个 this 到底指向的是什么。
首先看看 render 函数的调用时机:
export function renderMixin (Vue: Class<Component>) {
Vue.prototype._render = function (): VNode {
const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
}
}
render 函数使用 call 执行代码体,render 函数内部的 this 指向 vm._renderProxy,接下来看看 vm._renderProxy 指向的是什么。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
}
}
从上面两处源码可知,render 函数执行时,其内部的 this 就是 vm(Vue 实例),又因为 redner 函数的内部使用 with 关键字将代码块的作用域指向了 this,所以 render 函数的作用域指向的就是 vm。因此,render 函数内是能直接调用的 _c、_v、_s 这些函数的。
2,代码生成器的源码解析
上一小节,我们知道了,render 函数生成 vnode 的原理。接下来看看代码生成器的源码。
注意:这一小节以一个简单的例子,理解代码生成器的主线流程即可,没有必要追求一次就把所有的细节搞懂。具体的细节等到后面分析具体的特性再详细分析。
以文章一开始的模板字符串为例,看代码生成器的运行机制:
let template = `
<div class="container">
<h1 v-if="isShow">我是标题</h1>
<ul>
<li v-for="item in names">{{item}}</li>
</ul>
</div>
`
2-1,src/compiler/index.js ==> baseCompile():代码生成器的入口
export const createCompiler = createCompilerCreator(
// 真正执行编译功能的函数,分为三步走:(1)解析器 ==>(2)优化器 ==>(3)代码生成器
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1,解析器。将模板字符串转换成抽象语法树
const ast = parse(template.trim(), options)
// 2,优化器。遍历抽象语法树,标记静态节点,
// 因为静态节点是不会变化的,所以重新渲染视图的时候,能够直接跳过静态节点,提升效率。
optimize(ast, options)
// 3,代码生成器。使用抽象语法树生成渲染函数字符串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
)
parse 函数是解析器,optimize 函数是优化器,generate 函数就是本文讨论的代码生成器,参数是优化过后的抽象语法树 AST 和配置对象 options。
generate 函数在 src/compiler/codegen/index.js 文件中,接下来看看该文件中的内容。
2-2,src/compiler/codegen/index.js
代码生成器的大部分源码都在这个文件中,首先看下该文件代码的主体结构。
// 代码生成器。AST ==> render 函数代码字符串
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// 对 ast 进行判断。
// 如果其存在的话,就执行 genElement(ast, state) 生成代码字符串,
// 否则的话,直接返回 '_c("div")',创建空的 div。
const code = ast ? genElement(ast, state) : '_c("div")'
return {
// 将生成的代码字符串(code)拼接到 `with(this){return ${code}}`,形成最终的代码字符串
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
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) {
// 用于处理 v-if
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
}
}
function genStatic (el: ASTElement, state: CodegenState): string {}
function genOnce (el: ASTElement, state: CodegenState): string {}
export function genIf (el: any, state: CodegenState, altGen?: Function, altEmpty?: string): string {}
function genIfConditions (conditions: ASTIfConditions, state: CodegenState, altGen?: Function, altEmpty?: string): string {}
export function genFor (el: any, state: CodegenState, altGen?: Function, altHelper?: string): string {}
export function genData (el: ASTElement, state: CodegenState): string {}
function genDirectives (el: ASTElement, state: CodegenState): string | void {}
function genInlineTemplate (el: ASTElement, state: CodegenState): ?string {}
function genScopedSlots (slots: { [key: string]: ASTElement }, state: CodegenState): string {}
function genScopedSlot (key: string, el: ASTElement, state: CodegenState): string {}
function genForScopedSlot (key: string, el: any, state: CodegenState): string {}
export function genChildren (el: ASTElement, state: CodegenState, checkSkip?: boolean, altGenElement?: Function, altGenNode?: Function): string | void {}
function getNormalizationType (children: Array<ASTNode>, maybeComponent: (el: ASTElement) => boolean): number {}
function genNode (node: ASTNode, state: CodegenState): string {}
export function genText (text: ASTText | ASTExpression): string {}
export function genComment (comment: ASTText): string {}
function genSlot (el: ASTElement, state: CodegenState): string {}
function genComponent (componentName: string, el: ASTElement, state: CodegenState): string {}
function genProps (props: Array<{ name: string, value: string }>): string {}
Vue 将生成不同特性代码字符串的工作都细致的拆分到了对应的函数中。这样,当处理到对应特性的 AST 节点的时候,调用对应的处理函数就可以了。
在众多的函数中,有两个函数应该重点关注:
- 第一个是作为代码生成器入口的 generate 函数,该函数以 AST 抽象语法树为参数,返回生成的代码字符串。
- 另一个是作为代码生成器主线的 genElement 函数,该函数通过判断当前处理的元素 AST 节点的属性执行对应的函数。
2-3,src/compiler/codegen/index.js ==> generate()
// 代码生成器。AST ==> render 函数代码字符串
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// 对 ast 进行判断。
// 如果其存在的话,就执行 genElement(ast, state) 生成代码字符串,
// 否则的话,直接返回 '_c("div")',创建空的 div。
const code = ast ? genElement(ast, state) : '_c("div")'
return {
// 将生成的代码字符串(code)拼接到 `with(this){return ${code}}`,形成最终的代码字符串
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
generate 函数的内部首先判断 ast 是否存在,如果存在的话,调用 genElement 生成对应的代码字符串,如果不存在的话,则直接返回 _c("div")。
注意,这里 genElement 生成的代码字符串还不是完整的 render 函数的代码字符串,genElement 函数生成的 code 还需要拼接到 `with(this){return ${code}}` 中才是完整的 render 函数代码字符串。
接下来看看 genElement 函数的具体内容。
2-4,src/compiler/codegen/index.js ==> genElement()
genElement 函数根据 AST 节点的属性,调用对应的函数生成对应的代码字符串。
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) {
// 用于处理 v-if
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
}
}
接下来看看,例子中能够调用到的 genData()、genChildren()、genIf()、genFor() 函数。
2-5,src/compiler/codegen/index.js ==> genData()
genData() 函数的简要代码如下所示:
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
......
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// attributes
if (el.attrs) {
data += `attrs:{${genProps(el.attrs)}},`
}
// DOM props
if (el.props) {
data += `domProps:{${genProps(el.props)}},`
}
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false, state.warn)},`
}
// component v-model
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
......
return data
}
genData() 函数的作用是:将 AST 节点中保存的开始标签的相关属性转换成对象字符串的形式。例如我们有如下的模板字符串。
<div class="container" key="containerKey" ref="containerRef"></div>
那么上面这个 div 开始标签中的属性将会转换成如下的对象字符串。
"{key:"containerKey",ref:"containerRef",staticClass:"container"}"
genData() 的工作原理也很简单,就是判断 AST 节点中有没有对应的属性,如果有的话,就拼接对应的 key、value 键值对字符串到变量 data 的后面。
2-6,src/compiler/codegen/index.js ==> genChildren()
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
if (children.length) {
......
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
}
function genNode (node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
} if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
genChildren() 的工作流程是遍历 children 数组,对每一个子元素都执行 genNode 方法生成该子元素对应的代码字符串,children 数组通过 map() 和 genNode() 方法生成了对应的代码字符串的数组,然后拼接成一个字符串 return 出去。
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
genNode() 方法的内部根据 AST 节点的类型调用对应的函数生成对应的代码字符串,AST 节点有三种类型:
- type1:元素节点
- type2:含有表达式的文本节点
- type3:纯文本节点
根据不同的类型分别调用 genElement()、genComment()、genText()。
genComment() 和 genText() 函数的源码如下所示:
export function genComment (comment: ASTText): string {
return `_e(${JSON.stringify(comment.text)})`
}
export function genText (text: ASTText | ASTExpression): string {
return `_v(${text.type === 2
? text.expression
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}
2-7,src/compiler/codegen/index.js ==> genIf()
export function genIf (
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
// ifProcessed 属性用于判断 el 的 v-if 有没有被处理过,避免重复处理。
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions (
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
if (!conditions.length) {
return altEmpty || '_e()'
}
const condition = conditions.shift()
if (condition.exp) {
return `(${condition.exp})?${
genTernaryExp(condition.block)
}:${
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}`
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
genIf() 函数首先将 el 的 ifProcessed 设置为 true,el 的这个属性用于判断 el 的 v-if 有没有被处理过,避免重复处理。将 ifProcessed 设置为 true 后,执行 genIfConditions 进行真正的 v-if 处理。
genIfConditions() 函数的处理逻辑是生成三元表达式的代码字符串,例如:我们上面例子中的 v-if。
<h1 v-if="isShow">我是标题</h1>
这个模板字符串会生成如下的代码字符串。
(isShow)?_c('h1',[_v(_s(title))]):_e()
?前面的内容使用 condition.exp,condition.exp 就是模板字符串中的 'isShow'。
:前面的内容是指 'isShow' 为 true 的话,应该显示的内容,从模板字符串中可知,如果 'isShow' 为 true 的话,应该渲染 <h1>我是标题</h1> 元素节点,所以 :前面的内容应该是能够生成 <h1>我是标题</h1> 元素节点的 vnode 的 render 代码字符串。在 genIfConditions() 方法中使用 genTernaryExp(condition.block) 生成。
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
在我们的例子中,genTernaryExp() 内部会通过调用 genElement(el, state) 生成
_c('h1',[_v(_s(title))])
:后面的内容是指 'isShow' 为 false 的话页面显示的内容,在我们的例子中,如果 'isShow' 为 false 的话,不显示任何内容,所以生成的代码字符串中 :后面的内容是 _e(),_e() 函数的意思是创建空的 vnode 节点。
target._e = createEmptyVNode
2-8,src/compiler/codegen/index.js ==> genFor()
export function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
在我们的例子中,有如下的 v-for 模板字符串:
<ul>
<li v-for="item in names">{{item}}</li>
</ul>
对应生成的代码字符串如下所示:
_c('ul', _l((names), function (item) {
return _c('li', [_v(_s(item))])
}))
可以发现生成的代码字符串的层级关系和模板字符串中 ul 和 li 的层级关系不一样,在模板字符串中,ul 标签的子级就是 li,但由于 li 使用了 v-for,所以生成的代码字符串中,_c('ul', ) 的第二个参数并不是 _c('li', ),而是 _l((names), function (item) {}),_c('li', ) 会从匿名函数中 return 出来。这是使用了 v-for 模板字符串生成的代码字符串的一个特点。
接下来说说 genFor() 函数的内部的处理逻辑,genFor() 函数很简单,首先从 AST 节点对象中取出已经被解析好的 v-for 的相关属性(exp、alias、iterator1、iterator2),然后使用 genElement(el, state) 生成 '<li>{{item}}</li>' 对应的代码字符串 _c('li', [_v(_s(item))])。最后,把属性和生成的代码字符串拼接成一个字符串,再 return 出去即可。
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
3,总结
代码生成器的流程就是根据 AST 节点对象的属性判断该 AST 节点的类型,然后调用对应的函数生成代码字符串,逐步拼接成完整 render 函数代码字符串的过程。
代码生成器的内容挺多的,但是代码的逻辑却很清晰,因为 Vue 将生成不同特性代码字符串的工作都细致的拆分到了对应的函数中,每个函数都针对处理对应的特性,所以源码理解起来并不算太晦涩。
在这里强烈建议读者先写一个比较简单的模板字符串,例如我们上面作为例子的模板字符串,然后利用浏览器的 debugger 调试代码走一遍,这样的话,就可以对代码生成器的总体流程有了直观的了解,如果想了解某个特性代码生成器是如何处理的话,就向模板字符串中写入这个特性(例如上面例子中的 v-if、v-for),然后再调试走一遍。通过这种方式,我们就可以简单轻快的理解代码生成器的源码。
注意,千万不要追求一次就理解代码生成器的全部内容,每次理解自己关注的特性即可。