optimize

当我们的模板 template 经过 parse 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化,optimize 的逻辑是远简单于 parse 的逻辑,所以理解起来会轻松很多。

为什么要有优化过程,因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对。

来看一下 optimize 方法的定义,在 src/compiler/optimizer.js 中:

  1. /**
  2. * Goal of the optimizer: walk the generated template AST tree
  3. * and detect sub-trees that are purely static, i.e. parts of
  4. * the DOM that never needs to change.
  5. *
  6. * Once we detect these sub-trees, we can:
  7. *
  8. * 1. Hoist them into constants, so that we no longer need to
  9. * create fresh nodes for them on each re-render;
  10. * 2. Completely skip them in the patching process.
  11. */
  12. export function optimize (root: ?ASTElement, options: CompilerOptions) {
  13. if (!root) return
  14. isStaticKey = genStaticKeysCached(options.staticKeys || '')
  15. isPlatformReservedTag = options.isReservedTag || no
  16. // first pass: mark all non-static nodes.
  17. markStatic(root)
  18. // second pass: mark static roots.
  19. markStaticRoots(root, false)
  20. }
  21. function genStaticKeys (keys: string): Function {
  22. return makeMap(
  23. 'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
  24. (keys ? ',' + keys : '')
  25. )
  26. }

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

标记静态节点

  1. function markStatic (node: ASTNode) {
  2. node.static = isStatic(node)
  3. if (node.type === 1) {
  4. // do not make component slot content static. this avoids
  5. // 1. components not able to mutate slot nodes
  6. // 2. static slot content fails for hot-reloading
  7. if (
  8. !isPlatformReservedTag(node.tag) &&
  9. node.tag !== 'slot' &&
  10. node.attrsMap['inline-template'] == null
  11. ) {
  12. return
  13. }
  14. for (let i = 0, l = node.children.length; i < l; i++) {
  15. const child = node.children[i]
  16. markStatic(child)
  17. if (!child.static) {
  18. node.static = false
  19. }
  20. }
  21. if (node.ifConditions) {
  22. for (let i = 1, l = node.ifConditions.length; i < l; i++) {
  23. const block = node.ifConditions[i].block
  24. markStatic(block)
  25. if (!block.static) {
  26. node.static = false
  27. }
  28. }
  29. }
  30. }
  31. }
  32. function isStatic (node: ASTNode): boolean {
  33. if (node.type === 2) { // expression
  34. return false
  35. }
  36. if (node.type === 3) { // text
  37. return true
  38. }
  39. return !!(node.pre || (
  40. !node.hasBindings && // no dynamic bindings
  41. !node.if && !node.for && // not v-if or v-for or v-else
  42. !isBuiltInTag(node.tag) && // not a built-in
  43. isPlatformReservedTag(node.tag) && // not a component
  44. !isDirectChildOfTemplateFor(node) &&
  45. Object.keys(node).every(isStaticKey)
  46. ))
  47. }

首先执行 node.static = isStatic(node)

isStatic 是对一个 AST 元素节点是否是静态的判断,如果是表达式,就是非静态;如果是纯文本,就是静态;对于一个普通元素,如果有 pre 属性,那么它使用了 v-pre 指令,是静态,否则要同时满足以下条件:没有使用 v-ifv-for,没有使用其它指令(不包括 v-once),非内置组件,是平台保留的标签,非带有 v-fortemplate 标签的直接子节点,节点的所有属性的 key 都满足静态 key;这些都满足则这个 AST 节点是一个静态节点。

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

标记静态根

  1. function markStaticRoots (node: ASTNode, isInFor: boolean) {
  2. if (node.type === 1) {
  3. if (node.static || node.once) {
  4. node.staticInFor = isInFor
  5. }
  6. // For a node to qualify as a static root, it should have children that
  7. // are not just static text. Otherwise the cost of hoisting out will
  8. // outweigh the benefits and it's better off to just always render it fresh.
  9. if (node.static && node.children.length && !(
  10. node.children.length === 1 &&
  11. node.children[0].type === 3
  12. )) {
  13. node.staticRoot = true
  14. return
  15. } else {
  16. node.staticRoot = false
  17. }
  18. if (node.children) {
  19. for (let i = 0, l = node.children.length; i < l; i++) {
  20. markStaticRoots(node.children[i], isInFor || !!node.for)
  21. }
  22. }
  23. if (node.ifConditions) {
  24. for (let i = 1, l = node.ifConditions.length; i < l; i++) {
  25. markStaticRoots(node.ifConditions[i].block, isInFor)
  26. }
  27. }
  28. }
  29. }

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

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

回归我们之前的例子,经过 optimize 后,AST 树变成了如下:

  1. ast = {
  2. 'type': 1,
  3. 'tag': 'ul',
  4. 'attrsList': [],
  5. 'attrsMap': {
  6. ':class': 'bindCls',
  7. 'class': 'list',
  8. 'v-if': 'isShow'
  9. },
  10. 'if': 'isShow',
  11. 'ifConditions': [{
  12. 'exp': 'isShow',
  13. 'block': // ul ast element
  14. }],
  15. 'parent': undefined,
  16. 'plain': false,
  17. 'staticClass': 'list',
  18. 'classBinding': 'bindCls',
  19. 'static': false,
  20. 'staticRoot': false,
  21. 'children': [{
  22. 'type': 1,
  23. 'tag': 'li',
  24. 'attrsList': [{
  25. 'name': '@click',
  26. 'value': 'clickItem(index)'
  27. }],
  28. 'attrsMap': {
  29. '@click': 'clickItem(index)',
  30. 'v-for': '(item,index) in data'
  31. },
  32. 'parent': // ul ast element
  33. 'plain': false,
  34. 'events': {
  35. 'click': {
  36. 'value': 'clickItem(index)'
  37. }
  38. },
  39. 'hasBindings': true,
  40. 'for': 'data',
  41. 'alias': 'item',
  42. 'iterator1': 'index',
  43. 'static': false,
  44. 'staticRoot': false,
  45. 'children': [
  46. 'type': 2,
  47. 'expression': '_s(item)+":"+_s(index)'
  48. 'text': '{{item}}:{{index}}',
  49. 'tokens': [
  50. {'@binding':'item'},
  51. ':',
  52. {'@binding':'index'}
  53. ],
  54. 'static': false
  55. ]
  56. }]
  57. }

我们发现每一个 AST 元素节点都多了 staic 属性,并且 type 为 1 的普通元素 AST 节点多了 staticRoot 属性。

总结

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

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

原文: https://ustbhuangyi.github.io/vue-analysis/compile/optimize.html