vue源码分析(十三) 编译之optimize

10/13/2018 vue源码分析面试

# 1. 概述

​ 我们在 vue源码分析(八) 编译之整体流程 (opens new window) 中分析过了编译的三个过程,即解析模板字符串生成 AST、优化语法树、生成代码,这一章我们主要来讲解第二部分——优化 AST 树。

​ 为什么要优化 AST 树,我们先看一下官方的解释,如下:

​ 源码目录:src/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.
 */
1
2
3
4
5
6
7
8
9
10
11

​ 我们把这段注释翻译成中文,如下:

优化器的目标 遍历生成的模板AST树,检测纯静态的子树,即永远不需要更改的DOM。 一旦我们检测到这些子树,我们可以: 1、把它们变成常数,这样我们就不需要了在每次重新渲染时为它们创建新的节点 2、在修补过程中完全跳过它们。

​ 从注释我们大致知道,optimize 的作用,我们知道 vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对,提高性能。

​ 所以 optimize 主要进行静态标注。下面我们从源码的角度分析,什么是静态标注?如何进行静态标注?

# 2. optimize

​ 首先我们看一下 optimize 定义,如下:

​ 源码目录:src/compiler/optimizer.js

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)
}
1
2
3
4
5
6
7
8
9

​ 在分析之前,我们还是以一个案例进行讲解,如下:

<ul :class="classObject" class="list" v-show="isShow">
  <li v-for="(l, i) in list" :key="i" ref="i" @click="clickItem(index)">{{ i }}:{{ l }}</li>
</ul>
1
2
3

​ 上面的模板,通过上一章的parse函数的解析生成的ast树为,如下:

