「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文主要完成了以下工作:
parseHTML的最后一个回调方法options.end的讨论;- 就
curerntParent在start和end中的更新作用进行了详细讨论; - 回顾了整个
parse方法的parseHTML的梗概方法及作用;
前面的部分已经介绍了 parse 方法获取 html 模板获取 ast 的过程,接下来的部分将继续后面的部分,本篇小作文聚焦于生成 ast 后的静态标记优化。
二、baseCompile 中的静态标记调用
在 parse 生成 ast 后就会调用 optimize 方法进行静态标记处理。
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
// 优化,为每个节点做静态标记
if (options.optimize !== false) {
optimize(ast, options)
}
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
三、optimize 方法
方法位置:src/compiler/optimizer.js -> function optimize
方法参数:
root,顶层的ast节点对象;options:编译器选项对象
方法作用:
- 生成
isStaticKey函数,isStaticKey函数时检测某些属性是否是静态属性的函数,options.staticKeys是'staicClass,staticStyle'这个字符串,这个字符串同样来自baseOptions,baseOptions来自createCompiler(baseOptions)传入的;- 1.1
isStatic方法是接收某个key,判断这个key是否是type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticStyle,staticClass中的一个,言外之意这些列出的都是ast静态属性;
- 1.1
- 遍历
root节点,给每个节点设置static属性,此属性标识当前节点是否为静态节点;
被标记成静态节点的节点后面数据更新时不再关注这些节点,patch 时也会忽略这些静态节点;
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 是否是保留平台标签
isPlatformReservedTag = options.isReservedTag || no
// 遍历节点,给每个节点设置 static 属性,标识其是否为静态节点
markStatic(root)
// 标记静态根
markStaticRoots(root, false)
}
3.1 genStaticKeysCached
方法位置:src/compiler/optimizer.js -> const genStaticKeysCached
方法参数:str
方法作用:返回一个函数,这个函数会优先从缓存中获取结果;
const genStaticKeysCached = cached(genStaticKeys)
3.1.1 cached
方法位置:src/shared/util.js -> function cached
方法参数:fn,目标函数
方法作用:创建缓存对象,返回一个新函数,这个新函数就优先取用缓存中的结果。当第一次执行这个新函数的时候,会把函数返回值放到缓存中
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null)
return (function cachedFn (str: string) {
const hit = cache[str] // hit 就是从缓存中取得的结果
// 如果 hit 有值说明命中缓存,否则就调用 fn 并缓存结果
// 在静态优化的时候,fn 就是下面 3.1.2 genStaticKeys
return hit || (cache[str] = fn(str))
}: any)
}
3.1.2 genStaticKeys
方法位置:src/compiler/optimizer.js -> genStaticKeys
方法参数:
keys: 由,分隔的key组成的字符串
方法作用:接收 keys 字符串,返回调用 makeMap 生成 map 后返回的验证 key 是否在 map 中的函数;
function genStaticKeys (keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
(keys ? ',' + keys : '')
)
}
3.1.2 makeMap
方法位置:src/shared/util.js -> functions makeMap
方法参数:
str,由,分隔的多个key组成的字符串expectLowerCase,是否小写
方法作用:把 str 用 , 拆分成数组,然后遍历数组生成一个 map,key 就是由 str 拆分成的数组,value 是 true,并且返回一个函数检验一个 key 是否在这个 map 中;
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void {
const map = Object.create(null)
const list: Array<string> = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
3.2 markStatic
方法位置:src/compiler/optimizer.js -> functions markStatic
方法参数:node,ast 节点对象
方法作用:
-
为
node设置static属性,值是isStatic(node)方法的返回值; -
在这个过程中会遍历
node的children,则递归处理children中的每一个child,如果child为非静态节点则node本身也不能算作静态节点; -
此外如果
node.ifConditions存在,则说明node有v-if/v-else-if/v-else指令,还要递归处理每个条件语句中的block,如果block不是静态元素,则node也不能算作静态元素,即node.stack = false -
递归终止的条件为:如果节点
不是平台保留标签&&不是 slot 标签&&不是内联模板;换言之,能够进行静态标记的都是啥呢?是平台保留标签或者slot 标签或者内联模板
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// 不要将组件的插槽内容设置为静态节点,这样可以避免:
// 1. 组件不能改变插槽节点
// 2. 静态插槽内容在热重载时失败
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
// 递归终止条件:
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
}
}
// 如果 node.ifConditons 存在说明 节点存在 v-if/v-else-if/v-else 指令
// 此时要递归处理指定条件下要渲染的元素是否静态,即 node.ifCondtions[].block
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
}
}
}
}
}
3.2.1 isStatic
方法位置:src/compiler/optimizer.js -> function isStatic
方法参数:node,ast 节点对象
方法作用:判断节点对象是否为静态,判断标准为:
node.type === 2时为表达式,是动态;多说一句,node.type为2的情况你还记得是在哪里创建的吗?没错,就是parse方法解析html模板时解析到文本时会调用options.chars方法,options.chars会判断文本中是否有{{}}这种语法,如果有,则创建node.type为2的ast节点;node.type === 3为文本,是静态- 综合判断条件,满足以下条件之一:
- 3.1 在
pre标签中,即被<pre></pre>包裹; - 3.2 下列条件都成立:
- 3.2.1
el.hasBindings不为true - 3.2.2
el.if不存在 - 3.2.3
node.for不存在 - 3.2.4 不是
slot或compnent这两个内建标签 - 3.2.5 带有
v-for的template的直接子级 - 3.2.6
node上的所有属性都是静态属性
- 3.2.1
- 3.1 在
那么何时为
true呢?
调用 prcessAttrs() 时如果发现元素有指令,所谓指令就是 Vue 的指令,包含简写例如 :/@,就会将 el.hansBindingds 置为 true;
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // 没有动态绑定
!node.if && !node.for && // 没有 v-for 、v-if/v-else-if/v-else
!isBuiltInTag(node.tag) && // 不是内建的 slot 或者 component 标签
isPlatformReservedTag(node.tag) && // 是平台保留标签,即不是一个自定义组件
!isDirectChildOfTemplateFor(node) && // 不是 带有 v-for 的 template 的直接子级
Object.keys(node).every(isStaticKey) // node 上的属性每个都是静态属性
))
}
3.3 markStaticRoots
方法位置:src/compiler/optimizer.js -> markStaticRoots
方法参数:
node,ast节点对象
方法作用:进一步标记静态根节点,一个节点要成为静态根节点要满足:
- 首先是元素节点,即
node.type === 1; - 元素必须是静态的,即
node.statick === type - 要有子元素,即
node.children.length - 元素不能只有一个文本节点
为啥有这么多要求?
For a node to qualify as a static root, it should have children that are not just static text. Otherwise the cost of hoisting out will outweigh the benefits and it's better off to just always render it fresh.
因为不这么做的话,提升这些节点位静态根代价比直接每次更新时直接渲染大的多
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
// 节点是静态的或者节点上有 v-once 指令,标记 node.staticInFor = true or 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-for/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)
}
}
}
}
四、总结
本文详细讲解了对 parse 得到的 ast 进行静态标记的过程,这个过程的意义在于被标记成静态的 ast 节点,在数据发生更新是不会被重新渲染;其核心实现主要有在 optimize 方法中:
- 调用
genStaticKeysCached获取isStaticKeys方法备用; - 调用
markStatic方法递归处理ast节点及其子节点和条件渲染节点,为每个节点设置static属性,值为isStatic()方法返回值,isStatic方法则根据ast节点对象上的信息判断是否为静态; - 调用
markStaticRoot()判断节点是否为静态根,静态根节点在数据更新时会被忽略,也不会被patch