【手写 Vue2.x 源码】第十五篇 - 生成 ast 语法树 - 构造树形结构

news2024/11/29 7:58:19

一,前言

上篇,主要介绍了生成 ast 语法树 - 模板解析部分

使用正则对 html 模板进行解析和处理,匹配到模板中的标签和属性

本篇,生成 ast 语法树 - 构造树形结构


二,构建树形结构

1,需要描述什么

前面提到将 html 模板构造成为 ast 语法树,使用 js 树形结构来描述 html 语法

对于一个 html 模板来说,主要有以下几点需要被描述和记录:

  • 标签
  • 属性
  • 文本
  • html 结构的层级关系,即父子关系

2,如何构建父子关系

简单 demo:

<div><sapn></span><p></p></div>

基于 html 标签特点:都是成对出现的,开始标签 + 结束标签为一组,如:和

基于这个特点,可以借助栈型数据结构来构建父子关系

[div,span]        span 是 div 的儿子
[div,span,span]   span 标签闭合,span 出栈,将 span 作为 div 的儿子
[div,p]           p 是 div 的儿子
[div,p,p]         p 标签闭合,p 出栈,将 p 作为 div 的儿子(即 p 和 span 是同层级的兄弟)
...略

3,ast 节点元素的数据结构

// 构建父子关系
function createASTElement(tag, attrs, parent) {
  return {
    tag,  // 标签名
    type:1, // 元素
    children:[],  // 儿子
    parent, // 父亲
    attrs // 属性
  }
}

4,处理开始标签

处理开始标签的逻辑

  • 取当前栈中最后一个标签作为父节点,并创建 ast 节点入栈,
  • 如果是第一个节点就自动成为整棵树的根节点

代码实现

// 处理开始标签,如:[div,p]
function start(tag, attrs) {
  // 取栈中最后一个标签,作为父节点
  let parent = stack[stack.length-1];
  // 创建当前 ast 节点
  let element = createASTElement(tag, attrs, parent);
  // 第一个作为根节点
  if(root == null) root = element;
  // 如果存在父亲,就父子相认(为当前节点设置父亲;同时为父亲设置儿子)
  if(parent){
    element.parent = parent;
    parent.children.push(element)
  }
  stack.push(element)
}

5,处理结束标签

处理结束标签的逻辑

  • 抛出栈中最后一个标签(即与当前结束标签成对的开始标签)
  • 验证栈中抛出的标签是否与当前结束标签成对

代码实现

// 处理结束标签
function end(tagName) {
  console.log("发射匹配到的结束标签-end,tagName = " + tagName)
  // 从栈中抛出结束标签
  let endTag = stack.pop();
  // check:抛出的结束标签名与当前结束标签名是否一直
  // 开始/结束标签的特点是成对的,当抛出的元素名与当前元素名不一致是报错
  if(endTag.tag != tagName)console.log("标签出错")
}

6,处理文本

处理文本的逻辑

  • 取当前栈中最后一个标签作为父节点
  • 删除文本中可能存在的空白字符
  • 无需入栈,直接将文本绑定为父节点的儿子

代码实现

// 处理文本(文本中可能包含空白字符)
function text(chars) {
  console.log("发射匹配到的文本-text,chars = " + chars)
  // 找到文本的父亲:文本的父亲,就是当前栈中的最后一个元素
  let parent = stack[stack.length-1];
  // 删除空格文本中的可能存在的空白字符:将空格替换为空
  chars = chars.replace(/\s/g, ""); 
  if(chars){
    // 绑定父子关系
    parent.children.push({
      type:2, // type=2 表示文本类型
      text:chars,
    })
  }
}

三,代码重构

1,模块化

将 parserHTML 逻辑进行封装,并提取为单独 js 文件:

src/compile/parser.js#parserHTML

// src/compile/parser.js


// 匹配标签名:aa-xxx
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// 命名空间标签:aa:aa-xxx
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 匹配标签名(索引1):<aa:aa-xxx
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 匹配标签名(索引1):</aa:aa-xxxdsadsa> 
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// 匹配属性(索引 1 为属性 key、索引 3、4、5 其中一直为属性值):aaa="xxx"、aaa='xxx'、aaa=xxx
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配结束标签:> 或 />
const startTagClose = /^\s*(\/?)>/;
// 匹配 {{   xxx    }} ,匹配到 xxx
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g

