[前端] vueparseHTML函数源码解析AST基本形成

2010 0
王子 2022-10-21 15:28:10 | 显示全部楼层 |阅读模式
目录

    AST(抽象语法树)?
      子节点
    Vue中是如何把html(template)字符串编译解析成AST
      解析html代码重新改造接着解析 html (template)字符串解析div



AST(抽象语法树)?

vue parseHTML函数解析器遇到结束标签
在上篇文章中我们已经把整个词法分析的解析过程分析完毕了。
例如有html(template)字符串:
  1. <div id="app">
  2.   <p>{{ message }}</p>
  3. </div>
复制代码
产出如下:
  1. {
  2. attrs: [" id="app"", "id", "=", "app", undefined, undefined]
  3. end: 14
  4. start: 0
  5. tagName: "div"
  6. unarySlash: ""
  7. }
  8. {
  9. attrs: []
  10. end: 21
  11. start: 18
  12. tagName: "p"
  13. unarySlash: ""
  14. }
复制代码
看到这不禁就有疑问?这难道就是AST(抽象语法树)??
非常明确的告诉你答案:No 这不是我们想要的AST,parse 阶段最终生成的这棵树应该是与如上html(template)字符串的结构一一对应的:
  1. ├── div
  2. │   ├── p
  3. │   │   ├── 文本
复制代码
如果每一个节点我们都用一个 javascript 对象来表示的话,那么 div 标签可以表示为如下对象:
  1. {
  2.   type: 1,
  3.   tag: "div"
  4. }
复制代码
子节点

由于每个节点都存在一个父节点和若干子节点,所以我们为如上对象添加两个属性:parent 和 children ,分别用来表示当前节点的父节点和它所包含的子节点:
  1. {
  2.   type: 1,
  3.   tag:"div",
  4.   parent: null,
  5.   children: []
  6. }
复制代码
同时每个元素节点还可能包含很多属性 (attributes),所以我们可以为每个节点添加attrsList属性,用来存储当前节点所拥有的属性:
  1. {
  2.   type: 1,
  3.   tag:"div",
  4.   parent: null,
  5.   children: [],
  6.   attrsList: []
  7. }
复制代码
按照以上思路去描述之前定义的 html 字符串,那么这棵抽象语法树应该长成如下这个样子:
  1. {
  2.   type: 1,
  3.   tag: "div",
  4.   parent: null,
  5.   attrsList: [],
  6.   children: [{
  7.       type: 1,
  8.       tag: "p",
  9.       parent: div,
  10.       attrsList: [],
  11.       children:[
  12.          {
  13.           type: 3,
  14.           tag:"",
  15.           parent: p,
  16.           attrsList: [],
  17.           text:"{{ message }}"
  18.          }
  19.        ]
  20.   }],
  21. }
复制代码
实际上构建抽象语法树的工作就是创建一个类似如上所示的一个能够描述节点关系的对象树,节点与节点之间通过 parent 和 children 建立联系,每个节点的 type 属性用来标识该节点的类别,比如 type 为 1 代表该节点为元素节点,type 为 3 代表该节点为文本节点。
这里可参考NodeType:https://www.w3school.com.cn/jsref/prop_node_nodetype.asp
回顾我们所学的 parseHTML 函数可以看出,他只是在生成 AST 中的一个重要环节并不是全部。 那在Vue中是如何把html(template)字符串编译解析成AST的呢?

Vue中是如何把html(template)字符串编译解析成AST

在源码中:
  1. function parse (html) {
  2.   var root;
  3.   parseHTML(html, {
  4.    start: function (tag, attrs, unary) {
  5.       // 省略...
  6.     },
  7.     end: function (){
  8.       // 省略...
  9.     }
  10.   })
  11.   return root
  12. }
复制代码
可以看到Vue在进行模板编译词法分析阶段调用了parse函数,parse函数返回root,其中root 所代表的就是整个模板解析过后的 AST,这中间还有两个非常重要的钩子函数,之前我们没有讲到的,options.start 、options.end。
接下来重点就来看看他们做了什么。

解析html

假设解析的html字符串如下:
  1. <div></div>
复制代码
这是一个没有任何子节点的div 标签。如果要解析它,我们来简单写下代码。
  1. function parse (html) {
  2.   var root;
  3.   parseHTML(html, {
  4.    start: function (tag, attrs, unary) {
  5.       var element = {
  6.         type: 1,
  7.         tag: tag,
  8.         parent: null,
  9.         attrsList: attrs,
  10.         children: []
  11.       }
  12.       if (!root) root = element
  13.     },
  14.     end: function (){
  15.       // 省略...
  16.     }
  17.   })
  18.   return root
  19. }
复制代码
如上: 在start 钩子函数中首先定义了 element 变量,它就是元素节点的描述对象,接着判断root 是否存在,如果不存在则直接将 element 赋值给 root 。当解析这段 html 字符串时首先会遇到 div 元素的开始标签,此时 start 钩子函数将被调用,最终 root 变量将被设置为:
  1. {
  2.   type: 1,
  3.   tag:"div",
  4.   parent: null,
  5.   children: [],
  6.   attrsList: []
  7. }
