【手写 Vue2.x 源码】第十四篇 - 生成 ast 语法树 - 模板解析

news2024/11/28 13:34:49

一,前言

上篇,主要介绍了生成 ast 语法树-正则说明部分,涉及以下几个点:

  • 简要说明了 HTML模板的解析方式
  • 对模板解析相关正则说明和测试

本篇,生成 ast 语法树-代码实现


二,模板解析

模板解析的方式:对模板不停截取,直至全部解析完毕,可以使用 while 循环

上篇说到,标签还是文本,要看内容开头的第一个字符是否为尖角号 < :

  • 如果是尖角号,说明是标签;
  • 如果不是尖角号,说明是文本
// src/compiler/index.js#parserHTML

function parserHTML(html) {
  while(html){
    // 解析标签or文本,看第一个字符是否为尖角号 < 
    let index = html.indexOf('<');
    if(index == 0){
      console.log("是标签")
    } else{
      console.log("是文本")
    }
  }
}

image.png

1,parseStartTag 解析开始标签

包含尖叫号 < 的情况,有可能是开始标签 <div>,但也有可能是结束标签 </div>

所以当为标签时,先使用正则匹配开始标签;如果没有匹配成功,再使用结束标签进行匹配

parseStartTag方法:匹配开始标签,返回匹配结果

备注:匹配结果的索引 1 可以得到标签名,属性后续解析

// src/compiler/index.js#parserHTML

function parserHTML(html) {

  /**
   * 匹配开始标签,返回匹配结果
   */
  function parseStartTag() {
    // 匹配开始标签,开始标签名为索引 1
    const start = html.match(startTagOpen);
    // 构造匹配结果,包含标签名和属性
    const match = {
      tagName:start[1],
      attrs:[]
    }
    console.log("match结果:" + match)
  }

  // 对模板不停截取,直至全部解析完毕
  while (html) {
    // 解析标签和文本(看开头是否为<)
    let index = html.indexOf('<');
    if (index == 0) {
      console.log("解析 html:" + html + ",结果:是标签")
      // 如果是标签,继续解析开始标签和属性
      const startTagMatch = parseStartTag();// 匹配开始标签,返回匹配结果
      if (startTagMatch) {  // 匹配到开始标签
        continue; // 如果是开始标签,无需执行下面逻辑,继续下次 while 解析后续内容
      }
      // 没有匹配到开始标签,此时有可能为结束标签 </div>,继续处理结束标签
      if (html.match(endTag)) {// 匹配到结束标签
        continue; // 如果是结束标签,无需执行下面逻辑,继续下次 while 解析后续内容
      }
    } else {
      console.log("解析 html:" + html + ",结果:是文本")
    }
  }
}

调试标签 match 结果:

image.png

上边开始标签解析完成后,标签将被截掉:

<!-- 解析前 -->
<div id=app>{{message}}</div>
<!-- 解析后 -->
 id=app>{{message}}</div>

为了实现对已经匹配到标签进行截取,

需要 advance 方法:前进,即截取至当前已解析位置

// src/compiler/index.js#parserHTML

function parserHTML(html) {

  /**
   * 截取字符串
   * @param {*} len 截取长度
   */
  function advance(len){
    html = html.substring(len);
  }
  
  /**
   * 匹配开始标签,返回匹配结果
   */
  function parseStartTag() {
    // 匹配开始标签,开始标签名为索引 1
    const start = html.match(startTagOpen);
    // 构造匹配结果,包含标签名和属性
    const match = {
      tagName:start[1],
      attrs:[]
    }
    console.log("match 结果:" + match)
    // 截取匹配到的结果
    advance(start[0].length)
    console.log("截取后的 html:" + html)
  }
	...
}

调试查看截取后的html:

image.png

2,解析开始标签中的属性

 id="app">{{message}}</div>

开始标签中,可能存在多个属性,此部分需要循环进行处理

