浅曦Vue源码-21-挂载阶段-$mount(10)

189 阅读6分钟

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

一、前情回顾 & 背景

本篇小作文继续讨论了 options.start 方法的内部方法的作用:

  1. processIf:处理 ast 节点对象上的 v-if/v-else-if/v-else,将这些条件渲染信息添加到 el.if/el.elseif/el.else

  2. processOnce:处理 v-once,得到 el.once = true

  3. checkRootContraints:检查根元素的限制,例如不能用 slottemplate 作为根;

  4. 以及 options.start 接收到自闭和标签时执行的 closeElement 及其工具方法,这里并没有说完 closeElement 方法,后面两篇将会这个讲述这个过程

二、 procesKey

方法位置:src/compiler/parser/index.js -> processKey

方法参数:elast 节点对象

方法作用:解析 el 上的 key 属性值,这个 key 就是我们使用 v-for 设置的 :key="xxx";在这个过程中会检查 template 标签上不能使用 keytransition-group 子元素不能使用 v-forindex 作为 key

function processKey (el) {
  // 拿到 key 的属性值
  const exp = getBindingAttr(el, 'key')
  if (exp) {
    // 关于 key 使用上的异常处理
    if (process.env.NODE_ENV !== 'production') {
      // template 标签不允许使用 key
      if (el.tag === 'template') {
       
      }

      // 不要在 <transition-group> 子元素上
      // 使用 v-for 的 index 作为 key,这和没有 key 没区别
      if (el.for) {
       
      }
    }
    // 设置 el.key = exp
    el.key = exp
  }
}

三、processRef

方法位置:src/compiler/parser/index.js -> functions processRef

方法参数:elast 节点对象

方法作用:处理 el 上的 ref 属性,el.ref = ref,随后 checkInFor(el) 即检查当前节点是否处于有 v-for 的元素包裹中(子代或者其他后代),如果被有 v-for 的元素包围了,ref 就是这些 DOM 组成的数组;

function processRef (el) {
  const ref = getBindingAttr(el, 'ref')
  if (ref) {
    el.ref = ref
    // 判断包含 ref 属性的元素是否包含在具有 v-for 指令的元素内或后代元素中
    // 如果是,则 ref 将会是有这个 ref 的元素组成的的数组
    el.refInFor = checkInFor(el)
  }
}

四、 checkInFor

方法位置:src/compiler/parser/index.js -> function checkInFor

方法参数:elast 节点对象

方法作用:查看当前元素的长辈(父辈及更高)是否有 v-for 指令。原理就是 while(parent) 循环,检查 parent.for 是否有值,parent.for 是处理 v-for 的时候添加到 el 上的属性。el.for 的值指向可迭代对象,el.alias 指向可迭代对象的条目名称,例如 item

function checkInFor (el: ASTElement): boolean {
  let parent = el
  while (parent) {
    if (parent.for !== undefined) {
      return true
    }
    parent = parent.parent
  }
  return false
}

五、processSlotContent

方法位置:src/compiler/parser/index.js -> function processSlotContent

方法参数:elast 节点对象

方法作用:处理被作为插槽传递给组件的内容;解析插槽名、插槽是否为动态插槽、作用域插槽的的值,当在组件上直接使用 v-slot 语法时,将前述内容放到 el.scopedSlot 对象上,其他情况下直接放到 el 上;

  1. 如果 el.tag 即当前 el 标签为 template 时,从 el 上获取 scope 属性值或者 slot-scope 值;
  2. 如果不是 template 标签,获取 slot-scope 属性值;
  3. 处理完插槽作用域,接着处理 slot 插槽的名字,
    • 3.1 即具名插槽的名字,slot="xxx" 是老旧的具名插槽语法,设置 el.slotTarget = 插槽名或者 "default"
    • 3.2 接着还要判断插槽名字是否使用了动态绑定的语法即 v-bind:slot="xxx" 或者 :slot="xxx" 这种形式的,就是动态绑定了动态插槽语法,如果用了就设置 el.slotTargetDynamic 属性,命中为 true,否则 false
  4. 处理 Vue 2.6 以后的版本中 v-slot 指令:
    • 4.1 如果 v-slottemplate 标签上,通过 slotBinding = getAndReomveAttrByRegex(el, slotReg) 获取 v-slot 绑定的 slot 信息:namedynamic,即插槽名和是否动态插槽。期间检测是否存在 v-slotslot-scope 混用的情况;将 name 赋值给 el.slotTarget,dynamic 赋值给 el.slotTargetDynamic,而作用域插槽的值 el.slotScope = slotBinding.value
    • 4.2 else 就是直接在组件上使用 v-slot 指令,要手动创建一个 <template></template> ast 节点 slotContainer,用于包装这个组件,所以直接在组件上 v-slot 只不过是一个语法糖;接着进行前面 4.1 中的赋值 el.slotTarget/el.slotTargetDynamic/el.slotScope,然后将这个包装 slotContrainerchildren 中的非作用域插槽的元素的筛选出来,并且将其父元素 parent 设置为 slotContrainer
    • 4.3 清空 el.children,因为他的孩子都跑到了 el.scopedSlots
    • 4.4 标记 el.plainfalse
