解析器的作用

解析器要实现的功能就是将模板解析成AST。AST类似vnode只是用JavaScript中的对象来描述一个节点。

解析器内部的运行原理

解析器中最主要的就是HTML解析器,它在解析HTML过程中会不断触发各种钩子函数。

parseHTML(template,{
    start(tag,attrs,unary){
        //三个参数分别是标签名,标签属性,是否是自闭合标签
        //每当解析到标签的开始位置触发的钩子函数
    },
    end(){
        //每当解析到标签的结束位置触发的钩子函数
    },
    chars(text){
        //每当解析到文本时触发的钩子函数
    },
    comment(text){
        //每当解析到注释时触发的钩子函数
    }
})

例如一个简单的例子

<div><p>Hello</p></div>

解析器从前往后解析,解析到第一个div标签,会触发一个开始标签的钩子函数start,然后解析到p又一次触发一次钩子函数start,然后解析到Hello这段文本,触发了文本钩子函数chars,然后解析到p的结束标签,触发标签结束的钩子函数end,然后解析到div的结束标签,触发标签结束的钩子函数end,解析完毕。在start钩子函数中构建元素类型的节点,在chars钩子函数构建文本类型的节点,在comment钩子函数中构建注释类型的节点。
AST节点具有父节点和子节点,但在创建节点时没有层级关系,所以创建完节点要来构建AST节点之间的层级关系,构建这个层级关系需要维护一个栈(stack),用栈来记录层级关系,层级关系可以理解为DOM的深度。
基于HTML解析器的逻辑,在每次触发钩子函数start时,把当前构建的节点推入栈中;每当触发钩子函数end时,从栈中弹出一个节点,这样就可以保证,每次触发钩子函数start时,栈的最后一个节点(栈顶)就是当前即将入栈的父节点当模板截取解析完,栈也空,就得到了一个完整的带层级关系的AST语法树,写明了每个节点的父节点、子节点以及其节点类型。

运行原理

解析HTML模板的过程就是循环的过程,用HTML模板字符串来循环,每轮循环都从模板中截取一小段字符串,重复以上过程,直到HTML模板被截取成一个空字符串的时候结束循环,解析完成。

function parseHTML(html,options){
    while(html){
        //截取模板字符串并触发钩子函数
    }
}

被截取的片段分为许多种类型:
1.开始标签,例如<div>
2.结束标签,例如</div>
3.HTML注释,例如<!-- 注释 -->
4.DOCTYPE,例如<!DOCTYPE html>
5.条件注释,例如<!--[if !IE]>-->我是注释<!--<![endif]>-->
6.文本,例如Hello

截取开始标签

1.判断是不是'<'开头,如果不是一定不是开始标签,所以不需要进行开始标签的截取,如果是的话还需要进一步的判断,因为结束标签和注释标签等都有可能是。
2.利用正则表达式来匹配开始标签的模板
3.匹配成功后需要将标签名、属性和自闭和标识解析出来。

解析标签属性

通常标签属性是可选的,可能存在也可能没有,所以首先要判断是否存在属性,如果存在,再进行截取,截取也是使用正则表达式的方式,每解析一个属性,则从原模板字符串中截取一个属性,如果剩余的模板依然符合标签属性的正则表达式,则说明还有剩余属性需要处理,则继续重复刚才执行的流程,直至剩余的模板不存在属性,也就是说剩余的模板字符串不符合正则表达式所预设的规则说明属性已经截取完毕。

解析自闭合标识

自闭合标签是没有子节点的,所以构建AST层级时,需要维护一个栈,而一个节点是否需要推入栈中,就可以使用自闭合标识来判断,通过正则匹配开始标签的结尾部分是否有“/”,例如
<input type="text" />

截取结束标签

结束标签截取只需要分辨出当前是否已经截取到结束标签,如果是,则触发end钩子函数即可。

截取注释

与截取结束标签相同,用正则来匹配剩余模板是否符合正则的规则,注释的钩子函数可以通过选项来配置,只有options,shouldKeepComment为真时,才会触发钩子函数,否则只截取模板,不触发钩子函数

截取条件注释合DOCTYPE

利用正则进行字符串匹配

截取文本

如果HTML模板的第一个字符不是"<",说明一定是文本。下一个"<"之前的全是文本,例如
我是文本</div>,直接使用substring从模板最开始截取到"<"之前的位置.如果找不到"<",说明全都是文本,然后触发钩子函数将截取出的文本放在参数中。
有一个特殊情况,就是文本中包含<符号的时候,1<2</div>,此时将1截取出来,剩余模板是<2</div>不属于任何被解析的类型,说明这个<是属于文本的,使用while来解决问题,如果剩余的模板不符合任何被解析的类型,那么就重复使用文本来解析,直到剩余模板符合被解析的类型为止。

纯文本内容元素的处理

script、style合textarea这三种元素叫做纯文本元素内容。解析它们的时候,会把这三种标签内包含的所有内容都当作文本处理。

使用栈维护DOM层级

之前提到HTML解析器内部来维护一个栈来维护DOM层级关系,同时,它还有另外一个作用:检测出HTML标签是否正确闭合,当没有正常闭合会在非生产环境下的控制台打印警告提示。

整体逻辑

export function parseHTML(html,options){
    while(html){
        if(!lastTag||!isPlainTextElement(lastTag)){
            //父元素为正常元素的处理逻辑
        }else{
            //父元素为script、style和textarea的处理逻辑
        }
    }
}

文本解析器

文本解析器的作用就是解析文本,准确的说是对HTML解析器解析出的文本进行二次加工,因为文本分为两种类型:纯文本和带变量的文本。
如果是纯文本,则不需要再进行处理,如果是带变量的文本,则需要使用文本解析器进一步解析,因为在渲染时,需要将变量替换为具体的值。
假设一个"Hello {{name}}",被文本解析器解析完就是"Hello "+_s(name),_s其实是toString的别名。因此文本解析器解析时,就要使用正则表达式来判断文本是否带变量,是否存在{{xxx}}这样的语法,如果是纯文本则返回undefined,如果是,则将变量解析出来,使用_s函数然后再将字符串连接起来。

总结

解析器的作用是通过模板得到AST。
生成AST的过程需要借助HTML解析器,当HTML解析器触发不同的钩子函数,我们可以构建出不同的节点。
随后,我们可以通过栈来得到当前正在构建的节点的父节点,然后将构建出的节点添加到父节点的下面。
最终,当HTML解析器运行完毕后,我们就可以得到一个完整的带DOM层级关系的AST。
HTML解析器的内部原理是一小段一小段地截取模板字符串,每截取一小段字符串,就会根据截取出来的字符串类型触发不同地钩子函数,直到模板字符串截空停止运行。
文本分两种类型,不带变量的纯文本和带变量的文本,后者需要使用文本解析器进行二次加工。