「这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
前两篇小作文在说 parse 生成 ast 后的下一个阶段 generate,这个阶段会将 ast 变成渲染函数代码字符串。这个过程的核心方法是 generate() 方法,而 generate 方法的核心又是 genElment 方法,genElement 方法的第一个步骤就是处理静态根节点即 genStatic 方法,提升静态渲染函数到 staticRenderFns 数组中;
genStatic() 方法执行时会标记 el.satcicProcess 为 true,然后递归调用 genElement() 方法执行生成render函数的逻辑,生成 render 函数又需要先得到 ast 上携带的属性信息,这就要调用 genData 方法处理。
genData 是个中间步骤,genData 又会调用 genDirectives 来处理 el.directives,其中又会对 v-model 进行运行时辅助程序即运行时的事件处理函数的绑定。
由于调用栈过深,再进行后面的步骤之前,我们先来回顾一下调用栈:
generate -> genElement -> if(el.staticRoot && !el.saticProcessed) genStatic -> genElement else { data = genData() }
现在讲静态根的处理用的模板例子如下:
<div id="app">
+ <div class="staticR">
+ <article>hahahah</article>
+ </div>
<span v-for="item in someArr" :key="index">{{item}}</span>
<input :type="inputType" v-model="inputValue" />
{{ msg }}
<some-com :some-key="forProp"></some-com>
<div>someComputed = {{someComputed}}</div>
<div class="static-div">静态节点</div>
</div>
本篇我们会接着讲生成 genElement 在 genData() 之后的步骤:
export function genElement (el: ASTElement, state: CodegenState): string {
if (...) {}
else if (...) {
} else {
let code
if (...) {
//
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
// 上文所说的 genData
data = genData(el, state)
}
// 这后面就是得到 data 以后的操作了
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
return code
}
}
二、genChildren
方法位置:src/compiler/codegen/index.js -> functions genChildren
方法参数:
el,父元素的ast节点state,CodegenState对象checkSkip/altGenElement/altGenNode暂时忽略
方法作用:接收父元素,处理父元素的子节点数组 el.children,生成 el.children 中所有节点的渲染函数代码,形如:[_c(tag, data, children, normalizationType), _c(tag2, data2, children2...)....]。具体逻辑如下:
- 优化
el.children只有一项且带有v-for并且不是slot/template标签的情况,不用再走后门的 map 直接走 genElement; - 调用
el.children.map将每一项都变成render函数,并且拼接成一个数组格式的字符串;
export function genChildren (
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
// 获取 el 的所有子节点
const children = el.children
if (children.length) {
const el: any = children[0]
// optimize single v-for
if (children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
// 优化,只有一个子节点 && 子节点上有 v-for 指令 && 子节点不是 template / slot 标签
// 直接调用 genElement 生成该节点的渲染函数,不走下面的 map 再 genCode 的方式
const normalizationType = checkSkip
? state.maybeComponent(el) ? `,1` : `,0`
: ``
return `${(altGenElement || genElement)(el, state)}${normalizationType}`
}
// 获取节点规范化类型,忽略它吧
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
// 函数,生成代码的一个函数
const gen = altGenNode || genNode
// 返回一个数组,数组的每一项都是一个渲染函数代码字符串
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
}
2.1 genNode
方法位置:src/compiler/codegen/index.js -> function genNode
方法参数:
node:ast节点对象state,CodegenState实例
方法作用:根据节点类型不同调用不同方法生成渲染函数代码;如果节点类型为 1 说明是元素节点,直接递归 genElement() 方法;
结合上面的 genChildren() 方法可以看出,如果一个子节点是元素,则直接递归重新调用 genElement,如果子节点还有子节点,则一直递归下去生成渲染函数;
function genNode (node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
举个例子来说,div.demo 如下:
<div class="demo">
<article>hahahah</article>
<article>hahahah</article>
</div>
genChildren 的返回的子元素的渲染函数代码大致如下:
"[
_c('article', '', _s('hahhahahahha')),
_c('article', '', _s('hahhahahahha'))
]"
三、 genStatic 返回结果
3.1 梳理调用流程
从 generate 开始的的调用过程:generate() -> genElment() -> genStatic() -> genElement -> genChildren;
现在我们现在要看 genStatic 的返回结果需要返回上一层调用栈—— genElement,调用代码位置如下面示例代码:
genElement调用genStatic
export function genElement (): string {
// genElement 调用 genStatic
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
}
}
genStatic调用genElement
function genStatic () {
// 这里这个 push 语句递归调用 genElement
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
// 这个才是 genStatic 的返回值,这个就是重点了
return `_m(${
state.staticRenderFns.length - 1
}${
el.staticInFor ? ',true' : ''
})`
}
genElement调用genChildren
export function genElement (el: ASTElement, state: CodegenState): string {
if (...) {
} else {
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : ''
}${
children ? `,${children}` : '' // children
})`
}
return code
}
}
3.2 逐层返回结果
- 在我们的例子中
div.staticR是个静态根,我们以此为例来看返回值:
<div class="staticR">
<article>hahahah</article>
</div>
上一步 3.1 我们得知了调用栈,现在倒着退回去就可以拿到 genStatic 的返回值:
hahahah的render函数为:
"[_v(\"hahahah\")]"
<article>hahahah</article>的render函数:
"[_c('article',[_v(\"hahahah\")])]"
<div class="staticR"><article>hahahah</article></div>的render函数体:
"_c('div', {staticClass:\"staticR\"}, [_c('article',[_v(\"hahahah\")])])"
这些 render 函数还不是真正的函数,因为尚在编译阶段这些还只是字符串,这些代码都是要在运行时调用的。但是可以看的出,_c(...) 是一个 _c 方法的调用,_c 也是一个运行时的渲染帮助函数,这些方法最终都会挂载到 Vue 实例上,通过 with(this) 让浏览器能够从 this(Vue) 实例上找到 _c 方法;
这里为啥没有 this ?好问题,如果你有这疑问,说明你已经读懂了;这是因为上面的返回值在 push 进 staticRenderFns 之前被拼接的:
function genStatic () {
// 这里这个 push 的 `with(this)` 就是了
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
genStatic()返回结果
genStatic 的返回值形如: "_m(index, true/'')";_m() 也是一个渲染帮助函数,同理上面的
_c 也是 Vue 提前准备好挂载到 Vue 实例 this 上的方法,说完这部分渲染我们会用专门的篇幅来讲解这些渲染帮助函数的作用;
_m 的第一个参数为啥是 index?因为在返回值之前,我们才 push 到 staticRenderFns 中,所以数组最后一项就是当前元素对应的索引了。
function genStatic () {
// 返回值形式: _m(index, true/'')
return `_m(${
state.staticRenderFns.length - 1
}${
el.staticInFor ? ',true' : ''
})`
}
四、总结
本篇小作文讨论了 genElement 的第一个情况——genStatic 获取渲染结果的过程,genStatic 主要做了以下工作:
- 标记当前
el.staticProcessed为true,防止被重复处理; - 递归调用
genElement,产生各个节点的渲染函数; - 当
genElement最后的else就是处理普通元素的渲染的。首先调用genData获取元素上的data, 然后调用genChildren()将子元素都处理成由render函数代码组成的数组项,每一项都渲染一个子元素; genStatic获取genElement返回的结果,包裹with(this)语句,然后push到staticRenderFns;genStatic的最后用将当前静态根节点处理成_m 方法的运行时调用,_m(当前元素在 staticRenderFns 的索引, true 或 '')