// src/compiler/index.js#parserHTML#parseStartTag

function parseStartTag() {
  const start = html.match(startTagOpen);
  const match = {
    tagName: start[1],
    attrs: []
  }
  console.log("match 结果:" + match)
  // 截取匹配到的结果
  advance(start[0].length)
  console.log("截取后的 html:" + html)
  
  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 就结束
    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) {
    advance(end[0].length)
  }

  // 开始标签处理完成后,返回匹配结果:tagName 标签名 + attrs属性
  return match
}

3,开始标签的处理步骤

<div id="app" a=1 b=2>
  
处理过程:

  <开头,说明是标签:可能是开始标签,也可能是结束标签
  匹配正则 startTagOpen,获取属性名和属性
  匹配“<div”            剩    “ id="app" a=1 b=2>”
  匹配“ id="app"”       剩    “ a=1 b=2>”
  匹配“ a=1”            剩    “ b=2>”
  匹配“ b=2”            剩    “>”
  匹配“>”

匹配到 “>”,while 循环就终止了

至此,开始标签就解析完成了

4,处理开始标签、结束标签和文本

继续,将开始标签的状态(开始标签、结束标签、文本标签)发射出去

编写三个发射状态的方法,分别用于向外发射开始标签、结束标签、文本标签

// src/compiler/index.js#parserHTML#start
// src/compiler/index.js#parserHTML#end
// src/compiler/index.js#parserHTML#text

// 开始标签
function start(tagName, attrs) {
  console.log("start", tagName, attrs)
}
// 结束标签
function end(tagName) {
  console.log("end", tagName)
}
// 文本标签
function text(chars) {
  console.log("text", chars)
}

当匹配到开始标签、结束标签、文本时,将数据发送出去

// src/compiler/index.js#parserHTML#parseStartTag

/**
  * 匹配开始标签,返回匹配结果
  */
function parseStartTag() {
  const start = html.match(startTagOpen);
  if(start){
    const match = {
      tagName: start[1],
      attrs: []
    }
    advance(start[0].length)
    let end;
    let attr;
    while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
      match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] })
      advance(attr[0].length)
    }
    if (end) {
      advance(end[0].length)
    }
    return match
  }
  return false;
}

while (html) {
  let index = html.indexOf('<');
  if (index == 0) {
    console.log("解析 html:" + html + ",结果:是标签")
    // 如果是标签,继续解析开始标签和属性
    const startTagMatch = parseStartTag();
    console.log("开始标签的匹配结果 startTagMatch = " + JSON.stringify(startTagMatch))
    
    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 解析后面的部分
    }
  }
  
  if(index > 0){	// 文本
    // 将文本取出来并发射出去,再从 html 中拿掉
    let chars = html.substring(0,index) // hello</div>
    text(chars);
    advance(chars.length)
  }
}

至此,已经拿到了标签名、属性等,但此时还不是树形结构,没有形成一棵树


三,测试一个较复杂的模板解析

<body>
  <div id="app" a='1' b=2 > <p>{{message}} <span>Hello Vue</span></p></div>
  <script src="./vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return { message:  "Brave" }
      },
    }); 
  </script>
</body>

打印结果:

