Vue
在 template -> AST -> parse -> optimize -> generate -> render -> vnode
这个template
转化为 vnode
流程中,在 optimize
阶段会做一些优化,就是检测不需要进行DOM改变的静态子树,并做相应处理。
静态节点的定义
那么,Vue
是怎么定义静态节点的呢?来看一下 Vue
源码中是怎么定义的:
// vue/compiler/optimizer.js
// node.type === 1 是标签元素
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
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)
))
}
根据注释,我们可以知道静态节点是:
- 节点类型是
text
; - 预格式化文本节点;
- 其他节点,符合以下特征:
- 没有动态绑定的
- 不包含
v-if
,v-for
,v-else
指令 - 不是构造中的节点(slot, component)
- 不是组件类型
- 静态节点的父节点,不是带有
v-for
指令的template
节点 - 节点的属性是静态属性
对于第二种类型的静态节点的特征中,第3,4,5,6点的详细解释可能需要结合源码看一下。下面来分析一下。
按顺序来,先看第3点:
isBuiltInTag
定义如下:
// /vue/shared/util.js
// 检查字段是否存在 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]
}
/**
* Check if a tag is a built-in tag.
*/
export const isBuiltInTag = makeMap('slot,component', true)
从代码中可以看到,buildIn
的节点,指的就是 slot
和 component
。
下面来看第4点:
isPlatformReservedTag
定义如下
// vue/compiler/optimizer.js
let isPlatformReservedTag
// ...
isPlatformReservedTag = options.isReservedTag || no
从代码中,可以知道,该字段是标记节点标签是否是平台保留的标签,划重点,平台 。也就是说,面对不同平台,该字段执行的方法,匹配的结果也各不相同。
这是 web 平台
的 isReservedTag
方法的定义:
// vue/platforms/web/util/element.js
export const isReservedTag = (tag: string): ?boolean => {
return isHTMLTag(tag) || isSVG(tag)
}
从代码中可以知道,isPlatformReservedTag
就是用于检测该节点类型是否是该平台的节点类型。
下面来看第5点:
isDirectChildOfTemplateFor
定义如下:
// vue/compiler/optimizer.js
function isDirectChildOfTemplateFor (node: ASTElement): boolean {
while (node.parent) {
node = node.parent
if (node.tag !== 'template') {
return false
}
if (node.for) {
return true
}
}
return false
}
明显地从代码中看到,isDirectChildOfTemplateFor
就是检测是否是下面这种情况的:
<!-- 忽略其他,只展示关键代码 -->
<template v-for="item in [1,2,3]">
{{ item }}
</template>
!isDirectChildOfTemplateFor(node) 说明了静态节点的父节点,不可以是带有 v-for
指令的 template
节点。
下面来看第6点:
首先看一下 isStaticKey
,它是定义在最外面的。
let isStaticKey
使用到 isStaticKey
的地方是
// vue/compiler/optimizer.js
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// ... 忽略其他代码
}
顺藤摸瓜,我们看下 genStaticKeysCached
,它也是定义到最外面的。
// vue/compiler/optimizer.js
const genStaticKeysCached = cached(genStaticKeys)
看下 genStaticKeys
和 cached
。
// vue/compiler/optimizer.js
// genStaticKeys 静态属性 map 表
// makeMap 方法请看第 3 点分析贴的代码
function genStaticKeys (keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
(keys ? ',' + keys : '')
)
}
// vue/shared/util.js
// cached 缓存方法执行结果
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null)
return (function cachedFn (str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
由此,可以得知,Object.keys(node).every(isStaticKey)
的意义在于检测节点属性是否是静态属性。
优化
// vue/compiler/optimizer.js
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
为什么 React 没有做静态节点的优化
思考这个问题之前,先思考另一个问题:为什么 Vue 可以做到静态节点的优化?
我们都知道,Vue
跟 React
最显著的区别就是:Vue
是模版化开发,React
是使用 jsx
来编写的。
Vue 也是可以使用
jsx
来开发的,如官方文档中 cn.vuejs.org/v2/guide/re… 所示:Vue.component('anchored-heading', { render: function (createElement) { return createElement( 'h' + this.level, // 标签名称 this.$slots.default // 子节点数组 ) }, props: { level: { type: Number, required: true } } })
如上所示,
render
方法中调用了createElement
方法来替代template
的写法。模版转化为
vnode
的流程如下:template -> AST -> parse -> optimize -> generate -> render -> vnode
而
jsx
的写法,得到vnode
的流程如下:render -> vnode
前面提到的静态节点的优化,如果使用
jsx
来写的话,就享受不了了呢。
所以 template
的存在,对于静态节点的识别和优化,有天然的优势。也不能肯定地说 jsx
做不到这种优化,只是,这是一种可选择优化方向,需要衡量利好和代价。
React
优化方向跟 Vue
不同,它将目光投在如何更快地响应浏览器上,创造性地使用时间切片,分优先级等,解决大量元素渲染或元素层级深嵌套渲染时导致页面卡顿或卡死的问题。
那么 Vue
要不要也用上时间切片,分优先级处理渲染的这些优化方案呢?这就需要考虑利好和代价之间的权衡了。