vue2-编译之生成AST

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 前言在上篇文章我们分析了编译中parse的部分代码,也就是parseHTML的实现。在parseHTML中通过逐字匹配将template进行了初步解析。现在我们继续分析在parseHTML中输出的结果是如何被parse进行使用的。以此结束完整parse流程的分析。

「这是我参与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前后的数据对比

51.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返回,我们来看看其输入输出值的区别。


52.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等属性中。


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

53.png54.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)
复制代码


55.png


总结


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


相关文章
|
4月前
|
自然语言处理 JavaScript 前端开发
Vue 3的编译器是什么
【8月更文挑战第15天】Vue 3的编译器是什么
49 0
|
7月前
|
JavaScript API
Vue3.3 编译宏
Vue3.3 编译宏
125 0
|
7月前
|
JavaScript 安全 容器
Vue3 + setup + TypeScript: 构建现代、类型安全的Vue应用的关键技巧总结
当使用 setup 的时候,组件直接引入就可以了,不需要再自己手动注册
|
7月前
|
前端开发 JavaScript 测试技术
Vue3+Vite+TypeScript常用项目模块详解(下)
现在无论gitee还是github,越来越多的前端开源项目采用Vue3+Vite+TypeScript+Pinia+Elementplus+axios+Sass(css预编译语言等),其中还有各种项目配置比如eslint 校验代码工具配置等等,而我们想要进行前端项目的二次开发,就必须了解会使用这些东西,所以作者写了这篇文章进行简单的介绍。
145 0
|
7月前
|
JavaScript
Vega-Embed 在 Vue Typescript 项目中引入报错
Vega-Embed 在 Vue Typescript 项目中引入报错
80 0
|
机器学习/深度学习 编译器
Vue3编译器 第一步Template转AST(上)
Vue3编译器 第一步Template转AST(上)
|
移动开发 编译器
Vue3编译器 第一步Template转AST(下)
Vue3编译器 第一步Template转AST(下)
|
JavaScript 前端开发 编译器
🎖️使用 esbuild 简化 TypeScript 构建并跳过 tsc/tsx
JavaScript 生态系统一直在不断创新,最近的一位游戏规则改变者是 esbuild,这是一个极速的 JavaScript 和 TypeScript 打包器。
1071 0
|
JavaScript
TypeScript 安装(简单对比 JS)
TypeScript 安装(简单对比 JS)
69 0
|
缓存 JSON JavaScript
关于Vue3编译器的一些优化
Vue3 编译器 本章主要介绍Vue3编译器的作用,这个编译器是如何提高性能的,静态dom与动态dom的不同处理,缓存的使用以及块的作用。 致谢Vue Mastery非常好的课程,可以转载,但请声明源链接:文章源链接justin3go.com(有些latex公式某些平台不能渲染可查看这个网站)
115 0