{
  attrsList: [{name: "v-show", value: "isShow", start: 38, end: 53}],
  attrsMap: {:class: "classObject", class: "list", v-show: "isShow"},
  children: [
    {
      alias: "l",
      attrsList: [{name: "@click", value: "clickItem(index)", start: 111, end: 136}],
      attrsMap: {v-for: "(l, i) in list", :key: "i", ref: "i", @click: "clickItem(index)"},
      children: [{
        end: 152
        expression: "_s(i)+":"+_s(l)",
        start: 137,
        text: "{{ i }}:{{ l }}",
        tokens: (3) [ {@binding: "i"}, ":",  {@binding: "l"}],
        type: 2
      }],
      end: 157,
      events: {click: {value: "clickItem(index)", dynamic: false, start: 111, end: 136}},
      for: "list",
      forProcessed: true,
      hasBindings: true,
      iterator1: "i",
      key: "i",
      parent: {/*省略 ul*/},
      plain: false,
      pre: undefined,
      rawAttrsMap: {v-for: {}, :key: {}, ref: {}, @click: {}},
      ref: ""i"",
      refInFor: true,
      start: 67,
      tag: "li",
      type: 1
    }
  ],
  classBinding: "classObject",
  directives: [{name: "show", rawName: "v-show", value: "isShow", arg: null, isDynamicArg: false, modifiers: undefined, start: 38, end: 53}],
  end: 173,
  hasBindings: true,
  parent: undefined,
  plain: false,
  rawAttrsMap: {
    :class: {name: ":class", value: "classObject", start: 4, end: 24}
    class: {name: "class", value: "list", start: 25, end: 37}
    v-show: {name: "v-show", value: "isShow", start: 38, end: 53}
  },
  start: 0,
  staticClass: ""list"",
  tag: "ul",
  type: 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

​ 首先判断 rootAST 树存不存在,如果不存在直接返回,如果存在接下来调用 genStaticKeysCached 缓存了所有静态标签,赋值给 isStaticKey

​ 接下获取判断是否是保留的标签的函数,赋值给变量 isPlatformReservedTag

说明:关于 genStaticKeysCachedisReservedTag我们在后面小节中单独分析。

​ 我们继续往下看,接下来是调用 markStatic(root) 标记所有非静态节点和调用 markStaticRoots(root, false)标记静态 root 节点。

说明:关于markStaticmarkStaticRoots我们在后面小节中单独分析。

# 2.1 isReservedTag

isReservedTag 是从 options 配置中获取的,在 **vue源码分析(八) 编译之整体流程 (opens new window)**中我们分析过 baseOptions 的源码,知道 isReservedTag 的作用是检查给定的标签是否是保留的标签, 源码如下:

​ 源码目录:src/platforms/web/util/element.js

export const isReservedTag = (tag: string): ?boolean => {
  return isHTMLTag(tag) || isSVG(tag)
}
1
2
3

​ 其中 isHTMLTagisSVG 的定义如下:

​ 源码目录:src/platforms/web/util/element.js

// html 保留标签
export const isHTMLTag = makeMap(
  'html,body,base,head,link,meta,style,title,' +
  'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
  'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
  'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
  's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
  'embed,object,param,source,canvas,script,noscript,del,ins,' +
  'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
  'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
  'output,progress,select,textarea,' +
  'details,dialog,menu,menuitem,summary,' +
  'content,element,shadow,template,blockquote,iframe,tfoot'
)

// this map is intentionally selective, only covering SVG elements that may
// contain child elements.
// svg保留标签
export const isSVG = makeMap(
  'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +
  'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
  'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
  true
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

​ 这里只要知道 isHTMLTagisSVG 通过 makeMap 产生的, makeMap 的作用是根据 str, 生成一个map, 然后返回一个方法, 这个方法的作用是, 判断一个值是否在这个生成的 map 中。

# 2.2 makeMap

​ 上面分析我们知道了 makeMap的作用,接下来我们看一下 makeMap 的定义,如下:

​ 源码目录:src/shared/util.js

export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {
  // 创建一个空对象
  const map = Object.create(null)
  // 通过 `,` 将字符串分割成数组,例如:[type,tag,attrsList,attrsMap,plain]
  const list: Array<string> = str.split(',')
  // 通过for循环将数组中的每一项作为对象的健,`true`作为值,给对象添加相应的属性
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  // 返回一个检查传递进来的参数是否在此对象中的函数
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

​ 最终返回的值一个函数,如下:

function (val) { return map[val.toLowerCase()]; }
1

# 2.3 genStaticKeysCached

​ 我们来看看 genStaticKeysCached 的实现,源码如下:

​ 源码目录:src/compiler/optimizer.js

const genStaticKeysCached = cached(genStaticKeys)
1

genStaticKeysCached 通过我们前面分析过的 cached 函数来实现,参数是 genStaticKeys 函数,我们先看一下 genStaticKeys 的定义,再来回顾一下 cached 的实现,源码如下:

​ 源码目录:src/compiler/optimizer.js

function genStaticKeys (keys: string): Function {
  return makeMap(
    'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
    (keys ? ',' + keys : '')
  )
}
1
2
3
4
5
6

​ 前面我们已经分析过了 makeMap 的作用。在这里上面的字符串中 keysstaticClass,staticStyle,所以最后的字符串为 type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticClass,staticStyle,然后通过 makeMap 生成一个闭包,如:

function (val) { 
  var map = {
    type: ture,
    tag: ture,
    attrsList: ture,
    attrsMap: ture,
    plain: ture,
    parent: ture,
    children: ture,
    attrs: ture,
    start: ture,
    end: ture,
    rawAttrsMap: ture,
    staticClass: ture,
    staticStyle: ture
  }
  return map[val.toLowerCase()]; 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2.4 cached

​ 我们先来看一下 cached 的定义如下:

​ 源码目录:src/shared/util.js

export function cached<F: Function> (fn: F): F {
  // 创建一个空对象
  const cache = Object.create(null)
  return (function cachedFn (str: string) {  
    // 获取缓存对象str属性的值
    const hit = cache[str] 
    // 如果该值存在,直接返回,不存在调用一次fn,然后将结果存放到缓存对象中
    return hit || (cache[str] = fn(str)) 
  }: any)
}
1
2
3
4
5
6
7
8
9
10

cached 方法接受一个参数为函数,其会将该传入 fn 函数的运行结果缓存,返回一个函数 cachedFn,函数内部先获取调用该函数 cachedFn 传入的参数 str 在缓存对象 cache 中的的值,如果在缓存对象 cache 中有值,直接返回该值,否则调用 cached 函数中传入的方法 fn ,然后将运行结果存到 cache 中。这样如果 cachedFn 调用两次,第一次要执行一次 fn ,并将其运行结果缓存起来,当第二次调用 cachedFn 并且参数 str 与之前调用过的一致的时候,直接从缓存对象 cache 中获取结果,这样就不用再一次调用 fn 方法,节省一次函数的运行。

​ 通过上面的分析,我们知道在编译阶段可以把一些 AST节点优化成静态节点,所以整个 optimize 的过程实际上就干 2 件事情,markStatic(root) 标记静态节点 ,markStaticRoots(root, false) 标记静态根。

​ 接下来我们分析这两件事执行的代码逻辑。

# 3. markStatic

​ 我们来看一下markStatic的定义,如下:

​ 源码目录:src/compiler/optimizer.js

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
      }
    }
    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
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

​ 从上面的代码我们可以看出,markStatic 函数的主要作用是标注节点的状态和对标签节点进行处理,下面我们就详细分析一下这两个过程。

# 3.1 标注节点的状态

​ 首先看标记静态节点的 markStatic 这个方法,如下:

​ 源码目录:src/compiler/optimizer.js

node.static = isStatic(node)
1

markStatic 一开始通过调用 isStatic 进行标注节点的状态,下面我们来分析isStatic,源码如下:

​ 源码目录:src/compiler/optimizer.js

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)
  ))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

