我说过html
字符串编译为render
函数,需要经过三个过程,本文将的是第二步——优化静态内容。顾名思义,Vue
中对于生成的ast
会做优化,静态内容是指和数据没有关系,不需要每次都刷新的内容,这一步主要就是找出ast
中的静态内容,并加以标注。
对应的源码中的文件是src/compiler/optimizer.js
,打开文件,顿时就会舒一口气,因为这里所做的处理很简单,代码只有100多行,分分钟看懂它干了什么。
同样我们通过一个示例来讲解。原本想用上一篇文章中的例子,结果发现静态内容太少,这里再来一个。
<div id="app">
这里是文本<箭头之后的文本
<p>{{message}}</p>
<p>静态文本<a href="https://www.imliutao.com">博客地址</a></p>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
message: '动态文本'
}
})
</script>
以上的template
内容,经过parse
生成的ast
如下,具体过程就不再多说了:
element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本'
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
parent: ,
children: [{
type: 2,
expression: '_s(message)',
text: '{{message}}'
}],
plain: true
},
{
text: " ",
type: 3
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
children: [{
text: "静态文本",
type: 3
},
{
attrs: [{name: "href", value: '"http://www.imliutao.com"'}],
attrsList: [{name: "href", value: 'http://www.imliutao.com'}],
attrsMap: {href: 'http://www.imliutao.com'}
children: [{
text: "博客地址",
type: 3
}]
plain: false,
tag: "a",
type: 1
}
],
plain: true
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}]
}
这里略去了parent
属性,大家都懂。
以上的ast
会传入optimize
函数,我们接着一起来看:
const genStaticKeysCached = cached(genStaticKeys)
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.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
首先定义了两个函数,一个是判断传入的key
是不是静态的,另一个是判断是不是平台保留tag
。
isStaticKey
我们在compile概述中介绍过,传入的options.staticKeys
的值为staticClass,staticStyle
。所以该函数返回true
的有下面genStaticKeys
中定义的属性加上staticClass,staticStyle
。
function genStaticKeys (keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
(keys ? ',' + keys : '')
)
}
isPlatformReservedTag
这里所有的HTML
和SVG
标签都会返回true
,具体定义在src/platforms/web/util/element.js
中。
正如代码中所注释的,标记共分为两步:
1、标记所有的静态和非静态结点
2、标记静态根节点
对应的方法就是markStatic
,它接收一个ast
作为参数。
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
}
}
这里我们通过isStatic
方法来判断结点是不是静态,具体判断的内容我们稍后说,接着往下看,如果node.type === 1
即ast
是元素结点,会添加一些其他的操作。
!isPlatformReservedTag(node.tag)
是指node.tag
不是保留标签,即我们自定义的标签时返回true
。
node.tag !== 'slot'
是指标签不是slot
。
node.attrsMap['inline-template'] == null
是指node
不是一个内联模板容器。
如果以上三个条件都符合的话,就不对它的children
进行标记,实际上这个时候node.static = false
,因为isStatic
中判断了如果isPlatformReservedTag(node.tag) == false
,函数返回的就是false
。
如果以上三个条件有一个不符合,则递归标记子节点,且如果子节点有不是静态的,当前结点node.static = false
。
我们再来看isStatic
的判断逻辑:
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)
))
}
node.type === 2
或node.type === 3
没什么好说的,本来就一个是表达式、一个是静态文本。
最后是两个**"或"**的判断逻辑。
1、 如果node.pre
返回true
,即元素上有v-pre
指令,这时结点的子内容是不做编译的,所以函数返回true
。
2、 第二个判断比较复杂,我们一个一个说。
!node.hasBindings
: 结点没有动态属性,即没有任何指令、数据绑定、事件绑定等。
!node.if
:没有v-if
和v-else
。
!node.for
:没有v-for
。
!isBuiltInTag(node.tag)
:不是内置的标签,内置的标签有slot
和component
。
isPlatformReservedTag(node.tag)
:是平台保留标签,即HTML
或SVG
标签。
!isDirectChildOfTemplateFor(node)
:不是template
标签的直接子元素且没有包含在for
循环中,代码如下:
function isDirectChildOfTemplateFor (node: ASTElement): boolean {
while (node.parent) {
node = node.parent
if (node.tag !== 'template') {
return false
}
if (node.for) {
return true
}
}
return false
}
Object.keys(node).every(isStaticKey)
:结点包含的属性只能有isStaticKey
中指定的几个。
所以经过第一步的标记之后,我们的ast
变为:
element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本',
static: true
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
parent: ,
children: [{
type: 2,
expression: '_s(message)',
text: '{{message}}',
static: false
}],
plain: true,
static: false
},
{
text: " ",
type: 3,
static: true
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
children: [{
text: "静态文本",
type: 3,
static: true
},
{
attrs: [{name: "href", value: '"http://www.imliutao.com"'}],
attrsList: [{name: "href", value: 'http://www.imliutao.com'}],
attrsMap: {href: 'http://www.imliutao.com'}
children: [{
text: "博客地址",
type: 3,
static: true
}],
plain: false,
tag: "a",
type: 1,
static: true
}
],
plain: true,
static: true
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}],
static: false
}
执行这一步的函数是markStaticRoots(root, false)
,第一个参数是ast
,第二个是标示ast
是否在for
循环中。
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
// 如果node.static为true,则会添加node.staticInFor
if (node.static || node.once) {
node.staticInFor = isInFor
}
// 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.
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)
}
}
if (node.ifConditions) {
walkThroughConditionsBlocks(node.ifConditions, isInFor)
}
}
}
这里我们只处理node.type === 1
的结点。
最开始会给node.static = true
或node.once = true
的结点添加node.staticInFor
属性,值为传入的isInFor
。
下面的几句注释比较重要,大体意思是“对于一个静态根结点,它不应该只包含静态文本,否则消耗会超过获得的收益,更好的做法让它每次渲染时都刷新。”
所以就有了下面判断node.staticRoot = true
的条件:node.static
说明该结点及其子节点都是静态的,node.children.length
说明该结点有子节点,!(node.children.length === 1 && node.children[0].type === 3)
说明该结点不是只有一个静态文本子节点,这与上面的注释正好对应。
如果不满足这三个条件,则node.staticRoot = false
。
之后再以同样的方式递归地对子节点进行标记。
最后如果结点有if
块,则对块儿内结点同样进行标记。
所以,经过上面两步的处理,最终的ast
变为:
element1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
parent: undefined,
children: [{
type: 3,
text: '这里是文本<箭头之后的文本',
static: true
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
parent: ,
children: [{
type: 2,
expression: '_s(message)',
text: '{{message}}',
static: false
}],
plain: true,
static: false,
staticRoot: false
},
{
text: " ",
type: 3,
static: true
},
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
children: [{
text: "静态文本",
type: 3,
static: true
},
{
attrs: [{name: "href", value: '"http://www.imliutao.com"'}],
attrsList: [{name: "href", value: 'http://www.imliutao.com'}],
attrsMap: {href: 'http://www.imliutao.com'}
children: [{
text: "博客地址",
type: 3,
static: true
}],
plain: false,
tag: "a",
type: 1,
static: true
}
],
plain: true,
static: true,
staticInFor: false,
staticRoot: true
}
],
plain: false,
attrs: [{name: "id", value: "'app'"}],
static: false,
staticRoot: false
}