前言
编译器中的优化,其实就是做静态标记:
- 通过遍历 AST 对象,为每个节点做 静态标记,通过标记其是否为静态节点,然后进一步标记出 静态根节点,方便在后续更新过程中跳过这些静态节点
- 标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
深入源码
createCompiler() 方法 —— 入口
文件位置:/src/compiler/index.js
其中主要是通过 optimize()
方法做静态标记,从而实现优化的.
/*
在这之前做的所有的事情,只是为了构建平台特有的编译选项(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 对象上都设置了元素的所有信息,如,标签信息、属性信息、插槽信息、父节点、子节点等
*/
const ast = parse(template.trim(), options)
/*
优化,遍历 AST,为每个节点做静态标记
- 标记每个节点是否为静态节点,,保证在后续更新中跳过这些静态节点
- 标记出静态根节点,用于生成渲染函数阶段,生成静态根节点的渲染函数
*/
if (options.optimize !== false) {
optimize(ast, options)
}
/*
从 AST 语法树生成渲染函数
如:code.render = "_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return _c('div',{key:item},[_v(_s(item))])}),0)"
*/
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
复制代码
optimize() 方法
文件位置:/src/compiler/optimizer.js
其中最主要的就是 markStatic(root)
和 markStaticRoots(root, false)
方法.
/*
优化的目标:遍历生成的模板 AST 树并检测纯静态的子树,即永远不需要改变的 DOM
一旦检测到这些子树就可以:
1. 将它们提升为常数,这样就不再需要在每次重新渲染时为其创建新节点
2. 在修补过程中完全跳过它们
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 是否平台保留标签
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
// 遍历所有节点,给每个节点设置 static 属性,标识其是否为静态节点
markStatic(root)
// second pass: mark static roots.
/*
进一步标记静态根,一个节点要成为静态根节点,需要具体以下条件:
- 节点本身是静态节点,而且有子节点,而且子节点不只是一个文本节点,则标记为静态根
- 静态根节点不能只有静态文本的子节点,因为这样收益太低,这种情况下始终更新它就好了
*/
markStaticRoots(root, false)
}
复制代码
markStatic() 方法
文件位置:/src/compiler/optimizer.js
这里主要通过 isStatic(node)
方法来判断当前节点是否属于静态节点,针对子节点则通过递归调用 markStatic()
进行标记
/*
在所有节点上设置 static 属性,用来标识是否为静态节点
注意:如果有子节点为动态节点,则父节点也被认为是动态节点
*/
function markStatic (node: ASTNode) {
// 通过 node.static 来标识节点是否为 静态节点
node.static = isStatic(node)
if (node.type === 1) {
/*
不将组件插槽内容设置为静态节点,这是为了避免:
1. 组件无法改变插槽节点
2. 静态插槽内容无法进行热重载
*/
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
// 递归终止条件:节点非平台保留标签 && 非 slot 标签 && 非内联模版,则直接结束
return
}
// 遍历子节点,递归调用 markStatic 来标记这些子节点的 static 属性
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
// 如果子节点是非静态节点,则将父节点更新为非静态节点
if (!child.static) {
node.static = false
}
}
// 如果节点存在 v-if、v-else-if、v-else 这些指令,则依次标记 block 中节点的 static
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
复制代码
isStatic() 方法
文件位置:/src/compiler/optimizer.js
/*
判断节点是否为静态节点
- 动态节点:
1. 包含表达式 {{ msg }},即 node.type === 2
2. 包含 v-bind、v-if、v-for 等指令的都属于动态节点
3. 组件、slot 插槽都为动态节点
4. 父节点为含有 v-for 指令的 template 标签
- 静态节点:除了动态节点的情况之外就属于静态节点,如文本节点,即 node.type === 3
*/
function isStatic (node: ASTNode): boolean {
// node.type === 2 为表达式,如:{{ msg }} ,返回 false
if (node.type === 2) {
return false
}
// node.type === 3 为文本节点,返回 true 标记为静态节点
if (node.type === 3) {
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
复制代码
markStaticRoots() 方法
文件位置:/src/compiler/optimizer.js
/*
进一步标记静态根,一个节点要成为静态根节点,需要具体以下条件:
- 节点本身是静态节点,且有子节点,而且子节点不只是一个文本节点,则标记为静态根
- 静态根节点不能只有静态文本的子节点,因为这样收益太低,这种情况下始终更新它就好了
@param { ASTElement } node 当前节点
@param { boolean } isInFor 当前节点是否被包裹在 v-for 指令所在的节点内
*/
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
// 节点是静态的 或 节点上有 v-once 指令,标记 node.staticInFor = true || false
node.staticInFor = isInFor
}
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
// 节点本身是静态节点,而且有子节点,而且子节点不只是一个文本节点,则标记为静态根
node.staticRoot = true
return
} else {
// 否则为非静态根
node.staticRoot = false
}
// 当前节点不是静态根节点的时候,递归遍历其子节点,标记静态根
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
// 如果节点存在 v-if、v-else-if、v-else 指令,则为 block 节点标记静态根
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
复制代码
总结
静态标记的过程是什么?
- 标记静态节点
- 通过递归的方式标记所有的元素节点
- 如果节点本身是 静态节点,但是存在 非静态的子节点,则将节点修改为 非静态节点
- 标记静态根节点,基于静态节点进一步标记静态根节点
- 如果节点本身是
静态节点
&&
有子节点
&&
子节点不全是文本节点
,则标记为静态根节点
- 如果节点本身
不是静态根节点
,则递归遍历所有子节点
,在子节点中标记静态根
- 如果节点本身是
什么样的节点才可以被标记为静态节点?
-
动态节点:
- 包含表达式
{{ msg }}
,即node.type === 2
则为动态节点 - 包含
v-bind、v-if、v-for
等指令的都属于动态节点 - 组件、
slot
插槽都为动态节点 - 父节点为含有
v-for
指令的template
标签
- 包含表达式
-
静态节点:
- 除了动态节点的情况之外就属于静态节点,如 文本节点 (
node.type === 3
)
- 除了动态节点的情况之外就属于静态节点,如 文本节点 (