isStatic 函数主要的作用是:

  1. 判断节点类型为表达式,标注为非静态。
  2. 判断节点类型为普通文本,标注为静态。
  3. v-pre 指令(无需编译)标注为静态,或者满足以下条件,也标注为静态。
    • (1) 无动态绑定;
    • (2) 没有 v-ifv-for
    • (3) 不是内置的标签,内置的标签有slotcomponent
    • (4) 是平台保留标签(htmlsvg标签);
    • (5) 不是 template 标签的直接子元素并且没有包含在for循环中;
    • (6) 结点包含的属性只能有isStaticKey中指定的几个;

# 3.2 对标签节点进行处理

​ 分析完 isStatic 我们再回到 markStatic,继续往下看,接下的代码是对标签节点进行处理。

​ 首先判断节点类型为1即普通元素,则执行 if 语句中的代码。

说明:AST 元素节点总共有 3 种类型, type1 表示是普通元素,为2 表示是表达式,为 3 表示是纯文本

​ 接下来又是一个 if 语句,成立的条件是既不是平台保留标签(html,svg)不是slot标签不是一个内联模板容器,此段代码的作用是对 slot 内容不做递归标注,直接返回。

​ 继续往下看,紧接着是一个 for 循环子节点,进行递归标注,如果子节点为非静态,那么该节点也要标注非静态。所以整个标注过程是自下而上,先标注子节点,然后再是父节点,一层一层往上回溯。

​ 最后因为所有的 elseifelse 节点都不在 children 中, 如果节点的 ifConditions 不为空,则遍历 ifConditions 拿到所有条件中的 block,也就是它们对应的 AST 节点,递归执行 markStatic 。在这些递归过程中,一旦子节点有不是 static 的情况,则它的父节点的static 均变成 false

​ 通过调用 markStatic 函数处理过的 ast 树如下:

