Vue3编译器 第一步Template转AST(上)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: Vue3编译器 第一步Template转AST(上)

theme: fancy

highlight: a11y-light

编译器的第一步是将模板字符串解析为抽象语法树(AST)。这个AST表示模板的结构和层次关系,它包含了模板中的标签、属性、文本内容等等,并且将它们组织成一个树状结构

下面给出了将模板"<div><p></p><span></span>Hi,{ {message}}</div>"转化成AST的主要结构

  • type 用来表示插值,文本,元素等类型。type:'Root'是默认添加的根节点
  • children 子节点
  • tag 元素的标签名
  • props 标签上的属性这里没有给出

这个结构和VNode很像,注意区分。

const content = ;
{
    
    
  type: "Root",
  children: [
    {
    
    
      type: "Element",
      tag: "div",
      children: [
        {
    
    
          type: "Element",
          tag: "p",
          children: [
          ],
        },
        {
    
    
          type: "Element",
          tag: "span",
          children: [
          ],
        },
        {
    
    
          type: "Text",
          content: "Hi,",
        },
        {
    
    
          type: "Interpolation",
          content: "message",
        },
      ],
    },
  ],
}

下面来介绍一下解析思路

先来创建一个parse函数,接受content(模板字符串)作为参数。

  1. 首先创建一个上下文对象context,为了在之后的递归中共享这个对象的使用。
    function parse(content) {
         
         
    const context = {
         
          source: content };
    return {
         
         
     type: "Root",
     children: parseChild(context),
    };
    }
    
  2. parseChild是执行递归操作的核心函数。可以理解为:对于树的下一级调用这个函数,再根据子节点的类型调用不同的方法进行处理
  3. 很明显parseChild会返回一个数组类型,进入while循环,它需要一个停止判断,这个isEnd待会再讲。
  4. 进入循环,如果以<开头并且第二个元素是一个字母,表示将用parseElement处理一个标签元素。如果第二个元素是/,需要处理标签结束。parseTag负责处理其中的标签头/尾。
  5. 当以{ { 开头表示处理插值parseInterpolation,其他情况当成文本处理parseText

另外补充一下,< div>< /div>,这种前面有空格的是不合法的,但是后面有空字符是合法的例如 :<div >

function parseChild(context) {
   
   
  const nodes = [];
  while (!isEnd(context)) {
   
   
    let node;
    if (context.source[0] === "<") {
   
   
      if (/[a-z]/i.test(context.source[1])) {
   
   
        node = parseElement(context);
      } else if (context.source[1] === "/") {
   
   
          parseTag(context, "End");
      }
    } else if (context.source.startsWith("{
   
   {")) {
   
   
      node = parseInterpolation(context);
    } else {
   
   
      node = parseText(context);
      }
  }
  return nodes;
}

用图表示就是:
企业微信截图_16980475415818.png

下面讲讲parseElement(处理标签)

  1. parseTag用来处理标签名,第二个参数用来区分处理的是开始还是结束标签
  2. 处理完开始标签,意味着可以进行下一个递归。即调用parseChild,将它的返回值赋给element.children

    function parseElement(context, ancestors) {
         
         
    const element = parseTag(context, "Start");
    element.children = parseChild(context, ancestors);
    return element;
    }
    

    parseTag的实现

  3. advanceBy只是为了消费字符。因为数据已经被记录。

  4. 当type为'End',即结束标签不需要返回,直接调用 advanceBy将结束标签消费。
  5. 里面这个正则很重要,不熟悉正则的可以看看。
  /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source);
 这个正则的意思是匹配以`<`开头,有一个,或者没有`/`
 把后面的小括号当成一个整体,/i忽略大小写
 再来看小括号里面的:*作用于[^\t\r\n\f />],表示零个或多个
 也就是表示匹配一个字母后面跟着零个或多个除制表符、回车符、换行符、进纸符、斜杠 `/` 或尖括号 `>` 之外的字符
  1. 对于这个match正则处理<div>结果为[<div,div,...],处理</div>结果为[</div,div,...],得到的tag就是标签名了。
  2. exec 方法会返回一个数组,数组的第一个元素是与整个正则表达式匹配的文本,接下来的元素是与每个捕获组匹配的文本,捕获组(小括号里的内容)就是匹配文本的子串
function parseTag(context, type) {
   
   
  const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source);
  const tag = match[1];
  advanceBy(context, type === "Start" ? 2 + tag.length : 3 + tag.length);
  return type === "Start"
    ? {
   
   
        type: "Element",
        tag,
      }
    : null;
}

function advanceBy(context, nums) {
   
   
  context.source = context.source.slice(nums);
}

parseInterpolation的实现

  1. 找到插值的结束符号}}对应的下标。
  2. 很容易可以把其中的内容截取下来,再消费字符
