「这是我参与2022首次更文挑战的第27天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文讲述挂载阶段的另一个十分重要的主题——生成渲染函数(render函数)代码主体的过程。
parse 把 html 模板转成 ast,ast 包含了包裹指令例如v-if条件渲染、v-for 列表渲染等全部信息。接着 generate 就是借用渲染函数的帮助函数实现这些指令并生成对应的 DOM 的过程。主要分为两个大的步骤:
- 实例化
CodegenState对象,准备一些属性和方法给后面的创建render函数主体备用; - 调用
genElement分情况处理ast语法,将ast变成对应的帮助函数调用;
说道这里,相信大家大家已经有点感觉了,generate 的作用就是把 Vue 的模板语法变成真正的 HTML 代码的中间步骤,这一步还不是 HTML 而是 js 代码,这些 js 代码执行后就会得到真正的 HTML;
本篇小作文将详述 genElment 方法处理具体情况时使用的具体方法,比如 genFor、genStatic 等,在这个过程中我们会一步一步看到渲染函数主体是如何产生的。
二、genStatic
方法位置:src/compiler/codegen/index.js -> function genStatic
方法参数:
el:ast节点对象state:CodegenState对象,这里用到了state.staticRenderFns用于提升静态子树
方法作用:
- 标记当前
ast节点对象el.staticProcessed = true,防止被重复处理,前面genElement调用genStatic前判断了el.staticProcessed不为rue才调用; - 将当前
l这个静态根生成的渲染函数保存到tate.staticRenderFns注意这里还会再次调用{genElement(el, state)} - 最后返回_m 方法的调用的字符串形式的代码,形如:
_m(静态根render函数索引, true 或者 ''),第一个参数是面生成的静态根的渲染函数代码在state.staticRenderFns的索引位置,第二个参数el.staticInFor标识结果;
function genStatic (el: ASTElement, state: CodegenState): string {
// 标记当前静态节点已经被处理过了
el.staticProcessed = true
const originalPreState = state.pre
if (el.pre) {
state.pre = el.pre
}
// 将静态根节点的渲染函数 push 到 staticRendersFns 数组中,比如:
// [`with(this){return _c('div',{staticClass:\"staticR\"},[_c('article',[_v(\"hahahah\")])]) }]
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
state.pre = originalPreState
// 返回一个可执行函数 _m(idx, true or '')
// idx = 当前静态节点的渲染函数在 staticRenderFns 数组中的下标
return `_m(${
state.staticRenderFns.length - 1
}${
el.staticInFor ? ',true' : ''
})`
}
我们在 test.html 中加入一段静态 HTML 模板内容如下:
<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>
显而易见的 div.staticR 就是个静态根,我们以此为例看下最后得到的 staticRenderFns 的真容。
在 generate() 方法中第一次调用 genStatic 的目的其实只是提升而已,把静态根节点 push 到staticRenderFns 中,在 genStatic 中再次调用 genElement 才是真正的生成静态的 render 函数的步骤,此时 el.staticProcessed 已经为 true,所以 genElement() 中的 if (el.staticRoot && !el.staticProcessed) { genStatic() } ,就不会再执行了,而是向下执行,
最终上图中的 code 就是 genStatic 调用 genElement 得到的 code:
"_c('div',{staticClass:\"staticR\"},[_c('article',[_v(\"hahahah\")])])"
所以,state.staticRenderFns.push("with(this){return ${genElement(el, state)}}") 最终得到的结果是:
state.staticRenderFns = [`with(this){return _c('div',{staticClass:\"staticR\"},[_c('article',[_v(\"hahahah\")])]) }`]
三、genData
方法位置:src/compiler/codegen/index.js -> function genData
方法参数:
el:ast节点对象state:CodegenState实例
方法作用:处理 ast 节点上的属性,最终拼接生成一个 JSON 字符串,这些信息描述了渲染生成的 DOM 的具体行为;这些属性在 HTML 模板中是写在标签行内的属性,这些属性的信息被解析处理后放在了 ast 节点上,例如写 HTML 标签上的 v-for,被处理后就是 el.for = 可迭代对象,el.alias = 可迭代对象条目名称。具体工作如下:
- 优先处理指令,因为指令运行时有可能会更改
el上的某些属性; - 收集
el上属性:key,ref,refInfor,pre,component放到data中; - 调用
state.genDataFns,genDataFns是baseOptions.modules即klass/style/model三个模块中导出的方法; - 处理
el.attrs,props,events,slot,scopedSlot - 处理
v-model,将v-model拆分成多个属性:modle,callback,expression; - 处理
inlineTemplate内联模板,dynamicAttrs、wrapData、warapListeners
export function genData (el: ASTElement, state: CodegenState): string {
// 节点属性组成的 JSON 字符串
let data = '{'
// 首先处理指令,指令可能在生成其他属性之前改变这些属性
// 执行处理指令的方法,比如 web 平台的 v-text、v-html、v-model 等,
// v-html 会向 el 上增加 el.innerHTML = _s(value, dir)
// 如果指令的完整能力有一部分是依赖运行时的,比如 v-model 指令,
// 则返回 directives: [{ name, rawName, value, arg, modifiers }, ....]
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// key data= { key: xx }
if (el.key) {
data += `key:${el.key},`
}
//
// ref, data= { ref: xx }
if (el.ref) {
data += `ref:${el.ref},`
}
// 带有 ref 属性的节点在带有 v-for 指令的节点内部,data = { refInFor: true }
if (el.refInFor) {
data += `refInFor:true,`
}
// pre, v-pre 指令,data = { pre: true }
// pre
if (el.pre) {
data += `pre:true,`
}
// 动态组件,data= { tag: 'component' }
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// 为节点执行模块(class,
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// 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)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
// component v-model
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
// inline-template
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}
data = data.replace(/,$/, '') + '}'
if (el.dynamicAttrs) {
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data)
}
return data
}
在我们的例子中只有一个静态的类名 staticR,这个静态类名会通过调用 state.dataGenFns 数组的 genData 方法处理,即class.js 模块中导出的处理类名的 genData 方法处理返回 "staticClass: 'staticR'",所以在我们的这个静态根 div.staticR 的 data 只有一个属性:{ staticClass: \"staticR\"};
3.1 class.js 中 的 genData
方法位置:src/platforms/web/compiler/modules/class.js -> function genData
方法参数:el,ast 节点对象
方法作用:提取 ast 节点对象上的动态和静态类名,这个方法会就是前面的 state.dataGenFns 数组中的一项。所谓静态类名就是实实在在的确定的类名,而动态类名则是通过 v-bind:class/:class 语法绑定的 Vue 实例上的变量,比如 props/data/computed 中的属性;
function genData (el: ASTElement): string {
let data = ''
if (el.staticClass) {
// 静态类名 <div class="staticR" ></div> 中 staticR 就是静态类名
data += `staticClass:${el.staticClass},`
}
if (el.classBinding) {
// 动态类名 <div :class="someDynamicClass"></div> 这里的 someDynamicClass 就是动态类名
data += `class:${el.classBinding},`
}
return data
}
export default {
staticKeys: ['staticClass'],
transformNode,
genData // state.dataGenFns 中的方法来自这里
}
3.2 genDirectives
方法位置:src/compiler/codegen/index.js -> function genDirectives
方法参数
el,ast节点对象;state:CodegenState实例对象
方法作用:调用 genDirectives() 方法进行指令的编译,这部分内容也很经典,这里详细介绍了 Vue 是如何处理各式各样的双向数据绑定的,我们在下一篇用专门的篇幅讨论它。
四、总结
本篇小作文在说 parse 生成 ast 后的下一个阶段 generate,这个阶段会将 ast 变成渲染函数代码字符串。这个过程的核心方法是 generate() 方法,而 generate 方法的核心又是 genElment 方法,genElement 方法的第一个步骤就是处理静态根节点即 genStatic 方法,提升静态渲染函数到 staticRenderFns 数组中;
genStatic() 方法执行时会标记 el.satcicProcess 为 true,然后递归调用 genElement() 方法执行生成render函数的逻辑,生成 render 函数又需要先得到 ast 上携带的属性信息,这就要调用 genData 方法处理。
genData 是个中间步骤,因为调用栈太深了,这里先简单回顾一下调用栈:
generate -> genElement -> if(el.staticRoot && !el.saticProcessed) genStatic -> genElement else { data = genData() }
下一篇我们会进一步讨论 genDirectives 的细节问题。