https://edu.csdn.net/course/detail/36074
https://edu.csdn.net/course/detail/35475
这篇文章是 Vue 编译器的最后一部分,前两部分分别是:Vue 源码解读(8)—— 编译器 之 解析、Vue 源码解读(9)—— 编译器 之 优化。
从 HTML 模版字符串开始,解析所有标签以及标签上的各个属性,得到 AST 语法树,然后基于 AST 语法树进行静态标记,首先标记每个节点是否为静态静态,然后进一步标记出静态根节点。这样在后续的更新中就可以跳过这些静态根节点的更新,从而提高性能。
这最后一部分讲的是如何从 AST 生成渲染函数。
深入理解渲染函数的生成过程,理解编译器是如何将 AST 变成运行时的代码,也就是我们写的类 html 模版最终变成了什么?
/src/compiler/index.js
/**
* 在这之前做的所有的事情,只有一个目的,就是为了构建平台特有的编译选项(options),比如 web 平台
*
* 1、将 html 模版解析成 ast
* 2、对 ast 树进行静态标记
* 3、将 ast 生成渲染函数
* 静态渲染函数放到 code.staticRenderFns 数组中
* code.render 为动态渲染函数
* 在将来渲染时执行渲染函数得到 vnode
*/
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 将模版解析为 AST,每个节点的 ast 对象上都设置了元素的所有信息,比如,标签信息、属性信息、插槽信息、父节点、子节点等。
// 具体有那些属性,查看 options.start 和 options.end 这两个处理开始和结束标签的方法
const ast = parse(template.trim(), options)
// 优化,遍历 AST,为每个节点做静态标记
// 标记每个节点是否为静态节点,然后进一步标记出静态根节点
// 这样在后续更新中就可以跳过这些静态节点了
// 标记静态根,用于生成渲染函数阶段,生成静态根节点的渲染函数
if (options.optimize !== false) {
optimize(ast, options)
}
// 代码生成,将 ast 转换成可执行的 render 函数的字符串形式
// code = {
// render: `with(this){return ${\_c(tag, data, children, normalizationType)}}`,
// staticRenderFns: [\_c(tag, data, children, normalizationType), ...]
// }
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
/src/compiler/codegen/index.js
/**
* 从 AST 生成渲染函数
* @returns {
* render: `with(this){return \_c(tag, data, children)}`,
* staticRenderFns: state.staticRenderFns
* }
*/
export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 实例化 CodegenState 对象,生成代码的时候需要用到其中的一些东西
const state = new CodegenState(options)
// 生成字符串格式的代码,比如:'\_c(tag, data, children, normalizationType)'
// data 为节点上的属性组成 JSON 字符串,比如 '{ key: xx, ref: xx, ... }'
// children 为所有子节点的字符串格式的代码组成的字符串数组,格式:
// `['\_c(tag, data, children)', ...],normalizationType`,
// 最后的 normalization 是 \_c 的第四个参数,
// 表示节点的规范化类型,不是重点,不需要关注
// 当然 code 并不一定就是 \_c,也有可能是其它的,比如整个组件都是静态的,则结果就为 \_m(0)
const code = ast ? genElement(ast, state) : '\_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
/src/compiler/codegen/index.js
阅读建议:
先读最后的 else 模块生成 code 的语句部分,即处理自定义组件和原生标签的 else 分支,理解最终生成的数据格式是什么样的;然后再回头阅读
genChildren
和genData
,先读genChildren
,代码量少,彻底理解最终生成的数据结构,最后再从上到下去阅读其它的分支。在阅读以下代码时,请把 Vue 源码解读(8)—— 编译器 之 解析(下) 最后得到的 AST 对象放旁边辅助阅读,因为生成渲染函数的过程就是在处理该对象上众多的属性的过程。
export function genElement(el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
/**
* 处理静态根节点,生成节点的渲染函数
* 1、将当前静态节点的渲染函数放到 staticRenderFns 数组中
* 2、返回一个可执行函数 \_m(idx, true or '')
*/
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
/**
* 处理带有 v-once 指令的节点,结果会有三种:
* 1、当前节点存在 v-if 指令,得到一个三元表达式,condition ? render1 : render2
* 2、当前节点是一个包含在 v-for 指令内部的静态节点,得到 `\_o(\_c(tag, data, children), number, key)`
* 3、当前节点就是一个单纯的 v-once 节点,得到 `\_m(idx, true of '')`
*/
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
/**
* 处理节点上的 v-for 指令
* 得到 `\_l(exp, function(alias, iterator1, iterator2){return \_c(tag, data, children)})`
*/
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
/**
* 处理带有 v-if 指令的节点,最终得到一个三元表达式:condition ? render1 : render2
*/
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(slotName, children, attrs, bind)
*/
return genSlot(el, state)
} else {
// component or element
// 处理动态组件和普通元素(自定义组件、原生标签)
let code
if (el.component) {
/**
* 处理动态组件,生成动态组件的渲染函数
* 得到 `\_c(compName, data, children)`
*/
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)
}
// 处理子节点,得到所有子节点字符串格式的代码组成的数组,格式ÿ