Skip to content

Latest commit

 

History

History
409 lines (357 loc) · 11.5 KB

compile——优化静态内容.md

File metadata and controls

409 lines (357 loc) · 11.5 KB

我说过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 这里所有的HTMLSVG标签都会返回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 === 1ast是元素结点,会添加一些其他的操作。

!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 === 2node.type === 3没什么好说的,本来就一个是表达式、一个是静态文本。

最后是两个**"或"**的判断逻辑。

1、 如果node.pre返回true,即元素上有v-pre指令,这时结点的子内容是不做编译的,所以函数返回true

2、 第二个判断比较复杂,我们一个一个说。

!node.hasBindings: 结点没有动态属性,即没有任何指令、数据绑定、事件绑定等。

!node.if:没有v-ifv-else

!node.for:没有v-for

!isBuiltInTag(node.tag):不是内置的标签,内置的标签有slotcomponent

isPlatformReservedTag(node.tag):是平台保留标签,即HTMLSVG标签。

!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 = truenode.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
}