vue2-编译之生成AST

620 阅读2分钟

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

前言

在上篇文章我们分析了编译中parse的部分代码,也就是parseHTML的实现。在parseHTML中通过逐字匹配将template进行了初步解析。现在我们继续分析在parseHTML中输出的结果是如何被parse进行使用的。以此结束完整parse流程的分析。

parse

我们依然从入口文件开始

const ast = parse(template.trim(), options)

我们来看看parse的实现

const stack = []
let root
let currentParent

parseHTML(template, {
  expectHTML: options.expectHTML,
  // ...
  start (tag, attrs, unary, start, end) {
    // 解析开始标签调用的钩子
  },

  end (tag, start, end) {
    // 解析结束标签调用的钩子
  },

  chars (text: string, start: number, end: number) {
    // 解析文本节点调用的钩子
  },

  comment (text: string, start, end) {
    // 解析注释节点调用的钩子
  }
})

return root

可以发现,parse的实现主要是初始化一些钩子函数然后将其作为参数传递给parseHTML。我们上篇分析了parseHTML的实现,就是通过正则提取teamplate的信息,将其标签属性提取出来,然后再调用parse中的钩子。所以本篇文章的重点在钩子函数中是如何进行进一步处理的。

我们通过实例来分析

<div>
  <div v-if="isShow" @click="doSomething" :class="activeClass">text{{name}}{{value}}text</div>
  <div v-for="item in 10"></div>
</div>

start

start (tag, attrs, unary, start, end) {
  // 1
  let element: ASTElement = createASTElement(tag, attrs, currentParent)

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

  // ...

  // 3
  if (inVPre) {
    processRawAttrs(element)
  } else if (!element.processed) {
    // structural directives
    processFor(element)
    processIf(element)
    processOnce(element)
  }

  // ...

  // 4
  if (!unary) {
    currentParent = element
    stack.push(element)
  } else {
    closeElement(element)
  }
}

简化后的satrt并不复杂,我们梳理下其实现

  1. 通过标签及属性数据创建节点AST
{
  type: 1,
  tag,
  attrsList: attrs,
  attrsMap: makeAttrsMap(attrs),
  rawAttrsMap: {},
  parent,
  children: []
}
  1. 执行preTransforms中暴露的函数,而preTransforms其实是收集了baseOptionsmodules中相关的函数,这点和以前分析的vue渲染中的节点更新是相似的。

  2. 执行不同的process函数,通过函数名其实就可以发现,process是对一些指令如forif之类的做进一步处理的。

  3. 前面我们定义了stack用于保存当前创建的节点栈,在创建之后将进其推入,并且将currentParent指向节点。

对于单个节点,我们来看看start前后的数据对比

image.png

image.png

end

 const element = stack[stack.length - 1]
  // pop stack
  stack.length -= 1
  currentParent = stack[stack.length - 1]
  closeElement(element)

end的主代码很简单,就是将刚才start中推入的节点推出,同时更新currentParent,此时表示当前节点标签已经闭合且处理完毕。closeElement则是会做一些额外的校验及调用之类的,这边不作分析。

chars

chars (text: string, start: number, end: number) {
  const children = currentParent.children
  //...

  if (text) {
    let res
    let child: ?ASTNode
    if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
      child = {
        type: 2,
        expression: res.expression,
        tokens: res.tokens,
        text
      }
    }

    if (child) {
      children.push(child)
    }
  }
}

chars用于处理文本信息,主要的是调用parseText对文本中的字符串进行解析,提取其中的变量。我们来看看其实现

export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if (!tagRE.test(text)) {
    return
  }

  // 1
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue

  // 2
  while ((match = tagRE.exec(text))) {
    index = match.index
    // push text token
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }

  // 3
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }

  // 4
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

parseText的实现也并不复杂,有点像parseHTML,实际就是将文本节点进一步token化处理

  1. 定义了一些输出变量以及遍历文本需要的临时变量

  2. 循环匹配文本,通过匹配{{}}(当然如果传递其它delimiters会有所不同)来提取遍历,同时通过匹配位置判断其前面是否还有字符串,有则一并提取。当然匹配的变量会添加_s()。不用多想_s()实际是会在后面用于渲染时执行的函数。

  3. 进行结尾处理,也就是{{name}}xxxx这样情况下的xxx文本。

  4. 将提取的表达式及token返回,我们来看看其输入输出值的区别。

image.png

image.png

comment

最后再看看注释节点的处理,就是使用对应的节点变量存储text,非常简单

comment (text: string, start, end) {
  if (currentParent) {
    const child: ASTText = {
      type: 3,
      text,
      isComment: true
    }
    currentParent.children.push(child)
  }
}

其它

我们在前面分析了parse的主要流程,感觉内容不算复杂。但实际parse中是包含很多内容的,因为我们跳过了很多指令处理的逻辑如v-ifv-forv-prev-slotv-eslev-elseifv-model等。它们的处理逻辑主要在各自的process函数中,我们将v-for作为例子来分析下其处理

processFor

export function processFor (el: ASTElement) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const res = parseFor(exp)
    if (res) {
      extend(el, res)
    }
  }
}

processFor的主要逻辑就是通过节点的attrsMap判断是否存在v-for指令,如果存在就进一步解析其值。将解析结果合并到element

我们再来看看parseFor

export function parseFor (exp: string): ?ForParseResult {
  const inMatch = exp.match(forAliasRE)
  if (!inMatch) return
  const res = {}
  res.for = inMatch[2].trim()
  const alias = inMatch[1].trim().replace(stripParensRE, '')
  const iteratorMatch = alias.match(forIteratorRE)
  if (iteratorMatch) {
    res.alias = alias.replace(forIteratorRE, '').trim()
    res.iterator1 = iteratorMatch[1].trim()
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim()
    }
  } else {
    res.alias = alias
  }
  return res
}

parseFor的逻辑实际就是解析开发者定义的如item in list将其分割处理并返回其对象,内容存在属性aliasfor等属性中。

我们来看看其处理前后的节点

image.png

image.png

AST

我们再拉看看我们的模板最终生成的AST代码

<div>
  <div v-if="isShow" @click="doSomething" :class="activeClass">text{{name}}{{value}}text</div>
  <div v-for="item in 10"></div>
</div>
const ast = parse(template.trim(), options)

image.png

总结

本篇文章分析了vue编译的第一步,将template编译成AST。发现对比babel那种对于JS代码的AST生成实际是简单不少的,其原因在于将模板的解析主要是按顺序匹配标签及属性即可,而对于代码的解析要考虑的东西就特别多,尤其是得考虑语法逻辑的处理。后面我们将继续分析编译的第二步AST的转化