浅曦Vue源码-24-挂载阶段-$mount(13)

544 阅读7分钟

「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

上一篇小作文主要完成了以下工作:

  1. parseHTML 的最后一个回调方法 options.end 的讨论;
  2. curerntParentstartend 中的更新作用进行了详细讨论;
  3. 回顾了整个 parse 方法的 parseHTML 的梗概方法及作用;

前面的部分已经介绍了 parse 方法获取 html 模板获取 ast 的过程,接下来的部分将继续后面的部分,本篇小作文聚焦于生成 ast 后的静态标记优化。

二、baseCompile 中的静态标记调用

parse 生成 ast 后就会调用 optimize 方法进行静态标记处理。

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  // 优化,为每个节点做静态标记
  if (options.optimize !== false) {
    optimize(ast, options)
  }

  
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

三、optimize 方法

方法位置:src/compiler/optimizer.js -> function optimize

方法参数:

  1. root,顶层的 ast 节点对象;
  2. options:编译器选项对象

方法作用:

  1. 生成 isStaticKey 函数,isStaticKey 函数时检测某些属性是否是静态属性的函数,options.staticKeys'staicClass,staticStyle' 这个字符串,这个字符串同样来自 baseOptionsbaseOptions 来自 createCompiler(baseOptions) 传入的;
    • 1.1 isStatic 方法是接收某个 key,判断这个 key 是否是 type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticStyle,staticClass 中的一个,言外之意这些列出的都是 ast 静态属性;
  2. 遍历 root 节点,给每个节点设置 static 属性,此属性标识当前节点是否为静态节点;

被标记成静态节点的节点后面数据更新时不再关注这些节点,patch 时也会忽略这些静态节点;

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')

  // 是否是保留平台标签
  isPlatformReservedTag = options.isReservedTag || no

  // 遍历节点,给每个节点设置 static 属性,标识其是否为静态节点
  markStatic(root)

  // 标记静态根
  markStaticRoots(root, false)
}

3.1 genStaticKeysCached

方法位置:src/compiler/optimizer.js -> const genStaticKeysCached

方法参数:str

方法作用:返回一个函数,这个函数会优先从缓存中获取结果;

const genStaticKeysCached = cached(genStaticKeys)

3.1.1 cached

方法位置:src/shared/util.js -> function cached

方法参数:fn,目标函数

方法作用:创建缓存对象,返回一个新函数,这个新函数就优先取用缓存中的结果。当第一次执行这个新函数的时候,会把函数返回值放到缓存中

export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str] // hit 就是从缓存中取得的结果
    // 如果 hit 有值说明命中缓存,否则就调用 fn 并缓存结果
    // 在静态优化的时候,fn 就是下面 3.1.2 genStaticKeys
    return hit || (cache[str] = fn(str)) 
  }: any)
}

3.1.2 genStaticKeys

方法位置:src/compiler/optimizer.js -> genStaticKeys

方法参数:

  1. keys: 由 , 分隔的 key 组成的字符串

方法作用:接收 keys 字符串,返回调用 makeMap 生成 map 后返回的验证 key 是否在 map 中的函数;

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

3.1.2 makeMap

方法位置:src/shared/util.js -> functions makeMap

方法参数:

  1. str,由,分隔的多个 key 组成的字符串
  2. expectLowerCase,是否小写

方法作用:把 str, 拆分成数组,然后遍历数组生成一个 mapkey 就是由 str 拆分成的数组,valuetrue,并且返回一个函数检验一个 key 是否在这个 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]
}

3.2 markStatic

方法位置:src/compiler/optimizer.js -> functions markStatic

方法参数:nodeast 节点对象

