vue parseHTML函数解析器遇到结束标签,在之前文章中已讲述完毕。
例如有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 属性用来标识该节点的类别。
这里可参考NodeType:https://www.w3school.com.cn/jsref/prop_node_nodetype.asp
现在我们总结所学 parseHTML 函数,只是在生成 AST 中的一个重要环节并非全部。 那在Vue中是如何把html(template)字符串编译解析成AST的呢?
Vue中是如何把html(template)字符串编译解析成AST
在源码中:
function parse (html) { var root; parseHTML(html, { start: function (tag, attrs, unary) { // 省略... }, end: function (){ // 省略... } }) return root }
接下来重点就来看看他们做了什么。parse函数返回root,其中root 所代表的就是整个模板解析过后的 AST,现在我们要用到另两个重要的钩子函数:options.start 、options.end。
下面进入Vue在进行模板编译词法分析阶段调用了parse函数,
解析html
假设解析的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)字符串
接着解析 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
这是一个什么思路呢?举个例子在解析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 变量的值为之前的值,这样我们就修正了当前正在解析的元素的父级元素。
原创文章,作者:网友投稿,如若转载,请注明出处:https://www.cloudads.cn/archives/3940.html