接上文:

李李:parseHTML 函数源码解析(三)​zhuanlan.zhihu.com

在上篇文章中我们已经把整个词法分析的解析过程分析完毕了。

例如有html(template)字符串:

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

产出如下:

{attrs: [" id="app"", "id", "=", "app", undefined, undefined]
end: 14
start: 0
tagName: "div"
unarySlash: ""
}{attrs: []
end: 21
start: 18
tagName: "p"
unarySlash: ""
}

看到这不禁就有疑问? 这难道就是AST(抽象语法树)??

非常明确的告诉你答案:No 这不是我们想要的AST,parse 阶段最终生成的这棵树应该是与如上html(template)字符串的结构一一对应的:

├── div
│   ├── p
│   │   ├── 文本

如果每一个节点我们都用一个 javascript 对象来表示的话,那么 div 标签可以表示为如下对象:

{type: 1,tag: "div"
}

由于每个节点都存在一个父节点和若干子节点,所以我们为如上对象添加两个属性:parent 和 children ,分别用来表示当前节点的父节点和它所包含的子节点:

{type: 1,tag:"div",parent: null,children: []
}

同时每个元素节点还可能包含很多属性 (attributes),所以我们可以为每个节点添加attrsList属性,用来存储当前节点所拥有的属性:

{type: 1,tag:"div",parent: null,children: [],attrsList: []
}

按照以上思路去描述之前定义的 html 字符串,那么这棵抽象语法树应该长成如下这个样子:

{type: 1,tag: "div",parent: null,attrsList: [],children: [{type: 1,tag: "p",parent: div,attrsList: [],children:[{type: 3,tag:"",parent: p,attrsList: [],text:"{{ message }}"}]}],
}

实际上构建抽象语法树的工作就是创建一个类似如上所示的一个能够描述节点关系的对象树,节点与节点之间通过 parent 和 children 建立联系,每个节点的 type 属性用来标识该节点的类别,比如 type 为 1 代表该节点为元素节点,type 为 3 代表该节点为文本节点。

这里可参考NodeType:https://www.w3school.com.cn/jsref/prop_node_nodetype.asp

回顾我们所学的 parseHTML 函数可以看出,他只是在生成 AST 中的一个重要环节并不是全部。 那在Vue中是如何把html(template)字符串编译解析成AST的呢?

在源码中:

