浅曦Vue源码-20-挂载阶段-$mount(9)

671 阅读3分钟

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

一、前情回顾 & 背景

本文接着前文讨论了 options.start 的工具方法:

  1. processPre:判断元素上是否有 v-pre 指令,有的话设置 el.pretrue

  2. getAndRemoveAttr:从 el.attrsMap 中获取指定属性名的属性值,然后从 el.attrsList 数组中删除这个属性,如果传递了 removeFromMap 也要从 attrsMap 中删除掉这个属性;

  3. platformIsPreTag:判断是否为 pre 标签;

  4. processRawAttr:将元素上的属性复制到 el.attrs 数组中,el.attrs 数组的属性均为静态属性,当数据发生变化时忽略这些属性;

  5. processFor: 处理 v-for 指令,得到 { for: 可迭代对象, alias: 可迭代对象条目名 };

本文接着讨论 options.start 的工具方法:processIf、processOnce、checkRootConstraints、closeElement` 及其部分内部方法

二、processIf

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

方法参数:elast 节点对象

方法作用:处理 ast 节点对象上的 v-if、v-else-if、v-else 属性,解析这些表达式的值分别挂载到 el.if、el.elseif、el.else 上;

解析到 v-if 时调用 addIfCondition(el, { exp, block })el.ifConditions 增加项;expv-if 表达式,blockel 这个 ast 节点,标识当 exp 代表的条件成立时,渲染 block 代表的 ast 节点

function processIf (el) {
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    // v-else 解析到的时候,el.else 值为 true
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

2.1 addIfCondition

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

方法参数:

  1. elAST 节点对象
  2. condition{ exp, block }expv-if 表达式的值,即条件,blockexp 所代表的条件成立时渲染的 AST 元素

方法作用:给 ast 节点对象的 ifConditions 添加条件对象,如果 el.ifConditions 不存在,则创建 ifConditions 属性值为空数组;

export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

三、processOnce

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

方法参数:elementast 节点对象

方法作用:处理 v-once 指令,如果 ast 节点上存在 v-once 指令给 element.once = true

function processOnce (el) {
  const once = getAndRemoveAttr(el, 'v-once')
  if (once != null) {
    el.once = true
  }
}

四、checkRootConstraints

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

方法参数:elast 节点对象

方法作用:检查 root 节点上的一些限制条件,作为模板上的根节点时,有一些能力是要被限制使用的,比如 <slot></slot> 或者 <template></template> 标签是不能作为根元素的,此外根节点上也不能使用 v-for 指令

function checkRootConstraints (el) {
  if (el.tag === 'slot' || el.tag === 'template') {
    warnOnce(
      `Cannot use <${el.tag}> as component root element because it may ` +
      'contain multiple nodes.',
      { start: el.start }
    )
  }
  if (el.attrsMap.hasOwnProperty('v-for')) {
    warnOnce(
      'Cannot use v-for on stateful component root element because ' +
      'it renders multiple elements.',
      el.rawAttrsMap['v-for']
    )
  }
}

五、closeElement

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

方法参数:elementast 节点对象

方法作用:这个 options.start 中的 closeElement 是当判断元素为自闭和标签时,在处理 options.start 即开始标签处理时就直接处理闭合逻辑。

closeElement 主要做了以下几件事:

  1. 调用 trimEndingWhitespace 移除自己节点中的空白符
  2. 如果 element.processed 不为 true,则调用 processElement 处理节点上的属性;
  3. 如果 stack 不为空且当前参数 elment 不是 root 时,就要看下 stack 不为空的原因,如果是 root 上使用 v-ifv-else-if 等条件语句,这是允许的;但是如果 stack 不为空又没有使用 v-if 这是就说明有多个根元素,这在 Vue 里是不允许的,给出提示;
  4. 组织节点关系,
    • 4.1 如果 element 上有 v-else-if 或者 v-else,调用 processIfConditions 处理当前 elementcurrentParent 的关系;
    • 4.2 否则,判断是否有 element.slotScope,有的话向 currentParent.scopedSlots 新增一项,keyelement.slotTarget 或者 "default"
    • 4.3 值为当前 element;最后将当前元素 pushcurrentParent.children 中,然后将 element.parent 设置为 currentParent;当然,closeElement 在处理非自闭合标签时还会将所有的非插槽子元素该元素添加到 currentParent.children 中;
  5. 处理 element.children,使之最后的值不包含作用域插槽,此后再次进行 trimEndingWhitespace
  6. 维护 inVPre、inPre
  7. 调用 postTransforms 中的方法,对 element 进行后处理;postTransforms pluckModuleFunction(options.modules, 'postTransformNode'),在 web 平台下,modules 中的 klass/style/model 没有任何一个导出了 postTransformNode 方法,所以 postTransforms 是空数组
function closeElement (element) {
  trimEndingWhitespace(element)
  if (!inVPre && !element.processed) {
    element = processElement(element, options)
  }
  // tree management
  if (!stack.length && element !== root) {
    // 如果 stack 不为空,并且 root 上使用 v-if/v-else-if/v-else,这是允许的
    if (root.if && (element.elseif || element.else)) {
   
      addIfCondition(root, {
        exp: element.elseif,
        block: element
      })
    } else if (process.env.NODE_ENV !== 'production') {
      // 否则就说明有多个根节点了,此时不被允许,给出提示
    }
  }
  if (currentParent && !element.forbidden) {
    if (element.elseif || element.else) {
      processIfConditions(element, currentParent)
    } else {
      if (element.slotScope) {
        // scoped slot
        // keep it in the children list so that v-else(-if) conditions can
        // find it as the prev node.
        const name = element.slotTarget || '"default"'
        ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
      }
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }

  // element.children 不包含作用域插槽
  element.children = element.children.filter(c => !(c: any).slotScope)
  // 再次移除 children 中的空白符
  trimEndingWhitespace(element)

  // 维护 inVPre、inPre,
  // 如果 element.pre 说明当前元素使用了 v-pre
  // 现在是 closeElement,需要再下一次开始处理开始标签之前将 inVPre、inPre 置为 false
  if (element.pre) {
    inVPre = false
  }
  if (platformIsPreTag(element.tag)) {
    inPre = false
  }
  // 调用 postTransforms 的处理方法
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}

5.1 trimEndingWhitespace

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

方法参数:el,ast 节点对象

方法作用:如果不在 pre 标签中,移除当前节点的所有子元素中的空白元素

function trimEndingWhitespace (el) {
  if (!inPre) {
    let lastNode
    while (
      (lastNode = el.children[el.children.length - 1]) &&
      lastNode.type === 3 &&
      lastNode.text === ' '
    ) {
      el.children.pop()
    }
  }
}

5.2 processElement

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

方法参数:

  1. element: ast 节点对象;
  2. optionscreateCompiler 接收的 baseOptions

方法作用:分别处理元素的 key、ref、插槽、自闭和slot、动态组件、class、style、v-bind、v-on等指令,将处理所得信息添加到元素上,最后返回 element

export function processElement (
  element: ASTElement,
  options: CompilerOptions
) {
  // 处理元素上的 key:设置 el.key = val
  processKey(element)
  
  element.plain = (
    !element.key &&
    !element.scopedSlots &&
    !element.attrsList.length
  )
  
  // 处理 ref 属性
  processRef(element)

  // 处理插槽内容
  processSlotContent(element)

  
  processSlotOutlet(element)

  processComponent(element)

 
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }

 
  processAttrs(element)
  return element
}

5.3 processIfConditions

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

方法参数:

  1. elast 节点对象;
  2. parentel 的父元素 ast 节点对象;

方法作用:从 parent children 中找到前一个元素节点 prev,如果 previf 属性,就调用 addIfConditions 添加 elseif 或者 else block

function processIfConditions (el, parent) {
  const prev = findPrevElement(parent.children)
  if (prev && prev.if) {
    addIfCondition(prev, {
      exp: el.elseif,
      block: el
    })
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
      `used on element <${el.tag}> without corresponding v-if.`,
      el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
    )
  }
}

六、总结

本篇小作文继续讨论了 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 方法,后面两篇将会这个讲述这个过程