源码分析:Vue 编译(compile)核心流程之parse(中)

516 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

上一节我们已经介绍了template解析成match对象的一个过程,不清楚可以点击这里,本节我们来分析match对象生成ast树的过程。

Vue 编译(compile)核心流程之parse(AST树的生成)

template模板字符串中的开始标签、结束标签、文本字符串等的大致解析过程在上一节我们已经介绍了,这一节我们来继续分析。

当处理开始标签的时候,其中会执行到handleStartTag函数,这个函数的最后一步会执行options.start函数,接下来我们分析下这个函数:

function handleStartTag (match) {
  ......

  // 调用options中的start函数
  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}

start函数

start (tag, attrs, unary) {
  // check namespace.
  // inherit parent ns if there is one
  const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

  // handle IE svg bug
  /* istanbul ignore if */
  if (isIE && ns === 'svg') {
    attrs = guardIESVGBug(attrs)
  }

  // 生成ast树的核心函数,返回ast对象 { type: 1, tag, attrsList, attrsMap, parent, children}
  let element: ASTElement = createASTElement(tag, attrs, currentParent)
  if (ns) {
    element.ns = ns
  }

  // 如果是script、stype、text/javascript等标签且非服务端渲染,则报错
  if (isForbiddenTag(element) && !isServerRendering()) {
    element.forbidden = true
    process.env.NODE_ENV !== 'production' && warn(
      'Templates should only be responsible for mapping the state to the ' +
      'UI. Avoid placing tags with side-effects in your templates, such as ' +
      `<${tag}>` + ', as they will not be parsed.'
    )
  }

  // apply pre-transforms 执行平台相关的preTransforms函数
  for (let i = 0; i < preTransforms.length; i++) {
    element = preTransforms[i](element, options) || element
  }

  // 对ast对象做一个丰富
  // v-pre相关,这个指定对于没有其他指令的标签用处较大,跳过大量没有指令的节点会加快编译。
  if (!inVPre) {
    // 拿到v-pre的值,给AST element添加pre属性,并且在attrs数组及attrsMap中删掉该指令
    processPre(element)
    if (element.pre) {
      // 解析到了v-pre指令,inVPre赋值为true
      inVPre = true
    }
  }
  // 如果tag是pre标签
  if (platformIsPreTag(element.tag)) {
    // inPre赋为true
    inPre = true
  }
  // inVPre为true的时候,执行processRawAttrs函数,就不需要编译分析其他的指令了
  if (inVPre) {
    processRawAttrs(element)
  } else if (!element.processed) {
    // 没有v-pre,解析attr中存储的指令
    // structural directives
    // 解析For指令
    processFor(element)
    // 解析If指令
    processIf(element)
    // 解析Once指令
    processOnce(element)
    // element-scope stuff
    // 解析其他的指令
    processElement(element, options)
  }

  function checkRootConstraints (el) {
    if (process.env.NODE_ENV !== 'production') {
      if (el.tag === 'slot' || el.tag === 'template') {
        warnOnce(
          `Cannot use <${el.tag}> as component root element because it may ` +
          'contain multiple nodes.'
        )
      }
      if (el.attrsMap.hasOwnProperty('v-for')) {
        warnOnce(
          'Cannot use v-for on stateful component root element because ' +
          'it renders multiple elements.'
        )
      }
    }
  }

  // tree management
  // ast树管理
  if (!root) {
    root = element
    // 检查根节点
    checkRootConstraints(root)
  } else if (!stack.length) {
    // 不止一个根节点,报错
    // allow root elements with v-if, v-else-if and v-else
    if (root.if && (element.elseif || element.else)) {
      checkRootConstraints(element)
      addIfCondition(root, {
        exp: element.elseif,
        block: element
      })
    } else if (process.env.NODE_ENV !== 'production') {
      warnOnce(
        `Component template should contain exactly one root element. ` +
        `If you are using v-if on multiple elements, ` +
        `use v-else-if to chain them instead.`
      )
    }
  }
  // 对父子节点的关系进行管理,生成规范的AST树
  if (currentParent && !element.forbidden) {
    if (element.elseif || element.else) { // elseif的逻辑分析
      processIfConditions(element, currentParent)
    } else if (element.slotScope) { // scoped slot slot的相关逻辑分析
      currentParent.plain = false
      const name = element.slotTarget || '"default"'
      ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
    } else { // 管理父子节点关系
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }
  // 非一元标签,需要执行推栈入栈操作,管理ast树
  if (!unary) {
    currentParent = element
    stack.push(element)
  } else {
    // 关闭标签
    closeElement(element)
  }
},

start函数主要的逻辑分成两个部分,第一部分是创建AST树,解析属性,对AST树进行扩展,第二部分是对AST树进行管理;

1.创建AST树并解析属性

创建AST树

export function createASTElement (
  tag: string,
  attrs: Array<Attr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    parent,
    children: []
  }
}
function makeAttrsMap (attrs: Array<Object>): Object {
  const map = {}
  for (let i = 0, l = attrs.length; i < l; i++) {
    if (
      process.env.NODE_ENV !== 'production' &&
      map[attrs[i].name] && !isIE && !isEdge
    ) {
      warn('duplicate attribute: ' + attrs[i].name)
    }
    map[attrs[i].name] = attrs[i].value
  }
  return map
}