方法作用:

  1. node 设置 static 属性,值是 isStatic(node) 方法的返回值;

  2. 在这个过程中会遍历 nodechildren,则递归处理 children 中的每一个 child,如果 child 为非静态节点则 node 本身也不能算作静态节点;

  3. 此外如果 node.ifConditions 存在,则说明 nodev-if/v-else-if/v-else 指令,还要递归处理每个条件语句中的 block,如果 block 不是静态元素,则 node 也不能算作静态元素,即 node.stack = false

  4. 递归终止的条件为:如果节点不是平台保留标签 && 不是 slot 标签 && 不是内联模板;换言之,能够进行静态标记的都是啥呢?是平台保留标签 或者 slot 标签 或者 内联模板

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {
    // 不要将组件的插槽内容设置为静态节点,这样可以避免:
    // 1. 组件不能改变插槽节点
    // 2. 静态插槽内容在热重载时失败
  
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      // 递归终止条件:
    
      return
    }

    // 遍历子节点,递归调用 markStatic 来标记这些子节点 static 属性
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      // 如果子节点为非静态节点,则将父节点更新为静态节点
      if (!child.static) {
        node.static = false
      }
    }

    // 如果 node.ifConditons 存在说明 节点存在 v-if/v-else-if/v-else 指令
    // 此时要递归处理指定条件下要渲染的元素是否静态,即 node.ifCondtions[].block
    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
        }
      }
    }
  }
}

3.2.1 isStatic

方法位置:src/compiler/optimizer.js -> function isStatic

方法参数:nodeast 节点对象

方法作用:判断节点对象是否为静态,判断标准为:

  1. node.type === 2 时为表达式,是动态;多说一句,node.type2 的情况你还记得是在哪里创建的吗?没错,就是 parse 方法解析 html 模板时解析到文本时会调用 options.chars 方法,options.chars 会判断文本中是否有 {{}} 这种语法,如果有,则创建 node.type2ast 节点;
  2. node.type === 3 为文本,是静态
  3. 综合判断条件,满足以下条件之一:
    • 3.1 在 pre 标签中,即被 <pre></pre> 包裹;
    • 3.2 下列条件都成立:
      • 3.2.1 el.hasBindings 不为 true
      • 3.2.2 el.if 不存在
      • 3.2.3 node.for 不存在
      • 3.2.4 不是 slotcompnent 这两个内建标签
      • 3.2.5 带有 v-fortemplate 的直接子级
      • 3.2.6 node 上的所有属性都是静态属性

那么何时为 true 呢?

调用 prcessAttrs() 时如果发现元素有指令,所谓指令就是 Vue 的指令,包含简写例如 :/@,就会将 el.hansBindingds 置为 true

function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // 没有动态绑定
    !node.if && !node.for && // 没有 v-for 、v-if/v-else-if/v-else
    !isBuiltInTag(node.tag) && // 不是内建的 slot 或者 component 标签
    isPlatformReservedTag(node.tag) && // 是平台保留标签,即不是一个自定义组件
    !isDirectChildOfTemplateFor(node) && // 不是 带有 v-for 的 template 的直接子级
    Object.keys(node).every(isStaticKey) // node 上的属性每个都是静态属性
  ))
}

3.3 markStaticRoots

方法位置:src/compiler/optimizer.js -> markStaticRoots

方法参数:

  1. nodeast 节点对象

方法作用:进一步标记静态根节点,一个节点要成为静态根节点要满足:

  1. 首先是元素节点,即 node.type === 1
  2. 元素必须是静态的,即 node.statick === type
  3. 要有子元素,即 node.children.length
  4. 元素不能只有一个文本节点

为啥有这么多要求?
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.
因为不这么做的话,提升这些节点位静态根代价比直接每次更新时直接渲染大的多

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      // 节点是静态的或者节点上有 v-once 指令,标记 node.staticInFor = true or false
      node.staticInFor = isInFor
    }
   
    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)
      }
    }

    // 如果节点存在 v-for/v-else-if/v-else 指令,则尝试处理 block 节点标记静态根
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

四、总结

本文详细讲解了对 parse 得到的 ast 进行静态标记的过程,这个过程的意义在于被标记成静态的 ast 节点,在数据发生更新是不会被重新渲染;其核心实现主要有在 optimize 方法中:

  1. 调用 genStaticKeysCached 获取 isStaticKeys 方法备用;
  2. 调用 markStatic 方法递归处理 ast 节点及其子节点和条件渲染节点,为每个节点设置 static 属性,值为 isStatic() 方法返回值,isStatic 方法则根据 ast 节点对象上的信息判断是否为静态;
  3. 调用 markStaticRoot() 判断节点是否为静态根,静态根节点在数据更新时会被忽略,也不会被 patch