export function parserHTML(html) {
  console.log("***** 进入 parserHTML:将模板编译成 AST 语法树 *****")
  let stack = [];
  let root = null;
  // 构建父子关系
  function createASTElement(tag, attrs, parent) {
    return {
      tag,  // 标签名
      type:1, // 元素类型为 1
      children:[],  // 儿子
      parent, // 父亲
      attrs // 属性
    }
  }
  // 开始标签,如:[div,p]
  function start(tag, attrs) {
    console.log("发射匹配到的开始标签-start,tag = " + tag + ",attrs = " + JSON.stringify(attrs))
    // 遇到开始标签,就取栈中最后一个,作为父节点
    let parent = stack[stack.length-1];
    let element = createASTElement(tag, attrs, parent);
    // 还没有根节点时,作为根节点
    if(root == null) root = element;
    if(parent){ // 父节点存在
      element.parent = parent;  // 为当前节点设置父节点
      parent.children.push(element) // 同时,当前节点也称为父节点的子节点
    }
    stack.push(element)
  }
  // 结束标签
  function end(tagName) {
    console.log("发射匹配到的结束标签-end,tagName = " + tagName)
    // 如果是结束标签,就从栈中抛出
    let endTag = stack.pop();
    // check:抛出的结束标签名与当前结束标签名是否一直
    if(endTag.tag != tagName)console.log("标签出错")
  }
  // 文本
  function text(chars) {
    console.log("发射匹配到的文本-text,chars = " + chars)
    // 文本直接放到前一个中 注意:文本可能有空白字符
    let parent = stack[stack.length-1];
    chars = chars.replace(/\s/g, ""); // 将空格替换为空,即删除空格
    if(chars){
      parent.children.push({
        type:2, // 文本类型为 2
        text:chars,
      })
    }
  }
  /**
   * 截取字符串
   * @param {*} len 截取长度
   */
  function advance(len) {
    html = html.substring(len);
    console.log("截取匹配内容后的 html:" + html)
    console.log("===============================")
  }

  /**
   * 匹配开始标签,返回匹配结果
   */
  function parseStartTag() {
    console.log("***** 进入 parseStartTag,尝试解析开始标签,当前 html: " + html + "*****")
    // 匹配开始标签,开始标签名为索引 1
    const start = html.match(startTagOpen);
    if(start){// 匹配到开始标签再处理
      // 构造匹配结果,包含:标签名 + 属性
      const match = {
        tagName: start[1],
        attrs: []
      }
      console.log("html.match(startTagOpen) 结果:" + JSON.stringify(match))
      // 截取匹配到的结果
      advance(start[0].length)
      let end;  // 是否匹配到开始标签的结束符号>或/>
      let attr; // 存储属性匹配的结果
      // 匹配属性且不能为开始的结束标签,例如:<div>,到>就已经结束了,不再继续匹配该标签内的属性
      //    attr = html.match(attribute)  匹配属性并赋值当前属性的匹配结果
      //    !(end = html.match(startTagClose))   没有匹配到开始标签的关闭符号>或/>
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        // 将匹配到的属性,push到attrs数组中,匹配到关闭符号>,while 就结束
        console.log("匹配到属性 attr = " + JSON.stringify(attr))
        // console.log("匹配到属性 name = " + attr[1] + "value = " + attr[3] || attr[4] || attr[5])
        match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] })
        advance(attr[0].length)// 截取匹配到的属性 xxx=xxx
      }
      // 匹配到关闭符号>,当前标签处理完成 while 结束,
      // 此时,<div id="app" 处理完成,需连同关闭符号>一起被截取掉
      if (end) {
        console.log("匹配关闭符号结果 html.match(startTagClose):" + JSON.stringify(end))
        advance(end[0].length)
      }

      // 开始标签处理完成后,返回匹配结果:tagName标签名 + attrs属性
      console.log(">>>>> 开始标签的匹配结果 startTagMatch = " + JSON.stringify(match))
      return match;
    }
    console.log("未匹配到开始标签,返回 false")
    console.log("===============================")
    return false;
  }

  // 对模板不停截取,直至全部解析完毕
  while (html) {
    // 解析标签和文本(看开头是否为<)
    let index = html.indexOf('<');
    if (index == 0) {// 标签
      console.log("解析 html:" + html + ",结果:是标签")
      // 如果是标签,继续解析开始标签和属性
      const startTagMatch = parseStartTag();// 匹配开始标签,返回匹配结果
      if (startTagMatch) {  // 匹配到了,说明是开始标签
        // 匹配到开始标签,调用start方法,传递标签名和属性
        start(startTagMatch.tagName, startTagMatch.attrs)
        continue; // 如果是开始标签,就不需要继续向下走了,继续 while 解析后面的部分
      }
      // 如果开始标签没有匹配到,有可能是结束标签 </div>
      let endTagMatch;
      if (endTagMatch = html.match(endTag)) {// 匹配到了,说明是结束标签
        // 匹配到开始标签,调用start方法,传递标签名和属性
        end(endTagMatch[1])
        advance(endTagMatch[0].length)
        continue; // 如果是结束标签,也不需要继续向下走了,继续 while 解析后面的部分
      }
    } else {// 文本
      console.log("解析 html:" + html + ",结果:是文本")
    }

    // 文本:index > 0 
    if(index > 0){
      // 将文本取出来并发射出去,再从 html 中拿掉
      let chars = html.substring(0,index) // hello</div>
      text(chars);
      advance(chars.length)
    }
  }
  console.log("当前 template 模板,已全部解析完成")
  return root;
}

