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.ucloud.cn/yun/127819.html
写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧 【Vue原理】Compile - 源码版 之 Parse 主要流程 本文难度较繁琐,需要耐心观看,如果你对 compile 源码暂时...
摘要:下面用具体代码进行分析。匹配不到那么就是开始标签,调用函数解析。如这里的转化为加上是为了的下一步转为函数,本文中暂时不会用到。再把转化后的内容进。 什么是AST 在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式。...
直接进入核心现在说说baseCompile核心代码: //`createCompilerCreator`allowscreatingcompilersthatusealternative //parser/optimizer/codegen,e.gtheSSRoptimizingcompiler. //Herewejustexportadefaultcompilerusingthede...
在说Vue parse源码之前,首先要了解周边的工具函数。 之前见过element元素节点四描述对象? varelement={ type:1, tag:tag, parent:null, attrsList:attrs, children:[] } 是用一个createASTElement函数,创建函数对象。 createASTElement函数 funct...
摘要:模板解析器原理本文来自深入浅出模板编译原理篇的第九章,主要讲述了如何将模板解析成,这一章的内容是全书最复杂且烧脑的章节。循环模板的伪代码如下截取模板字符串并触发钩子函数为了方便理解,我们手动模拟解析器的解析过程。 Vue.js 模板解析器原理 本文来自《深入浅出Vue.js》模板编译原理篇的第九章,主要讲述了如何将模板解析成AST,这一章的内容是全书最复杂且烧脑的章节。本文未经排版,真...
阅读 547·2023-03-27 18:33
阅读 732·2023-03-26 17:27
阅读 630·2023-03-26 17:14
阅读 591·2023-03-17 21:13
阅读 521·2023-03-17 08:28
阅读 1801·2023-02-27 22:32
阅读 1292·2023-02-27 22:27
阅读 2178·2023-01-20 08:28