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

437 阅读3分钟

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

上一节我们已经介绍了start函数解析开始标签的过程,初始化了AST树并解析了开始标签上的各种指令丰富了AST树,不清楚可以点击这里。本节我们来分析结束标签的end函数与文本字符串的char函数以及注释节点的comment函数的解析过程,关于结束标签与文本标签的编译大致流程之前已经分析过,不清楚的可以点击这里

Vue 编译(compile)核心流程之parse

end函数

end () {
  // remove trailing whitespace
  // 移除尾随的最后一个空格
  const element = stack[stack.length - 1]
  const lastNode = element.children[element.children.length - 1]
  // 如果最后一个节点存在且类型为文本节点,且文本内容是空格,且不是预编译的条件下
  if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
    // 移除这个空格节点
    element.children.pop()
  }
  // pop stack
  // 栈推出
  // stack长度减1,stack是管理ast树而创建的一个栈数组,当匹配上了stack这个栈之后,长度会减1
  stack.length -= 1
  currentParent = stack[stack.length - 1]
  closeElement(element)
},
function closeElement (element) {
  // check pre state
  // 检查pre属性,当开始标签有pre属性的话,pre为true,结束标签匹配上后,
  // 开始标签pop出stack栈,inVPre恢复false状态
  if (element.pre) {
    inVPre = false
  }
  // 原理同pre标签
  if (platformIsPreTag(element.tag)) {
    inPre = false
  }
  // apply post-transforms
  // 遍历执行postTransforms函数数组,web平台这个函数数组为空
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}

end函数主要是进行stack栈的管理,每匹配到结束标签,会将stack数组pop出一个与结束标签匹配的开始标签,直到完成template的扫描,stack被清空。

char函数

chars(text: string) {
  // (没有父节点,纯文本的情况下),(有父节点,文本定义在外面的情况下)会报错
  if (!currentParent) {
    if (process.env.NODE_ENV !== 'production') {
      // 没有父节点,纯文本的情况下,报错
      if (text === template) {
        warnOnce(
          'Component template requires a root element, rather than just text.'
        )
      // 有父节点,文本定义在外面的情况下,报错
      } else if ((text = text.trim())) {
        warnOnce(
          `text "${text}" outside root element will be ignored.`
        )
      }
    }
    return
  }
  // IE textarea placeholder bug
  // IE placeholder bug 的处理的逻辑,不太重要
  /* istanbul ignore if */
  if (isIE &&
    currentParent.tag === 'textarea' &&
    currentParent.attrsMap.placeholder === text
  ) {
    return
  }

  const children = currentParent.children
  // 对text进行处理
  text = inPre || text.trim()
    // 存在inPre,执行下面逻辑
    ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
    // only preserve whitespace if its not right after a starting tag
    // 不存在inpre
    : preserveWhitespace && children.length ? ' ' : ''
    
  // text存在
  if (text) {
    let res
    // 没有v-pre的节点,text不为空,且解析出的text文本有结果,这儿生成的是表达式节点
    if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
      children.push({
        type: 2,
        expression: res.expression,
        tokens: res.tokens,
        text
      })
    // text不是空节点,为纯文本节点
    } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
      // children push一个纯文本节点
      children.push({
        type: 3,
        text
      })
    }
  }
},

如果是含有插值表达式的文本节点,会执行parseText:

export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  // 默认的分隔符 是{{}},也可以自己配置分隔符
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  // 如果没有匹配到分隔符,说明是一个纯文本节点,直接返回
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  // 循环匹配表达式,如{{name}}
  // match的结果:['{{name}}', 'name', index: 0, input: '{{name}}', groups: undefined]
  while ((match = tagRE.exec(text))) {
    index = match.index
    // push text token
    // 当index大于lastIndex 说明匹配上了纯文本,纯文本的位置在index与lastIndex中间
    if (index > lastIndex) {
      // 进入这个逻辑,将纯文本推入rawTokens和tokens
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    // 有filter的时候,解析filter
    const exp = parseFilters(match[1].trim())
    // 将解析的表达式拼接成`_s(name)`这种形式,推入tokens
    tokens.push(`_s(${exp})`)
    // 将解析的表达式转换成对象{ '@binding': name }这种形式,推入rawTokens
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    // expression是一个拼接的表达式
    expression: tokens.join('+'),
    // 这个就是含有对象{ '@binding': exp }的数组
    tokens: rawTokens
  }
}

char会将文本分成两种情况来解析,一种情况是含有变量的插值表达式,这种情况下会在AST element上面生成expression和tokens属性,expression属性是一段拼接的属性字符串(例如_s(name)+":<"+_s(age));tokens是一个数组(例如[{@binding:"name"},":<",{@binding:"age"}])。 另一种情况是纯文本的表达式,这种情况直接生成纯文本节点。

comment函数

// 生成注释节点
comment (text: string) {
  currentParent.children.push({
    type: 3,
    text,
    isComment: true
  })
}

commnet函数就是给AST element上面添加isComment:true的属性,生成一个注释节点。

到此为止,parse的整体流程大致已经清楚了,接下来我们来看看optimize(优化ast树)的流程是怎么样的。

点击这里去往下一节