进入 state.js - initData,数据初始化操作
***** 进入 $mount,el = #app*****
获取真实的元素,el = [object HTMLDivElement]
options 中没有 render , 继续取 template
options 中没有 template, 取 el.outerHTML = <div id="app" a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>
***** 进入 compileToFunction:将 template 编译为 render 函数 *****
***** 进入 parserHTML:将模板编译成 AST 语法树*****
解析 html:<div id="app" a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: <div id="app" a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>*****
html.match(startTagOpen) 结果:{"tagName":"div","attrs":[]}
截取匹配内容后的 html: id="app" a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配到属性 attr = [" id=\"app\"","id","=","app",null,null]
截取匹配内容后的 html: a="1" b="2"> <p>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配到属性 attr = [" a=\"1\"","a","=","1",null,null]
截取匹配内容后的 html: b="2"> <p>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配到属性 attr = [" b=\"2\"","b","=","2",null,null]
截取匹配内容后的 html:> <p>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配关闭符号结果 html.match(startTagClose):[">",""]
截取匹配内容后的 html: <p>{{message}} <span>Hello Vue</span></p></div>
===============================
>>>>> 开始标签的匹配结果 startTagMatch = {"tagName":"div","attrs":[{"name":"id","value":"app"},{"name":"a","value":"1"},{"name":"b","value":"2"}]}
发射匹配到的开始标签-start,tagName = div,attrs = [{"name":"id","value":"app"},{"name":"a","value":"1"},{"name":"b","value":"2"}]
解析 html: <p>{{message}} <span>Hello Vue</span></p></div>,结果:是文本
发射匹配到的文本-text,chars =  
截取匹配内容后的 html:<p>{{message}} <span>Hello Vue</span></p></div>
===============================
解析 html:<p>{{message}} <span>Hello Vue</span></p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: <p>{{message}} <span>Hello Vue</span></p></div>*****
html.match(startTagOpen) 结果:{"tagName":"p","attrs":[]}
截取匹配内容后的 html:>{{message}} <span>Hello Vue</span></p></div>
===============================
匹配关闭符号结果 html.match(startTagClose):[">",""]
截取匹配内容后的 html:{{message}} <span>Hello Vue</span></p></div>
===============================
>>>>> 开始标签的匹配结果 startTagMatch = {"tagName":"p","attrs":[]}
发射匹配到的开始标签-start,tagName = p,attrs = []
解析 html:{{message}} <span>Hello Vue</span></p></div>,结果:是文本
发射匹配到的文本-text,chars = {{message}} 
截取匹配内容后的 html:<span>Hello Vue</span></p></div>
===============================
解析 html:<span>Hello Vue</span></p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: <span>Hello Vue</span></p></div>*****
html.match(startTagOpen) 结果:{"tagName":"span","attrs":[]}
截取匹配内容后的 html:>Hello Vue</span></p></div>
===============================
匹配关闭符号结果 html.match(startTagClose):[">",""]
截取匹配内容后的 html:Hello Vue</span></p></div>
===============================
>>>>> 开始标签的匹配结果 startTagMatch = {"tagName":"span","attrs":[]}
发射匹配到的开始标签-start,tagName = span,attrs = []
解析 html:Hello Vue</span></p></div>,结果:是文本
发射匹配到的文本-text,chars = Hello Vue
截取匹配内容后的 html:</span></p></div>
===============================
解析 html:</span></p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: </span></p></div>*****
未匹配到开始标签,返回 false
===============================
发射匹配到的结束标签-end,tagName = span
截取匹配内容后的 html:</p></div>
===============================
解析 html:</p></div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: </p></div>*****
未匹配到开始标签,返回 false
===============================
发射匹配到的结束标签-end,tagName = p
截取匹配内容后的 html:</div>
===============================
解析 html:</div>,结果:是标签
***** 进入 parseStartTag,尝试解析开始标签,当前 html: </div>*****
未匹配到开始标签,返回 false
===============================
发射匹配到的结束标签-end,tagName = div
截取匹配内容后的 html:
===============================
当前 template 模板,已全部解析完成

四,结尾

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

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

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

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

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

相关文章

22. 听说你想要用爬虫采集我的手机号?哎 ~ 我展示用的是图片

本篇博客我们实现图片渲染手机号码案例&#xff0c;用于防止爬虫直接采集文字信息。 爬虫训练场 本案例实现的效果如下所示 文章目录bootstrap5 实现名片样式卡片补充数据生成逻辑生成用户 5 个汉字的昵称调用头像 API&#xff0c;生成图片将手机号码生成图片bootstrap5 实现名…

菜鸡二次封装element中table表单