2,导入模块

src/compile/index.js中,引入parserHTML:

// src/compile/index.js

import { parserHTML } from "./parser";

export function compileToFunction(template) {
  console.log("***** 进入 compileToFunction:将 template 编译为 render 函数 *****")
  // 1,将模板变成 AST 语法树
  let ast = parserHTML(template);
  console.log("解析 HTML 返回 ast 语法树====>")
  console.log(ast)
}

3,测试 ast 构建逻辑

<div><p>{{message}}<sapn>HelloVue</span></p></div>

image.png

打印执行结果,如图:

div
    p
        {{message}}
        span
            HelloVue

四,结尾

本篇,生成 ast 语法树 - 构造树形结构部分

  • 基于 html 特点,使用栈型数据结构记录父子关系
  • 开始标签,结束标签及文本的处理方式
  • 代码重构及ast 语法树构建过程分析

下一篇,ast 语法树生成 render 函数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/152793.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文本相似度

传统方法 基于TF-IDF、BM25、Jaccord、SimHash、LDA等算法抽取两个文本的词汇、主题等层面的特征&#xff0c;然后使用机器学习模型&#xff08;LR, xgboost&#xff09;训练分类模型优点&#xff1a;可解释性较好缺点&#xff1a;依赖人工寻找特征&#xff0c;泛化能力一般&a…

linux有用技巧:使用ntfs-3g挂载ntfs设备

1.几种文件系统的比较 (1)在linux系统中支持以下文件系统&#xff1a; Ext2 第二扩展文件系统&#xff08;简称 ext2&#xff09;很多年前就已经成为 GNU/Linux 默认的文件系统了。ext2 取代了扩展文件系统(这是 “第二代” 的前身)。它纠正了它前身的一些错误并突破了…

【工具篇】41 # 常用可视化工具集整理(完结)

说明 【跟月影学可视化】学习笔记。 可视化场景主体需求 主体需求示例绘制基本图形根据数据绘制一些二维、三维的几何图形&#xff0c;它们不一定是完整的图表&#xff0c;通常是具有灵活性和视觉冲击力的小图形&#xff0c;例如粒子效果绘制基础图表通常是指绘制常见的饼图…

让开发人员偷懒的正则表达式

正则表达式是一种基于特殊模式符号系统的文本处理系统。简而言之&#xff0c;它为程序员提供了轻松处理和验证字符串的能力。它代表了DRY&#xff08;Dont Repeat Yourself&#xff09;原则的实现&#xff0c;在几乎所有支持的语言中&#xff0c;正则表达式模式根本不会改变形式…

【信管7.2】质量保证与质量控制

质量保证与质量控制项目质量管理的两个核心过程就是实施质量保证和控制质量。关于它们两个的区别我们在上一课已经讲了&#xff0c;实施质量保证其实保证的是过程&#xff0c;就是我们的开发过程是不是遵循了质量计划&#xff0c;也就是说&#xff0c;这是保证过程有质量的一个…

视频录制软件有哪些?4款录制视频软件,免费下载

对于不了解录屏的用户&#xff0c;肯定会有这些视频是如何制作出来的疑惑&#xff1f;其实录制视频一件很容易的事情&#xff0c;只需要找到一个可以在录视频的软件就可以了。哪有什么录制视频软件可以录制的呢&#xff1f;下面小编给大家分享4款就可以录制视频的软件&#xff…

ORB-SLAM3算法和代码学习—跟踪恒速运动模型TrackWithMotionModel()

0总述 跟踪运动模型核心思想&#xff1a;假设在极短时间内&#xff0c;相机的运动相同。设相邻时刻的三帧图像分别为k-2帧&#xff0c;k-1帧&#xff0c;k帧&#xff0c;则认为k-2帧到k-1帧相机的运动T_delta1和k-1帧到k帧相机的运动T_delta2相等&#xff0c;如下图所示。当相…

K8S概述及用途

K8S概述 1.K8S说明 K8S(Kubernetes) 是一个可移植的、可扩展的开源平台&#xff0c;用于管理容器化的工作负载和服务&#xff0c;可促进声明式配置和自动化。 Kubernetes 拥有一个庞大且快速增长的生态系统。Kubernetes 的服务、支持和工具广泛可用。 Kubernetes 这个名字源…

