「这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上文详细讲解了对 parse 得到的 ast 进行静态标记的过程,这个过程的意义在于被标记成静态的 ast 节点,在数据发生更新是不会被重新渲染;其核心实现主要有在 optimize 方法中:
- 调用
genStaticKeysCached获取isStaticKeys方法备用; - 调用
markStatic方法递归处理ast节点及其子节点和条件渲染节点,为每个节点设置static属性,值为isStatic()方法返回值,isStatic方法则根据ast节点对象上的信息判断是否为静态; - 调用
markStaticRoot()判断节点是否为静态根,静态根节点在数据更新时会被忽略,也不会被patch
本篇小作文聚焦于:用前面的 ast 生成渲染函数代码主体的 generate 方法,如下图,code 是一个对象,render 就是经过 generate 方法转换 ast 得来的代码;
上图中的 render 就是所谓 render 函数主体,接下来的篇幅我们将详细讨论如何得到这个结果的:
// 这个 code 就是上面 generate 方法返回的结果
code = {
render: "with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_v(\"\\n\\t\"+_s(msg)+\"\\n\\t\"),_c('some-com',{attrs:{\"some-key\":forProp}}),_v(\" \"),_c('div',[_v(\"someComputed = \"+_s(someComputed))]),_v(\" \"),_c('div',{staticClass:\"static-div\"},[_v(\"静态节点\")])],1)}"
staticRenderFns: [],
<prototype>: {…}
}
二、generate
方法位置:src/compiler/codegen/index.js -> function generate
方法参数:
ast,预期转成render函数的ast节点对象;compilerOptions:编译器选项对象
方法作用:
- 通过
CodegenState类结合传入的options生成state对象; - 然后调用
genElement(ast, state)得到render函数主体; - 最后返回一个对象,对象包含
render和staticRenderFns属性,render就是经过with(this)语句包裹的render函数主体;
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 创建 CodeGenState 实例,
// CodegenState 初始化了如 staticRenderFns 等属性
const state = new CodegenState(options)
// 生成字符串格式的代码,比如 '_c(tag, data, children, normalizationType)'
// 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
}
}
2.1 CodegenState 类
类的位置:src/compiler/codegen/index.js -> class CodegenState
构造函数参数:options,就是 createCompiler(baseOptions) 方法接收到的 baseOptions
类的作用:
- 缓存
baseOptions到this.options; - 从
options.modeules中提取transformCode,genData方法,这两个方法和我们前面讲parse时用到的preTransformNode和postTransformNode以及transformNode同宗同源;options.modules包含三个模块:klass/style/model,其中klass/style导出了genData方法,所以this.genDataFns = [klass导出 genData 方法, style 导出的 genData 方法]; - 实现
Vue中的基础指令处理方法的复用和options.directives处理方法的扩展,最终所有指令将会扩展到一个新的对象,并挂载到this.directives- 3.1 处理基础指令
v-on、v-bind的方法 - 3.2
options.directives处理指令v-model、v-text、v-html的方法
- 3.1 处理基础指令
- 初始化判断
ast节点是否为组件的方法this.maybeComponent,其判断原理是el.component属性为true,或者不是平台保留标签; - 初始化
staticRenderFns、pre等属性
export class CodegenState {
options: CompilerOptions;
warn: Function;
transforms: Array<TransformFunction>;
dataGenFns: Array<DataGenFunction>;
directives: { [key: string]: DirectiveFunction };
maybeComponent: (el: ASTElement) => boolean;
onceId: number;
staticRenderFns: Array<string>;
pre: boolean;
constructor (options: CompilerOptions) {
this.options = options
this.warn = options.warn || baseWarn
// 从 options.modules 提取 transformCode 方法,不过 web 平台下 klass、style、model 没有导出这个方法
this.transforms = pluckModuleFunction(options.modules, 'transformCode')
// style/klass 导出了 genData 方法
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
this.directives = extend(extend({}, baseDirectives), options.directives)
const isReservedTag = options.isReservedTag || no
this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
this.onceId = 0
this.staticRenderFns = []
this.pre = false
}
}
2.2 genElement
方法位置:src/compiler/codegen/index.js -> function genElement
方法参数:
ast,准备生成渲染函数的ast节点对象;state,CodegenState实例
方法作用:根据不同情况调用不同处理函数,最后得到渲染工具函数调用的字符串实现对应的指令或者组件的功能,例如 v-for 会变成 _l() 方法调用,如题处理如下:
- 如果
el不是根节点,处理el.pre属性,判断el是否有v-pre至指令或者处于有v-pre指令的元素包裹; el.staticRoot属性为true调用genStatic()方法处理静态根节点,将静态根节点的渲染函数保存到staticRenderFns属性中;el.once属性为true时处理v-once指令,调用genOnce()方法处理v-once指令;el.for属性值存在说明ast上存在v-for指令,调用genFor()方法处理v-for指令;el.if属性值存在,调用genIf()处理v-if;- 如果当前节点标签是
template且不是slot或者v-pre,则调用genChildren处理子节点; - 如果是
slot插槽,则调用genSlot()处理slot插槽; - 如果是普通元素、组件、者动态组件 则调用
genComponent()处理直接得到code;否则看是不是组件(!el.plain)或者有没有v-pre并且是组件,则调用genData()处理节点上所有的属性,得到data对象;然后在处理其子节点; - 经历前面的操作后得到
code代码字符串,然后调用transforms即前面从options.modules提取出来挂载到CodegenState实例上的
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
// 处理静态根节点,生成静态根节点的渲染函数,结果保存到 staticRenderFns 数组中
// genStatic 返回一个类似 _m(idx, true) 的字符串
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
// 处理带有 v-once 指令的节点,结果会有这三种:
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
// 处理节点上的 v-for 指令,
// 得到 `_l(exp, function (alias, iterator1, iterator2) { return _c(tag, data, children) })`
// _l 是个工具方法,是渲染一个列表处理
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
// 处理带有 v-if 指令的节点,得到三元表达式:condition ? render1 : render2
// condition 就是条件,成立则返回渲染函数1,否则2
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
// 当前节点不是 template 标签 && 不是插槽 && 不带有 v-pre 指令
// 处理所有子节点的渲染函数,返回一个数组,每个数组就是一个子节点,
// 格式如:[_c(tag, data, children, normalizationType), ...]
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
// 生成插槽的渲染函数,_t 是处理 slot 的工具方法,结果形如
// _t(slotName, children, attrs, bind)
return genSlot(el, state)
} else {
// 处理动态组件、普通HTML元素(自定义组件,原生标签)
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
// 非普通元素或者带有 v-pre 指令的组件,
// 处理节点的所有属性,返回一个 JSON 字符串,形如:
// 比如:'{ key: xx, ref: xx, ....}'
data = genData(el, state)
}
// 处理子节点,得到所有的子节点字符串格式的代码组成的数组,形如:
// `['_c(tag, data, children)', ....], normalizationType`
const children = el.inlineTemplate ? null : genChildren(el, state, true)
// 得到最终的字符串格式代码,形如:
// '_c(tag, data, children, normalizationType)'
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// 调用通过 pluckModuleFunction 从 options.modules 提取的 transformCode 方法,处理 code
// options.mdoules 中的 klass/style/model 没有导出 transformCode 方法
// 所以这里这个循环不会执行,code 还是前面的 code
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
提示:上面代码中用到的
_c、_l、_t都是渲染帮助函数的别名,_c是创建元素,_l是渲染列表即v-for的实现,_t则是处理slot的。
四、总结
本篇小作文开始讲述挂载阶段的另一个十分重要的主题——生成渲染函数(render函数)代码主体。
前面的 parse 把 html 模板转成 ast,ast 包含了包裹指令例如v-if条件渲染、v-for 列表渲染等全部信息。接着 generate 就是借用渲染函数的帮助函数实现这些指令的过程。主要分为两个大的步骤:
- 实例化
CodegenState对象,准备一些属性和方法给后面的创建render函数主体备用; - 调用
genElement分情况处理ast语法,将ast变成对应的帮助函数调用;
说道这里,相信大家大家已经有点感觉了,generate 的作用就是把 Vue 的模板语法变成真正的 HTML 代码的中间步骤,这一步还不是 HTML 而是 js 代码,这些 js 代码执行后就会得到真正的 HTML;