实现效果如下封装的table表单<template><el-table:span-method"arraySpanMethod":header-cell-style"rowClass":cell-style"cellStyle":data"tableData"style"width: 100%; height: 100%">//暂无数据展示<te…

汤姆斯的天堂梦(C++,Dijkstra)

题目描述 汤姆斯生活在一个等级为 000 的星球上。那里的环境极其恶劣&#xff0c;每天 121212 小时的工作和成堆的垃圾让人忍无可忍。他向往着等级为 NNN 的星球上天堂般的生活。 有一些航班将人从低等级的星球送上高一级的星球&#xff0c;有时需要向驾驶员支付一定金额的费…

【跟月影学可视化】学习笔记 41 篇(完结)

说明 【跟月影学可视化】专栏学习笔记。 个人学习笔记源码&#xff1a;https://github.com/kaimo313/visual-learning-demo 一共做了 162 个学习示例以及 41 篇博客学习笔记&#xff0c;要深入学习该课程请支持正版&#xff0c;个人笔记仅供参考。 笔记目录 【图形基础篇…

什么是无源相干定位系统?

无源定位&#xff08;Passive Localization&#xff09;不通过发射信号来探测目标的位置&#xff0c;而是接收目标的有意、无意辐射或反射信号来实现对侦察目标的探测、定位与追踪。接收的信号可以是目标直接辐射的信号&#xff0c;也可以是外辐射源照射到目标后反射或散射的信…

网站关键词怎么优化排名(网站关键词通常可以选择哪些词)

网站核心关键词的选取需要具备哪些条件 在对网站优化的过程中&#xff0c;肯定少不了对网站关键词的选取&#xff0c;关键词的选择又是网站优化中十分重要的一步&#xff0c;那么网站在选择关键词的过程中需要遵循哪些原则呢&#xff1f;关于这个问题老张带你了解一下。 1、首…

wav文件格式分析与详解

wav文件格式分析与详解WAV文件是在PC机平台上很常见的、最经典的多媒体音频文件,最早于1991年8月出现在Windows 3.1操作系统上,文件扩展名为WAV,是WaveFom的简写,也称为波形文件,可直接存储声音波形,还原的波形曲线十分逼真。WAV文件格式简称WAV格式是一种存储声音波形的数字音…

Wijmo 2022 v2 JavaScript UI Crack

Wijmo 2022 v2 采集 by Ω578867473 添加对 Angular 14 和 React 18 的支持以及对 FlexGrid 和 FlexChart 的改进。特征 Angular 14 支持——您今天就可以开始将 Angular 14 应用程序与 Wijmo 结合使用。Wijmo 提供了大量快速、灵活的 Angular 组件&#xff0c;每个组件都有丰富…

【学Vue就跟玩一样】组件-非单文件组件的使用

一&#xff0c;什么是组件实现应用中局部功能代和资源的集合&#xff08;简单来说就是将html&#xff0c;js&#xff0c;css&#xff0c;资源整合起来的一个小盒子&#xff09;理解&#xff1a;用来实现局部(特定)功能效果的代码集合为什么&#xff1a;一个界面的功能很复杂作用…

SD卡损坏了怎么办?sd卡恢复,80%的用户都试过这些方法

SD卡作为一种外部存储设备&#xff0c;多用在数据相机、监控、手机、无人机等设备中&#xff0c;可以帮我们保存很多数据。 但是SD卡也跟其他设备一样&#xff0c;容易发生数据丢失的情况。如果SD卡损坏了&#xff0c;或者我们把里面的数据误删或者格式化&#xff0c;sd卡恢复…

MySQL--什么情况下不建议使用join查询

关于join 当需要查询两个表的交集、并集等数据时&#xff0c;除了嵌套子查询的方式外&#xff0c;还可以使用join的方式提升性能。对于MySQL的join语句&#xff0c;需要两个最基础的“角色”&#xff1a;主表即驱动表&#xff0c;关联表即驱动表。join描述的就是驱动表与被驱动…

