大家都知道浏览器和服务器是通过HTTP协议传输数据的,而HTTP协议是纯文本协议,那么浏览器是如何将服务器传输的HTML字符串解析成真实的DOM元素的,也就是我们常说的生成DOM Tree,最近我们学习到了状态机的概念,于是想出了一个实现innerHTML函数的一个函数的想法,这是一个小练习。
功能原型
我们实现下面的函数,参数是DOM元素和HTML字符串,HTML字符串转换成真实的DOM元素,append是在参数一传入的DOM元素中。
函数html(元素,htmlstring) {//1。词法分析//2。正在解析//3。解释和执行}正如我在上面的评论中所指出的,这一步分为三个部分,即词法分析、语法分析以及解释和执行。
词汇分析
词法分析是一个特别重要和核心的部分,它的具体任务是将字符流转化为标记流。
词法分析通常有两种方案,一种是状态机,一种是正则表达式,两者是等价的。随便选你喜欢的。我们在这里选择状态机。
首先,我们需要确定令牌的类型。这里不考虑复杂的情况,因为我们只学习原理,不能像浏览器那样有很强的容错能力。除了容错,自封闭节点、标注和CDATA节点暂时不考虑。
接下来,输入主题,假设我们有以下节点信息,我们将分离哪个令牌?
p class=' a ' data=' js ' test element/p对于上面的节点信息,我们可以拆分以下标记
Start标记:p属性标记:class='a' text节点:test element end标记:/p状态机的原理,遍历整个HTML字符串,每次读取一个字符,都会做出一个决定(下一个字符处于哪个状态),这个决定与当前状态有关,这样读取过程就会得到一个又一个完整的令牌,这些令牌会记录在我们最终需要的令牌中。
万事开头难。我们首先要确定我们一开始可能处于哪种状态,也就是确定一个启动函数。在此之前,我们简单地封装了词法分析类,如下所示
函数HTMlCollabalParser(HTMlString,TokenHandler){ this . token=[];this . token=[];这个。htmlstring=htmlstringthis。tokenhandler=tokenhandler}简要解释上述每个属性
令牌:令牌的每个字符令牌:存储令牌html字符串:令牌处理程序:令牌处理程序:令牌处理程序。每次我们得到一个Token,我们就已经可以执行流解析了。我们可以很容易地知道,字符串要么以普通文本开始,要么以开始,因此开始代码如下
html词汇化arser . prototype . Start=function(c){ if(c===' '){ this . token . push(c)return this . tagstate } else { return this . textstate(c)} } Start处理相对简单。如果是字符,则表示开始标记或结束标记。因此,我们需要下一个字符信息来确定它是什么样的令牌,所以我们返回tagState函数进行重新判断。否则,我们会将其视为文本节点,并返回到文本状态函数。
然后分别展开tagState和textState函数。TagState根据下一个字符判断是进入开始标记状态还是结束标记状态。如果是/,则表示结束标记,否则为开始标记。textState用于处理每个文本节点字符。遇到它意味着获得了一个完整的文本节点标记。代码如下
html词典式标记器. prototype.tagState=函数(c){ this . token . push(c)if(c==='/'){ return This . endtagstate } else { return This . starttagstate } } html词典式标记器. prototype.textState=函数(c){ if(c==='){ This . emittoken(' text ',This。令牌。联接(“”)这个。token=[]返回这个。start (c)} else {this。令牌。按(c)返回这个。textstate } }我们在这里第一次遇到的函数有emitToken、startTagState和endTagState。
EmitToken用于将生成的完整令牌存储在Token中,参数为令牌类型和值。
StartTagState用于处理开始标记。这里有三种情况
如果下一个字符是字母,则确定它仍然处于开始标记状态并遇到空格,然后确定开始标记状态结束,然后也确定开始标记状态结束,但是下一步是处理新的节点信息。endTagState用于处理结束标记,结束标记中没有属性,所以只有两种情况。如果下一个字符是字母,则确定它仍然处于结束标记状态,这也被确定为结束标记状态结束,但是下一步是处理新的节点信息逻辑。
html词汇处理程序This . token handler . prototype . emittoken=function(type,value) { var res={ type,The value } This . token . push(RES)//streams . token handler This . token handler(RES)} html词汇服务器. prototype . starttagstate=function(c){ if(c . match(/[a-zA-Z]/)){ This . token . push(c . tolowercase())返回This . starttagstate { if(c==' '){ This . emittoken(' startTag ',This token。令牌。联接(“”)这个。token=[]返回这个。start}}最后,只需要处理属性标签,也就是上面看到的attrState函数也处理三种情况
如果是字母、单引号、双引号或等号,则仍处于属性标签状态。如果遇到空格,意味着属性标签状态结束。如果遇到,则认为属性标签状态结束。接下来,如下所示启动一个新的节点信息代码
htmldicalparser . prototype . attrstate=function(c){ if(c . match(/[a-zA-Z ' '=]/)){ this . token . push(c)返回This . attrstate } if(c===' '){ This . emit token(' attr ',This . token . join('))This . token=[]返回This . attrstate } if(c===' '){ This . emit token(' attr ',This。令牌。联接(“”)这个。token=[]返回这个。start}}最后,我们提供了一个解析函数和一个getOutPut函数,这两个函数可能会用来获取结果,所以不用担心,直接编码就可以了。
html词典alparser . prototype . parse=function(){ var state=this . start;for(this . HTML string . split(“”)的var c){ state=state . bind(this)(c)} } htmlaccalparser . prototype . getoutput=function(){ return this . token }接下来,只需测试/pp类=“a”data=“js”test parallel element的/p HTML字符串,输出结果为
看起来结果不错,然后进入解析步骤
语法分析
首先需要考虑两种情况,一种是有多个根元素,另一种是只有一个根元素。
有两种类型的节点,文本节点和普通节点,所以我们声明两种数据结构。
函数元素(标记名){this。标记名=tagnamethis。attr={}这个。childnodes=[]}函数文本(值){this。value=value | |''}目标:在元素之间建立父子关系。因为真正的DOM结构是父子关系,所以我在这里开始练习的时候,把childNodes属性的处理放在startTag token里面,把isEnd属性添加到Element里面,真的很蠢,很复杂,很难实现。
仔细考虑DOM结构,token也是顺序的。合理使用栈数据结构,这个问题就变得简单了。子节点在endTag中处理。具体逻辑如下
如果是开始标记令牌,直接推一个新元素如果是结束标签令牌,则表示当前节点处理完成,此时出栈一个节点,同时将该节点归入栈顶元素节点的子节点属性,这里需要做个判断,如果出栈之后栈空了,表示整个节点处理完成,考虑到可能有平行元素,将元素推到斯塔克斯。如果是属性令牌,直接写入栈顶元素的属性属性如果是文本令牌,由于文本节点的特殊性,不存在有子节点、属性等,就认定为处理完成。这里需要做个判断,因为文本节点可能是根级别的,判断是否存在栈顶元素,如果存在直接压入栈顶元素的子节点属性,不存在推到斯塔克斯。代码如下
函数超文本标记语言语法分析器(){这个。stack=[]this。stacks=[]} HTMl语法分析器。原型。getoutput=function(){返回这个。stacks }//一开始搞复杂了,合理利用基本数据结构真是一件很酷炫的事超文本标记语言语法分析器。原型。接收输入=函数(令牌){ var stack=this。堆栈if(令牌。type==' startTag '){ stack。推送(新元素(令牌。价值。substring(1)))} else if(token)。type==' attr '){ var t=token。价值。split('='),key=t[0],value=t[1].替换(/'|'/g ' ')堆栈[stack.length - 1]。attr[key]=value } else if(token。type==' Text '){ if(堆栈。长度){ stack[stack。长度-1]。子节点。推送(新文本(令牌。value))} else { this。斯塔克斯。推送(新文本(令牌。value))} } else if(token)。type==' end tag '){ var parsedTag=stack。pop()(堆栈。长度){ stack[stack。长度-1]。子节点。push(parsedTag)} } else { this。斯塔克斯。push(parsedTag)} }简单测试如下:
没啥大问题哈
解释执行
对于上述语法分析的结果,可以理解成虚拟域结构了,接下来就是映射成真实的多姆,这里其实比较简单,用下递归即可,直接上代码吧
函数vdomToDom(数组){ var RES=[]for(数组的let项){ RES . push(handleDom(item))} return RES }函数handleDom(项){ if(Element的项实例){ var Element=document。createelement(项。标记名)用于(让输入项。attr){元素。setattribute(键,项。attr[key])} if(item。子节点。length){ for(让I=0;一、项目。子节点。长度;I){元素。appendchild(handleDom)(项。child nodes[I])} } return element } else if(Text的项实例){ return document。createtextnode(项。值)} }实现函数
上面三步骤完成后,来到了最后一步,实现最开始提出的函数
函数html(元素,htmlString) { //parseHTML var语法解析器=新HTMl语法解析器()var词汇解析器=新HTMl词汇解析器(htmlString,语法解析器。接收输入。绑定(语法解析器))词汇解析器。解析器()var dom=vdomToDom(语法解析器。getoutput())var fragment=document。createdocumentfragment()DOM。foreach(item={ fragment。appendchild(item)})元素附录子(片段).三个不同情况的测试用例简单测试下
html(文档。getelementbyid(' app '),' p class='a' data='js '测试并列元素的/pp class='a' data='js '测试并列元素的/p ')html(文档。getelementbyid(' app '),'测试差异你好呀,我测试一下没有深层元素的/div ')html(文档。getelementbyid(' app '),' div class='div'p class='p '测试一下嵌套很深的span class='span'p的子元素/span/PSP同级别/span/div ')声明:简单测试下都没啥问题,本次实践的目的是对数字正射影像图这一块通过词法分析和语法分析生成数字正射影像图树有一个基本的认识,所以细节问题肯定还是存在很多的。
总结
其实在了解了原理之后,这一块代码写下来,并没有太大的难度,但却让我很兴奋,有两个成果吧
了解并初步实践了一下状态机数据结构的魅力代码已经基本都列出来了,想跑一下的童鞋也可以克隆这个回购:domtree
总结
以上是边肖介绍的用原生JS实现innerHTML功能的例子的详细说明,希望对大家有所帮助。如果你有任何问题,请给我留言,边肖会及时回复你。非常感谢您对我们网站的支持!