function parseInterpolation(context) {
   
   
  const index = context.source.indexOf("}}");
  const content = context.source.slice(2, index);
  advanceBy(context, index + 2);
  return {
   
   
    type: "Interpolation",
    content,
  };
}

parseText的实现

  1. 只需要比较离{ { ,还是<近,获取文本内容,再消费字符即可
    function parseText(context) {
         
         
    const indexI = context.source.indexOf("{
         
         {");
    const indexE = context.source.indexOf("<");
    const index = Math.min(indexI, indexE);
    const content = context.source.slice(0, index);
    advanceBy(context, content.length);
    return {
         
         
     type: "Text",
     content,
    };
    }
    

最后思考一下循环的终止条件即实现isEnd函数,它返回一个布尔值。
while 循环应该要遇到父级节点的结束标签才会停止

所以应该维护一个父节点栈ancestors,判断一下在剩余字符中能够找到最新的栈节点的标签名对应的结束标签。另一种结束情况是字符全都被消费。

function isEnd(context, ancestors) {
   
   
  const s = context.source;
  if (ancestors.length !== 0) {
   
   
    const tag = ancestors[ancestors.length - 1]?.tag;
    if (tag === s.slice(2, 2 + tag.length)) return true;
  }
  return !s;
}

因为需要维护ancestors,需要稍稍修改一下

  1. 在parse函数中给parseChildren再传入一个[],初始化ancestors
  2. parseChilrenparseElement都需要传入这个参数。
  3. parseElement中处理完开始标签之后,将parseTag的返回值push到栈里,执行完parseChildren也就是意味着递归结束,再退栈即可

另外还需要考虑文本模式对解析的影响,默认是DATA。比如:

<title>标签、<textarea> 标签,当解析器遇到这两个标签时,会切换到 RCDATA 模式,此时会将当前字符 < 作为普通字符处理,然后继续处理后面的字符。由此可知,在 RCDATA 状 态下,解析器不能识别标签元素。这里了解一下即可,下面给出了这几种模式对应的表格

模式 能否解析标签 是否支持 HTML 实体
DATA Y Y

RCDATA|N|Y
RAWTEXT|N|N
CDATA|N|N

所以需要在parseChild进行模式的判断

function parseChild(context, ancestors) {
   
   
//省略
  while (!isEnd(context, ancestors)) {
   
   
    let node;
    // 只有 DATA 模式和 RCDATA 模式才支持插值节点的解析
    if (context.mode === "DATA" || context.mode === "RCDATA") {
   
   
      // 只有 DATA 模式才支持标签节点的解析
      if (context.mode === "DATA" && context.source[0] === "<") {
   
   
       //省略
    }
  }
//省略
}

接下来处理没有结束标签的情况,并能给出提示具体是哪个标签的没有结束标签

  • 首先改造一下isEnd,这种情况下会造成死循环,因为永远找不到对应的结束标签。
  • 现在的判断条件改成遍历父节点栈,只要找到有与之相对应的结束标签则结束这个循环
  • parseElement也需要做处理,例如<div><p></div>
  1. 第一次进入parseChildren,进入while,使用parseElement处理标签,ancestors.push({type:Element,tag:div}),剩余字符<p></div>
  2. 进入递归parsechildren,再进入parseElement,ancestors.push({type:Element,tag:p}),剩余字符,进入while判断,为flase,退出循环。
  3. ancestors.push(),剩余字符对应标签是div,而element.tag是p,所以就知道这个标签缺少结束标签。
//省略代码
 ancestors.pop();
  if (context.source.startsWith(`</${
     
     element.tag}`)) {
   
   
    parseTag(context, "End");
  } else {
   
   
    // 缺少闭合标签
    console.error(`${
     
     element.tag} 标签缺少闭合标签`);
  }
  return element;

下一篇会在此基础上再完善一些内容,并且会解析标签上的属性props

相关文章
|
21天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
18天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
41 7
|
19天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
39 3
|
18天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
36 1
|
18天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
39 1
|
21天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
21天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
24天前
|
JavaScript API
vue3知识点:ref函数
vue3知识点:ref函数
30 2
|
24天前
|
API
vue3知识点:reactive函数
vue3知识点:reactive函数
26 1
|
24天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
24 0