{
  attrsList: [{name: "v-show", value: "isShow", start: 38, end: 53}],
  attrsMap: {:class: "classObject", class: "list", v-show: "isShow"},
  children: [
    {
      alias: "l",
      attrsList: [{name: "@click", value: "clickItem(index)", start: 111, end: 136}],
      attrsMap: {v-for: "(l, i) in list", :key: "i", ref: "i", @click: "clickItem(index)"},
      children: [{
        end: 152
        expression: "_s(i)+":"+_s(l)",
        start: 137,
        static: false,
        text: "{{ i }}:{{ l }}",
        tokens: (3) [ {@binding: "i"}, ":",  {@binding: "l"}],
        type: 2
      }],
      end: 157,
      events: {click: {value: "clickItem(index)", dynamic: false, start: 111, end: 136}},
      for: "list",
      forProcessed: true,
      hasBindings: true,
      iterator1: "i",
      key: "i",
      parent: {/*省略 ul*/},
      plain: false,
      pre: undefined,
      rawAttrsMap: {v-for: {}, :key: {}, ref: {}, @click: {}},
      ref: ""i"",
      refInFor: true,
      start: 67,
      static: false,
      tag: "li",
      type: 1
    }
  ],
  classBinding: "classObject",
  directives: [{name: "show", rawName: "v-show", value: "isShow", arg: null, isDynamicArg: false, modifiers: undefined, start: 38, end: 53}],
  end: 173,
  hasBindings: true,
  parent: undefined,
  plain: false,
  rawAttrsMap: {
    :class: {name: ":class", value: "classObject", start: 4, end: 24}
    class: {name: "class", value: "list", start: 25, end: 37}
    v-show: {name: "v-show", value: "isShow", start: 38, end: 53}
  },
  start: 0,
  static: false,
  staticClass: ""list"",
  tag: "ul",
  type: 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

​ 我们发现每一个 AST 元素节点都多了 staic 属性。

# 4. markStaticRoots

​ 我们继续看 markStaticRoots 函数的定义,如下:

​ 源码目录:src/compiler/optimizer.js

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    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) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

markStaticRoots 第二个参数是 isInFor ,对于已经是 static 的节点或者是 v-once 指令的节点,node.staticInFor = isInFor 。 接着就是对于 staticRoot 的判断逻辑,从注释中我们可以看到,对于有资格成为 staticRoot 的节点,除了本身是一个静态节点外,必须满足拥有 children ,并且 children 不能只是一个文本节点,不然的话把它标记成静态根节点的收益就很小了。

​ 接下来和标记静态节点的逻辑一样,遍历 children 以及 ifConditions ,递归执行 markStaticRoots

​ 通过调用 markStaticRoots 函数处理过的 ast 树如下:

{
  attrsList: [{name: "v-show", value: "isShow", start: 38, end: 53}],
  attrsMap: {:class: "classObject", class: "list", v-show: "isShow"},
  children: [
    {
      alias: "l",
      attrsList: [{name: "@click", value: "clickItem(index)", start: 111, end: 136}],
      attrsMap: {v-for: "(l, i) in list", :key: "i", ref: "i", @click: "clickItem(index)"},
      children: [{
        end: 152
        expression: "_s(i)+":"+_s(l)",
        start: 137,
        static: false,
        text: "{{ i }}:{{ l }}",
        tokens: (3) [ {@binding: "i"}, ":",  {@binding: "l"}],
        type: 2
      }],
      end: 157,
      events: {click: {value: "clickItem(index)", dynamic: false, start: 111, end: 136}},
      for: "list",
      forProcessed: true,
      hasBindings: true,
      iterator1: "i",
      key: "i",
      parent: {/*省略 ul*/},
      plain: false,
      pre: undefined,
      rawAttrsMap: {v-for: {}, :key: {}, ref: {}, @click: {}},
      ref: ""i"",
      refInFor: true,
      start: 67,
      static: false,
      staticRoot: false,
      tag: "li",
      type: 1
    }
  ],
  classBinding: "classObject",
  directives: [{name: "show", rawName: "v-show", value: "isShow", arg: null, isDynamicArg: false, modifiers: undefined, start: 38, end: 53}],
  end: 173,
  hasBindings: true,
  parent: undefined,
  plain: false,
  rawAttrsMap: {
    :class: {name: ":class", value: "classObject", start: 4, end: 24}
    class: {name: "class", value: "list", start: 25, end: 37}
    v-show: {name: "v-show", value: "isShow", start: 38, end: 53}
  },
  start: 0,
  static: false,
  staticClass: ""list"",
  staticRoot: false,
  tag: "ul",
  type: 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

​ 我们发现 type1 的普通元素 AST 节点多了 staticRoot 属性。

# 5. 总结

​ 至此我们分析完了 optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用。

​ 我们通过 optimize 我们把整个 AST 树中的每一个 AST 元素节点标记了 staticstaticRoot ,它会影响我们接下来执行代码生成的过程。