云服务器怎样搭建静态网站?

先买好域名和云服务器&#xff0c;然后把云服务器的ip地址和域名解析到一起。 然后登陆云服务器&#xff0c;安装Nginx 我的软件环境是 CentOS 1、安装 Nginx 在 CentOS 上&#xff0c;可直接使用 yum 来安装 Nginx&#xff08;安装时间稍微有点长&#xff0c;安装过程中代码会…

Linux应用编程---10.信号量

Linux应用编程—10.信号量 ​ 信号量用于任务间的同步!简单来理解&#xff0c;信号量是一个被内核维护的整数&#xff0c;这个整数一般是“大于等于零”的&#xff0c;我们对这个信号量的操作一般为&#xff1a;将信号量设置一个值、发布(加上一个信号量)、消耗(减去一个信号量…

LINUX提权之计划任务提权篇

前言 今天给大家带来的是计划任务提权&#xff0c;说起定时任务对于linux很熟悉的小伙伴一定不会陌生&#xff0c;但你有没有想过可以通过定时任务来进行权限提升的操作&#xff0c;本文会根据该知识点进行展开&#xff0c;同时给大家介绍一个用于探测漏洞的工具使用方法&…

线程通信:生产者消费者问题

问题 1.生产者&#xff08;Producer&#xff09;将产品给店员&#xff08;Clerk&#xff09;&#xff0c;而消费者&#xff08;Customer&#xff09;从店员处取走产品&#xff0c;店员一次只能持有固定数量的产品&#xff08;比如&#xff1a;20&#xff09; &#xff0c;如果生…

实验 1 MATLAB 图像处理基础

一、实验目的1. 熟悉启动和退出 MATLAB 的方法。2. 熟悉 MATLAB 命令窗口的组成。3. 掌握 MATLAB 基本绘图函数和图像处理函数的使用。4. 掌握图像内插和灰度图像的集合运算。二、实验例题1. 求下列表达式的值(1) (2) 答&#xff1a;(1)y1exp(2)/2*sin(35*pi/180)y1 2.1191(2)方…

索尼数字人研究:画质超逼真,面部表情与身体动作保持协调

近年来&#xff0c;3D动捕、数字虚拟人等技术受到越来越多关注&#xff0c;它不仅可以应用于电影场景&#xff0c;游戏、社交等领域也开始采用。相比于过去高成本、高门槛的全身动捕技术&#xff0c;现在制作基于动捕的虚拟人越来越容易&#xff0c;不需要过高的成本或是专业技…

Linux 可加载内核模块剖析

Linux 就是通常所说的单内核&#xff08;monolithic kernel&#xff09;&#xff0c;即操作系统的大部分功能都被称为内核&#xff0c;并在特权模式下运行。 它与微型内核不同&#xff0c;后者只把基本的功能&#xff08;进程间通信 [IPC]、调度、基本的输入/输出 [I/O] 和内存…

Hudi系列1:Hudi介绍

文章目录一. 什么是Hudi二. 发展历史三. Hudi 功能和特性四. Hudi 基础架构五. 使用公司六. 小结参考:一. 什么是Hudi Apache Hudi&#xff08;发音“hoodie”&#xff09;是下一代流数据湖平台。Apache Hudi将核心仓库和数据库功能直接带到数据湖中。Hudi提供了表&#xff0c…

6.6 工具-ELK安装

目录 6.6.1 Elasticsearch安装 6.6.1.1 安装 6.6.1.1.1 window 6.6.1.1.2 Linux 6.6.1.2 问题 6.6.1.2.1 问题一 6.6.1.2.2 问题二 6.6.2 Logstash安装 6.6.2.1 安装 6.6.2.1.1 window 6.6.2.1.2 Linux 6.6.2.2 问题 6.6.2.2.1 问题一 6.6.3 Kibana 6.6.3.1 安装…