function parse (html) {var root;parseHTML(html, {start: function (tag, attrs, unary) {// 省略...},end: function (){// 省略...}}) return root
}

可以看到Vue在进行模板编译词法分析阶段调用了parse函数,parse函数返回root,其中root 所代表的就是整个模板解析过后的 AST,这中间还有两个非常重要的钩子函数,之前我们没有讲到的,options.start 、options.end。

接下来重点就来看看他们做了什么。

假设解析的html字符串如下:

<div></div>

这是一个没有任何子节点的div 标签。如果要解析它,我们来简单写下代码。

function parse (html) {var root;parseHTML(html, {start: function (tag, attrs, unary) {var element = {type: 1,tag: tag,parent: null,attrsList: attrs,children: []}if (!root) root = element},end: function (){// 省略...}}) return root
}

如上: 在start 钩子函数中首先定义了 element 变量,它就是元素节点的描述对象,接着判断root 是否存在,如果不存在则直接将 element 赋值给 root 。当解析这段 html 字符串时首先会遇到 div 元素的开始标签,此时 start 钩子函数将被调用,最终 root 变量将被设置为:

{type: 1,tag:"div",parent: null,children: [],attrsList: []
}

html 字符串复杂度升级: 比之前的 div 标签多了一个子节点,span 标签。

<div><span></span>
</div>

此时需要把代码重新改造。

function parse (html) {var root;var currentParent;parseHTML(html, {start: function (tag, attrs, unary) {var element = {type: 1,tag: tag,parent: null,attrsList: attrs,children: []}if (!root){root = element;}else if(currentParent){currentParent.children.push(element)}if (!unary) currentParent = element},end: function (){// 省略...}}) return root
}

我们知道当解析如上 html 字符串时首先会遇到 div 元素的开始标签,此时 start 钩子函数被调用,root变量被设置为:

{type: 1,tag:"div",parent: null,children: [],attrsList: []
}

还没完可以看到在 start 钩子函数的末尾有一个 if 条件语句,当一个元素为非一元标签时,会设置 currentParent 为该元素的描述对象,所以此时currentParent也是:

{type: 1,tag:"div",parent: null,children: [],attrsList: []
}

接着解析 html (template)字符串,会遇到 span 元素的开始标签,此时root已经存在,currentParent 也存在,所以会将 span 元素的描述对象添加到 currentParent 的 children 数组中作为子节点,所以最终生成的 root 描述对象为:

{type: 1,tag:"div",parent: null,attrsList: []children: [{type: 1,tag:"span",parent: div,attrsList: [],children:[]}],
}

到目前为止好像没有问题,但是当html(template)字符串复杂度在升级,问题就体现出来了。

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

在之前的基础上 div 元素的子节点多了一个 p 标签,到解析span标签的逻辑都是一样的,但是解析 p 标签时候就有问题了。

注意这个代码:

if (!unary) currentParent = element

在解析 p 元素的开始标签时,由于 currentParent 变量引用的是 span 元素的描述对象,所以p 元素的描述对象将被添加到 span 元素描述对象的 children 数组中,被误认为是 span 元素的子节点。而事实上 p 标签是 div 元素的子节点,这就是问题所在。

为了解决这个问题,就需要我们额外设计一个回退的操作,这个回退的操作就在end钩子函数里面实现。

这是一个什么思路呢?举个例子在解析div 的开始标签时:

stack = [{tag:"div"...}]

在解析span 的开始标签时:

stack = [{tag:"div"...},{tag:"span"...}]

在解析span 的结束标签时:

stack = [{tag:"div"...}]

在解析p 的开始标签时:

stack = [{tag:"div"...},{tag:"p"...}]

在解析p 的标签时:

这样的一个回退操作看懂了吗? 这就能保证在解析p开始标签的时候,stack中存储的是p标签父级元素的描述对象。

接下来继续改造我们的代码。

function parse (html) {var root;var currentParent;var stack = [];  parseHTML(html, {start: function (tag, attrs, unary) {var element = {type: 1,tag: tag,parent: null,attrsList: attrs,children: []}if (!root){root = element;}else if(currentParent){currentParent.children.push(element)}if (!unary){currentParent = element;stack.push(currentParent);} },end: function (){stack.pop();currentParent = stack[stack.length - 1]}}) return root
}

通过上述代码,每当遇到一个非一元标签的结束标签时,都会回退 currentParent 变量的值为之前的值,这样我们就修正了当前正在解析的元素的父级元素。

以上就是根据 parseHTML 函数生成 AST 的基本方式,但实际上还不完美在Vue中还会去处理一元标签,文本节点和注释节点等等。

接下来你是否迫不及待要进入到源码部分去看看了? 但Vue这块代码稍微复杂点,我们还需要有一些前期的预备知识。

接下文:

李李:parseHTML 函数源码解析(五) AST 预备知识​zhuanlan.zhihu.com

html调用rpst 源码_parseHTML 函数源码解析(四) AST 基本形成相关推荐

  1. PHP 源码 —— is_array 函数源码分析

    is_array 函数源码分析 本文首发于 https://github.com/suhanyujie/learn-computer/blob/master/src/function/array/is ...

  2. is array php,PHP 源码 — is_array 函数源码分析

    php 中的 is_array php 中的 is_array,它的签名是 is_array ( mixed $var ) : bool 实现的源码 在\ext\standard\type.c中可以找 ...

  3. 【Android 逆向】ART 脱壳 ( dex2oat 脱壳 | aosp 中搜索 dex2oat 源码 | dex2oat.cc#main 主函数源码 )

    文章目录 前言 一.搜索 dex2oat 源码 二.dex2oat.cc#main 主函数源码 前言 在 [Android 逆向]ART 脱壳 ( DexClassLoader 脱壳 | exec_u ...

  4. 【Linux 内核 内存管理】物理分配页 ⑨ ( __alloc_pages_slowpath 慢速路径调用函数源码分析 | retry 标号代码分析 )

    文章目录 一.retry 标号代码分析 二.retry 标号完整代码 在 [Linux 内核 内存管理]物理分配页 ② ( __alloc_pages_nodemask 函数参数分析 | __allo ...

  5. 【Linux 内核 内存管理】物理分配页 ⑦ ( __alloc_pages_slowpath 慢速路径调用函数源码分析 | 判断页阶数 | 读取 mems_allowed | 分配标志位转换 )

    文章目录 一.__alloc_pages_slowpath 慢速路径调用函数 二.判断页阶数 三.读取进程 mems_allowed 成员 四.分配标志位转换 五.__alloc_pages_slow ...

  6. 【Linux 内核 内存管理】物理分配页 ⑧ ( __alloc_pages_slowpath 慢速路径调用函数源码分析 | 获取首选内存区域 | 异步回收内存页 | 最低水线也分配 | 直接分配 )

    文章目录 一.获取首选内存区域 二.异步回收内存页 三.最低水线也分配 四.直接分配内存 在 [Linux 内核 内存管理]物理分配页 ② ( __alloc_pages_nodemask 函数参数分 ...

  7. 【Linux 内核 内存管理】mmap 系统调用源码分析 ④ ( do_mmap 函数执行流程 | do_mmap 函数源码 )

    文章目录 一.do_mmap 函数执行流程 二.do_mmap 函数源码 调用 mmap 系统调用 , 先检查 " 偏移 " 是否是 " 内存页大小 " 的 & ...

  8. 【SA8295P 源码分析】22 - QNX Ethernet MAC 驱动 之 emac_entry / emac_attach 函数源码分析

    [SA8295P 源码分析]22 - QNX Ethernet MAC 驱动 之 emac_entry / emac_attach 函数源码分析 一.EMAC:libdevnp-emac-eth.so ...

  9. 【Linux 内核】实时调度类 ⑥ ( 实时调度类核心函数源码分析 | 插入进程到执行队列 | 从执行队列中选择优先级最高的进程 )

    文章目录 一.enqueue_task_rt 函数 ( 插入进程到执行队列 ) 二.pick_next_task_rt 函数 ( 从执行队列中选择优先级最高的进程 ) 本篇博客中 , 开始分析 str ...

  10. OpenCV resize函数源码解析——加速方法

    相信大家应该经常会用到OpenCV中的函数resize(),当我们想放大或者缩小图像的时候,会用到这个函数进行图像缩放,其中最核心的便是对图像的像素进行插值处理. 这里的插值interpolation ...

最新文章

  1. 综述|寻找自动驾驶中的关键场景
  2. 基于MATLAB的自由空间损耗模型的理论与仿真
  3. java中的CAS和原子类的实现
  4. angularAMD快速入门
  5. CF1612G Max Sum Array
  6. sklearn学习笔记之feature_selection(特征选择)
  7. Luogu4491 [HAOI2018]染色 【容斥原理】【NTT】
  8. 父与子一起学python3_父与子的编程之旅(与小卡特一起学Python第3版全彩印刷)/图灵程序设计丛书...
  9. J1939协议之通俗易懂----概述
  10. Mycat生产实践---分表分库案例
  11. 计算机设置新用户名和密码怎么设置路由器,怎么修改无线路由器密码和用户名【图】...
  12. html 中怎样显示enum,enum怎么读音发音
  13. 云服务器复现PointRCNN代码踩坑总结
  14. Spring boot启动报错ERROR 5208 --- [ restartedMain] o.s.b.d.LoggingFailureAnalysisReporter
  15. 安庆集团-冲刺日志(第三天)
  16. 【毒鸡汤】基层管理如果没有这些心态,难!
  17. 千万不要死于无知——心理状态
  18. 三种通信方式——单工、半双工和双工通信
  19. 图解Linux命令之--modprobe命令
  20. 《浅谈条形码技术在连锁超市中的应用》论文笔记(二)

热门文章

  1. 软件测试Homework03
  2. Eclipse-导入maven项目
  3. jQuery 效果函数
  4. 一个32岁入门的70后程序员给我的启示
  5. 【python】【multiprocessing】【Pool、pool.Pool、pool.ThreadPool】apply 和apply_async多进程有关时间的比较分析
  6. python爬虫之------每天给她(他)一个小故事啦啦啦啦
  7. 摄影测量学——解析法相对定向
  8. python 对文件夹的相关操作
  9. ENVI5.3.1使用Landsat 8影像进行NDVI计算实例操作
  10. 前端宽度一至显示宽度不一致_便利店装修注意事项,你确定不看看?