复制代码
html 字符串复杂度升级: 比之前的 div 标签多了一个子节点,span 标签。
  1. <div>
  2.   <span></span>
  3. </div>
复制代码
代码重新改造

此时需要把代码重新改造。
  1. function parse (html) {
  2.   var root;
  3.   var currentParent;
  4.   parseHTML(html, {
  5.    start: function (tag, attrs, unary) {
  6.       var element = {
  7.         type: 1,
  8.         tag: tag,
  9.         parent: null,
  10.         attrsList: attrs,
  11.         children: []
  12.       }
  13.       if (!root){
  14.         root = element;
  15.        }else if(currentParent){
  16.         currentParent.children.push(element)
  17.       }
  18.       if (!unary) currentParent = element
  19.     },
  20.     end: function (){
  21.       // 省略...
  22.     }
  23.   })
  24.   return root
  25. }
复制代码
我们知道当解析如上 html 字符串时首先会遇到 div 元素的开始标签,此时 start 钩子函数被调用,root变量被设置为:
  1. {
  2.   type: 1,
  3.   tag:"div",
  4.   parent: null,
  5.   children: [],
  6.   attrsList: []
  7. }
复制代码
还没完可以看到在 start 钩子函数的末尾有一个 if 条件语句,当一个元素为非一元标签时,会设置 currentParent 为该元素的描述对象,所以此时currentParent也是:
  1. {
  2.   type: 1,
  3.   tag:"div",
  4.   parent: null,
  5.   children: [],
  6.   attrsList: []
  7. }
复制代码
接着解析 html (template)字符串

接着解析 html (template)字符串,会遇到 span 元素的开始标签,此时root已经存在,currentParent 也存在,所以会将 span 元素的描述对象添加到 currentParent 的 children 数组中作为子节点,所以最终生成的 root 描述对象为:
  1. {
  2.   type: 1,
  3.   tag:"div",
  4.   parent: null,
  5.   attrsList: []
  6.   children: [{
  7.      type: 1,
  8.      tag:"span",
  9.      parent: div,
  10.      attrsList: [],
  11.      children:[]
  12.   }],
  13. }
复制代码
到目前为止好像没有问题,但是当html(template)字符串复杂度在升级,问题就体现出来了。
  1. <div>
  2. <span></span>
  3. <p></p>
  4. </div>
复制代码
在之前的基础上 div 元素的子节点多了一个 p 标签,到解析span标签的逻辑都是一样的,但是解析 p 标签时候就有问题了。
注意这个代码:
  1. if (!unary) currentParent = element
复制代码
在解析 p 元素的开始标签时,由于 currentParent 变量引用的是 span 元素的描述对象,所以p 元素的描述对象将被添加到 span 元素描述对象的 children 数组中,被误认为是 span 元素的子节点。而事实上 p 标签是 div 元素的子节点,这就是问题所在。
为了解决这个问题,就需要我们额外设计一个回退的操作,这个回退的操作就在end钩子函数里面实现。

解析div

这是一个什么思路呢?举个例子在解析div 的开始标签时:
  1. stack = [{tag:"div"...}]
复制代码
在解析span 的开始标签时:
  1. stack = [{tag:"div"...},{tag:"span"...}]
复制代码
在解析span 的结束标签时:
  1. stack = [{tag:"div"...}]
复制代码
在解析p 的开始标签时:
  1. stack = [{tag:"div"...},{tag:"p"...}]
复制代码
在解析p 的标签时:
这样的一个回退操作看懂了吗? 这就能保证在解析p开始标签的时候,stack中存储的是p标签父级元素的描述对象。
接下来继续改造我们的代码。
  1. function parse (html) {
  2.   var root;
  3.   var currentParent;
  4.   var stack = [];  
  5.   parseHTML(html, {
  6.    start: function (tag, attrs, unary) {
  7.       var element = {
  8.         type: 1,
  9.         tag: tag,
  10.         parent: null,
  11.         attrsList: attrs,
  12.         children: []
  13.       }
  14.       if (!root){
  15.         root = element;
  16.        }else if(currentParent){
  17.         currentParent.children.push(element)
  18.       }
  19.       if (!unary){
  20.           currentParent = element;
  21.           stack.push(currentParent);
  22.        }
  23.     },
  24.     end: function (){
  25.       stack.pop();
  26.       currentParent = stack[stack.length - 1]
  27.     }
  28.   })
  29.   return root
  30. }
复制代码
通过上述代码,每当遇到一个非一元标签的结束标签时,都会回退 currentParent 变量的值为之前的值,这样我们就修正了当前正在解析的元素的父级元素。
以上就是根据 parseHTML 函数生成 AST 的基本方式,但实际上还不完美在Vue中还会去处理一元标签,文本节点和注释节点等等。
接下来你是否迫不及待要进入到源码部分去看看了? 但Vue这块代码稍微复杂点,我们还需要有一些前期的预备知识。
parseHTML 函数源码解析 AST 预备知识
以上就是vue parseHTML 函数源码解析AST基本形成的详细内容,更多关于vue parseHTML 函数AST的资料请关注中国红客联盟其它相关文章!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行