这个地方会返回一个原始的AST树对象,makeAttrsMap函数会遍历attr数组里面的对象属性,解析成为一个map对象键值对的形式,这样会方便后续对属性的查找取值操作。

解析指令

生成了基本的AST树后,会对标签的属性进行解析,这里我们分析下部分指令的解析:

  • v-if指令
function processIf (el) {
  // getAndRemoveAttr函数会取出AST element对象中的v-if指令作为exp,
  // 并移除el对象的attr数组和attrMap对象中的v-if指令
  // 如v-if="isTrue"会被解析成exp会赋值为isTrue
  const exp = getAndRemoveAttr(el, 'v-if')
  // 有表达式的情况
  if (exp) {
    // 在AST element对象上添加if属性
    el.if = exp
    添加ifConditions属性数组,将第二个参数push进该数组
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    // 如果是v-if不存在,设置v-else指令
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    // 如果是v-if不存在,设置v-else-if指令
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  // 给AST element添加ifConditions属性,该属性为数组
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  
  el.ifConditions.push(condition)
}
  • 其他指令
export function processElement (element: ASTElement, options: CompilerOptions) {
  // 解析Key指令
  processKey(element)

  // determine whether this is a plain element after
  // removing structural attributes
  element.plain = !element.key && !element.attrsList.length

  // 解析Ref指令
  processRef(element)
  // 解析Slot指令
  processSlot(element)
  // 解析Component指令
  processComponent(element)
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }
  // 解析剩余的其他指令如event model等
  processAttrs(element)
}

2.AST树管理

// tree management
// ast树管理
if (!root) {
  root = element
  // 检查根节点
  checkRootConstraints(root)
} else if (!stack.length) {
  // 不止一个根节点,报错
  // allow root elements with v-if, v-else-if and v-else
  if (root.if && (element.elseif || element.else)) {
    checkRootConstraints(element)
    addIfCondition(root, {
      exp: element.elseif,
      block: element
    })
  } else if (process.env.NODE_ENV !== 'production') {
    warnOnce(
      `Component template should contain exactly one root element. ` +
      `If you are using v-if on multiple elements, ` +
      `use v-else-if to chain them instead.`
    )
  }
}
if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
    processIfConditions(element, currentParent)
  } else if (element.slotScope) { // scoped slot
    currentParent.plain = false
    const name = element.slotTarget || '"default"'
    ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
  } else {
    currentParent.children.push(element)
    element.parent = currentParent
  }
}
if (!unary) {
  currentParent = element
  stack.push(element)
} else {
  // 关闭标签
  closeElement(element)
}

AST树管理的主要作用就是去维护AST树对象的父子关系,并对根节点做一些检测,比如不能出现重复的根节点情况等,此外还会对一些指令进行解析。

总结

本节我们分析了处理开始标签中start函数的整个解析流程,大致可以分为三步:

  • 1.生成初始的AST对象;
  • 2.将我们扫描开始标签生成的match对象中attr数组作进一步的解析,针对不同的指令属性,解析生成不同的属性扩展到AST树对象上;
  • 3.对生成的AST对象进行管理,维护AST树对象的父子关系;

下一节我们继续分析parse过程的其他流程,点击这里去往下一节