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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 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

相关文章
|
14天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
117 64
|
14天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
|
14天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
24 8
|
13天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
17 1
|
13天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
25 1
|
14天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
22天前
|
JavaScript 索引
Vue 3.x 版本中双向数据绑定的底层实现有哪些变化
从Vue 2.x的`Object.defineProperty`到Vue 3.x的`Proxy`,实现了更高效的数据劫持与响应式处理。`Proxy`不仅能够代理整个对象,动态响应属性的增删,还优化了嵌套对象的处理和依赖追踪,减少了不必要的视图更新,提升了性能。同时,Vue 3.x对数组的响应式处理也更加灵活,简化了开发流程。
|
1月前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
53 1
|
17天前
|
JavaScript 前端开发 API
从Vue 2到Vue 3的演进
从Vue 2到Vue 3的演进
31 0
|
17天前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
45 0