海量数据存储面临的问题

海量数据存储面临的问题海量数据存储面临的问题成本高性能低可扩展性差如何实现分布式文件存储如何支撑高效率的计算分析如何解决海量数据存储的问题如何解决海量数据文件查询便捷问题如何解决大文件传输效率慢的问题如何解决硬件故障数据丢失问题如何解决用户查询视角统一规整…

pyhon把程序打包为whl

首先需要一个库&#xff1a;setuptools如果是conda环境的话&#xff0c;这个包是自带的&#xff0c;不需要另外安装。首先把需要打包的py文件放在一个文件夹内&#xff08;我的文件夹名为coordTrans&#xff0c;记住这个名字&#xff0c;后面要用&#xff09;。同时&#xff0c…

dll修复工具下载,dll修复工具注意事项

Dll文件的缺失相信很多人都遇见过吧&#xff0c;只要缺失了一个这样的dll文件&#xff0c;我们的游戏或者软件程序就启动不了了&#xff0c;所以我们就需要去修复它&#xff0c;目前修复有几种方法&#xff0c;最简单的&#xff0c;最适合电脑小白的&#xff0c;那就是dll修复工…

Vue3——第四章(响应式基础:reactive、ref)

一、用reactive()声明响应式状态 我们可以使用 reactive() 函数创建一个响应式对象或数组&#xff1a; 响应式对象其实是 JavaScript Proxy&#xff0c;其行为表现与一般对象相似。不同之处在于 Vue 能够跟踪对响应式对象属性的访问与更改操作。 要在组件模板中使用响应式状…

java后端第六阶段:SpringMVC

1、Spring IoC&#xff08;Inversion of Controller&#xff09;控制反转 使用对象时&#xff0c;由主动new产生对象转换为由外部提供对象&#xff0c;此过程中对象中创建控制权由程序转移到外部&#xff0c;此思想称为控制反转 Spring技术对IoC思想进行了实现 Spring提供了一…

第四十九讲:神州路由器IPv6 OSPFv3和RIPng路由的配置

神州路由器支持IPv6的内部网关路由协议常用的有OPSFv3和RIPng。 实验拓扑图如下所示 配置要求&#xff1a;在两台路由器上启用IPv6 routing&#xff0c; 在接口上配子ipv6协议后&#xff0c;通过配置RIPng和OSPFv3相关命令&#xff0c;观察学习到的路由。 配置步骤&#xff1…

产品试用记录

某产品试用记录 还可以选屏哦

【PWA学习】3. 让你的 WebApp 离线可用

引言 PWA 其中一个令人着迷的能力就是离线(offline)可用 即使在离线状态下&#xff0c;依然可以访问的 PWA离线只是它的一种功能表现而已&#xff0c;具体说来&#xff0c;它可以&#xff1a; 让我们的Web App在无网(offline)情况下可以访问&#xff0c;甚至使用部分功能&#…

Redis哨兵模式搭建

以下配置机器部署ip为 a、b、c&#xff0c;其中a为master节点 需提前创建 /app/user/oms/redis/data 目录 1.1上传 redis-5.0.5.zip 到对应目录&#xff0c;解压 unzip redis-5.0.5.zip # 生成 redis-5.0.5 目录 1.2 修改配置文件 maxclients 10000 #20000 &#xff0…

接口测试实战| GET/POST 请求区别详解

在日常的工作当中&#xff0c;HTTP 请求中使用最多的就是 GET 和 POST 这两种请求方式。深度掌握这两种请求方式的原理以及异同之处&#xff0c;也是之后做接口测试一个重要基础。GET、POST 的区别总结请求行的 method 不同&#xff1b;POST 可以附加 div&#xff0c;可以支持 …

概率论【离散型二维变量与连续性二维变量(下)】--猴博士爱讲课

6.连续型二维变量&#xff08;下&#xff09; 1/7 求边缘分布函数 边缘概率密度 边缘概率密度 2/7 求边缘密度函数 边缘概率密度 3/7 判断连续型二维变量的独立性 F(x,y) Fx(X) * Fy(Y)那么X、Y互相独立 f(x,y) fx(X) * fy(Y)那么X、Y互相独立 这种题目带入验证就可以了 先求…

百度举办首个人机共创大会,最强技术天团邀约全球开发者

1月10日&#xff0c;百度举办Create AI开发者大会&#xff08;下称“Create大会”&#xff09;。作为首个“人机共创大会”&#xff0c;AIGC&#xff08;利用AI技术自动生成内容的生产方式&#xff09;技术被深度应用&#xff0c;创造、搭建、连接了多个科技感爆棚的数字化演讲…