function processSlotContent (el) {
  let slotScope
  if (el.tag === 'template') {
    // template 标签上使用 scope 属性的提示
 
    slotScope = getAndRemoveAttr(el, 'scope')
   
    if (process.env.NODE_ENV !== 'production' && slotScope) {
      
    }

    // el.slotScope = val
    el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
  } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
    // slot-scope 也可以用到非 template 标签上
  
    if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
      // 元素不能同时使用 slot-scope 和 v-for 指令,v-for 优先级更高
      // 应该用 template 标签作为容器,将 slot-scope 放到 template 标签上,
      // 而 v-for 写在外面
      
    }
    // 将 slotScope 添加到 el 对象上
    el.slotScope = slotScope
  }

  // 获取 slot 属性值,老的具名插槽的语法
  // <div slot="someSlotName" ></div>
  const slotTarget = getBindingAttr(el, 'slot')
  if (slotTarget) {
    // el.slotTarget = 插槽名(具名插槽)
    el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget

    // 动态插槽名:<div v-bind:slot="someDynamicSlot" 或者 :slot="sds" > 
    el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
    
    // 保留 slot 作为一个原生属性以兼容原生的 shadow DOM,仅当非作用域插槽
    if (el.tag !== 'template' && !el.slotScope) {
      addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
    }
  }

  // 2.6 以后的 v-slot 语法
  if (process.env.NEW_SLOT_SYNTAX) {
    if (el.tag === 'template') {
      // v-slot 在 template 标签上,得到 v-slot 的值
      // v-slot on <template>
      const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
      if (slotBinding) {
        // 异常提示
        if (process.env.NODE_ENV !== 'production') {
          if (el.slotTarget || el.slotScope) {
            // 不同插槽语法禁止混用
            warn(
              `Unexpected mixed usage of different slot syntaxes.`,
              el
            )
          }

          if (el.parent && !maybeComponent(el.parent)) {
            // <template v-slot> 这能出现在组件内部的直接子级,比如
            // <comp>
            //    <template v-slot>xx</template>
            // </comp>
            // 而不能是
            // <comp>
            //    <div>
            //      <template v-slot>xx</template>
            //    </div>
            // </comp>
            warn(
              `<template v-slot> can only appear at the root level inside ` +
              `the receiving component`,
              el
            )
          }
        }

        // 得到插槽名称
        const { name, dynamic } = getSlotName(slotBinding)

        // 插槽名
        el.slotTarget = name

        // 是否为动态插槽
        el.slotTargetDynamic = dynamic

        // 作用域插槽的值
        el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
      }
    } else {
      // 处理组件上的 v-slot,<comp v-slot:header />
      // slotBinding = { name: 'v-slot:header', value: '', start, end }
      // v-slot on component, denotes default slot
      const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
      if (slotBinding) {
        if (process.env.NODE_ENV !== 'production') {
          // el 不是组件,提示 v-slot 只能出现在组件或者 template 标签上
          if (!maybeComponent(el)) {
            warn(
              `v-slot can only be used on components or <template>.`,
              slotBinding
            )
          }

          // 语法混用
          if (el.slotScope || el.slotTarget) {
            warn(
              `Unexpected mixed usage of different slot syntaxes.`,
              el
            )
          }

          // 为了避免歧义,当存在其他命名插槽时,默认插槽也该用 <template> 语法
          if (el.scopedSlots) {
            warn(
              `To avoid scope ambiguity, the default slot should also use ` +
              `<template> syntax when there are other named slots.`,
              slotBinding
            )
          }
        }

        // 将组件的孩子添加到它的默认插槽内
        // add the component's children to its default slot
        const slots = el.scopedSlots || (el.scopedSlots = {})

        // 获取插槽名称以及是否为动态插槽
        const { name, dynamic } = getSlotName(slotBinding)

        // 创建一个 template 标签的 ast 对象,用于容纳插槽内容,父级是 el
        const slotContainer = slots[name] = createASTElement('template', [], el)

        // 插槽名
        slotContainer.slotTarget = name

        // 是否为动态插槽
        slotContainer.slotTargetDynamic = dynamic

        // 所有的孩子,将每一个孩子的 parent 属性都设置为 slotContainer
        slotContainer.children = el.children.filter((c: any) => {
          if (!c.slotScope) {
            // 筛选并给插槽内非插槽元素设置 parent 属性
            // 为 slotContainer,也就是这个语法糖的 template 元素
            c.parent = slotContainer
            return true
          }
        })

        // el 的 插槽都被弄到 el.scopedSlots 属性了
        el.children = []
        // mark el non-plain so data gets generated
        el.plain = false
      }
    }
  }
}

六、总结

本篇小作文在讨论 options.start 中的 closeElement 的内部工具方法:processKeyprocessRefcheckInForprocessSlotContent

  1. processKey:检查元素上的 key 属性并检查使用 key 的限制;

  2. processRef:处理 ref 属性,若 refv-for 包裹,则 ref 指向一个数组;

  3. checkInFor:检测当前元素的父辈元素中是否有 v-for 指令;

  4. processSlotContent:处理插槽内容,获取插槽名、动态插槽以及 2.6.x 以后的 v-slot 指令;