目录

1  解析器的作用

2  解析器内部运行原理

3  html解析器

3.1  运行原理

3.2  截取开始标签

3.3  截取结束标签

3.4  截取注释

3.5  截取条件注释

3.6  截取DOCTYPE

3.7  截取文本

3.8  纯文本内容元素的处理

3.9  使用栈维护dom层级

3.10  整体逻辑

4  文本解析器

5  总结


1  解析器的作用

解析器要实现的功能是将模板解析成AST

案例:

<div><p> {{ name }} </p>
</div>

转换后的AST:

{tag:"div",type:1,staticRoot:false,static:false,plain:true,parent:undefined,attrsList:[],attrsMap:{},children:[{tag:"p",type:1,staticRoot:false,static:false,plain:true,parent:{tag:"div",// ...},attrsList:[],attrsMap:{},children:[{type:2,text:"{{name}}",static:false,expression:"_s(name)"}]}]
}

2  解析器内部运行原理

解析器也分好几个子解析器,比如:html解析器、文本解析器、过滤解析器,主要是html解析器,html解析器的伪代码如下所示:

parseHTML(template,{start(tag,attrs,unary){// 解析到标签开始位置时,触发该函数},end(){// 解析到标签结束位置时,触发该函数},chars(text){// 解析到文本时,触发该函数},comment(text){// 解析到注释时,触发该函数}
})

比如:<div><p>我是lisa</p></div>当被html解析器解析时,所触发的钩子函数依次是:start、start、chars、end、end;

当html解析器不再触发钩子函数时,就说明所有模板都解析完毕,所有类型的节点都在钩子函数中构建完成,即AST构建完成。

在start钩子函数中,使用三个参数来创建一个元素类型的AST节点,例如:

function createASTElement(tag,attrs,parent){return {type:1,tag,attrsList:attrs,parent,children:[]}
}
parseHTML(template,{start(tag,attrs,unary){let element = createASTElement(tag,attrs,unary)},chars(text){let element = { type:3,text }},comment(text){let element = { type:3,text,isComment:true }}
})

构建AST层级关系其实非常简单,只需要维护一个栈(stack)即可,用栈来记录层级关系,这个层级关系也可以理解为dom的深度。

html解析器在解析html时,是从前往后解析。每当遇到开始标签,就触发钩子函数start,并把当前节点推入栈中;遇到结束标签,就触发end钩子函数,从栈中弹出一个节点。如下图所示:

模板:

<div><h1>我是Berwin</h1><p>我今年23岁</p>
</div>

上面模板被解析成AST的过程如下图所示:

3  html解析器

3.1  运行原理

解析html模板的过程就是循环的过程,简单来说就是用html模板字符串来循环,每轮循环从html模板中截取一小段字符串,然后重复以上过程,直到html模板被截成一个空字符串结束循环,解析完毕。

手动模拟html解析器的解析过程:

<div><p>{{name}}</p>
</div>

最初始的html模板:

`<div>

<p>{{ name }}</p>

</div>`

第一轮循环时,截取出一段字符串<div>,并触发钩子函数start,截图后的结果为:

`

<p>{{ name }}</p>

</div>`

第二轮时,截取出一段字符串:

`

`

并且触发钩子函数chars,截取后的结果为:

`<p>{{ name }}</p>

</div>`

第三轮循环时,截取出一段字符串<p>,并触发钩子函数start,截取后的结果为:

`{{ name }}</p>

</div>`

第四轮循环时,截取出一段字符串{{ name }},并触发钩子函数chars,截取后的结果为:

`</p>
</div>`

第五轮循环时,截取出一段字符串</p>,并触发钩子函数end,截取结果为:

`

</div>`

第六轮循环时,截取出一段字符串:

`

`

并触发钩子函数chars,截取后的结果为:

`</div>`

第七轮循环时,截取出一段字符串</div>,并触发end钩子函数,截取后的结果为:

``

解析完毕。

被截取的片段分为很多种类型:

1.开始标签;

2.结束标签;

3.html注释,<!-- 注释 -->;

4.DOCTYPE;

5.条件注释;

6.文本;

3.2  截取开始标签

如何确定模板是不是开始标签开头(下面代码不包含标签属性的解析)?

// 正则表达式来匹配模板以开始标签开头
const ncname = '[a-zA-Z_][\\w\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)// 以开始标签开始的模板
"<div></div>".match(startTagOpen) // ["<div","div",index:0,input:"<div></div>"]
// 以结束标签开始的模板
"</div>".match(startTagOpen) // null

当html标签解析到标签开始时,会触发钩子函数start,同时会给出三个参数,分别是标签名(tagName)、属性(attrs)和自闭合标识(unary)。

接下来就是解析标签名、属性、自闭合标识,解析后得到下面的数据结构:

const start = '<div></div>'.match(startTagOpen)
if (start) {const match = {tagName: start[1],attrs: [],}
}

1.解析标签属性

解析完开始标签开头之后,模板中伪代码的样例如下:'  class="box"></div>'

每解析一个属性就截取一个属性。如果截取完后,剩下的html模板依然符合标签属性的正则表达式,那说明还有剩余的属性需要处理,此时需要重复执行前面的流程,直到剩余的模板不存在属性,也就是剩余的模板不存在符合正则表达式所预设的规则。

const startTagClose = /^\s*(\/?)>/
const attribute =/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>']+)))?/
let html = ' class="box" id="el"></div>'
let end, attr
const match = { tagName: 'div', attrs: [] }while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {html = html.substring(attr[0].length)match.attrs.push(attr)
}

解析后的结果为:

{tagName: 'div',attrs: [[' class="box"', 'class', '=', 'box', null, null],[' id="el"', 'id', '=', 'el', null, null],],
}

2.解析自闭合标识

自闭合标签没有子节点的,所以我们提前构建AST层级时,需要维护一个栈,而一个节点是否需要推入到栈中,可以使用自闭合标识来判断。

代码如下:

function parseStartTagEnd(html) {const startTagClose = /^\s*(\/?)>/const end = html.match(startTagClose)const match = {}if (end) {match.unarySlash = end[1]html = html.substring(end[0].length)return match}
}
console.log(parseStartTagEnd('></div>')) // {unarySlash:""}
console.log(parseStartTagEnd('/><div></div>')) // {unarySlash:"/"}

3.实现源码

解析开始标签时,被拆分成三个部分,分别是标签名、属性、结尾。vue中的源码如下:

const ncname = '[a-zA-Z_][\\w-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
function advance(n) {html = html.substring(n)
}
function parseStartTag() {// 解析标签名,判断模板是否符合开始标签的特征const start = html.match(startTagOpen)if (start) {const match = {tagName: start[1],attrs: [],}advance(start[0].length)// 解析标签属性let end, attrwhile (!(end = html.match(startTagClose)) &&(attr = html.match(attribute))) {advance(attr[0].length)match.attrs.push(attr)}// 判断该标签是否是自闭合标签if (end) {match.unarySlash = end[1]advance(end[0].length)return match}}
}

如果调用它后得到了解析结果,那么说明剩余模板得开始部分符合开始标签得规则,此时将解析出来得结果取出来并调用钩子函数start即可:

const startTagMatch = parseStartTag()
if(startTagMatch){handleStartTag(startTagMatch)continue
}

所有解析操作都运行在循环中,所以continue的意思是这一轮的解析工作已经完成,可以进行下一轮解析工作。

从代码中可以看出,如果调用parseStartTag之后有返回值,那么会进行开始标签的处理,其处理逻辑主要在handleStartTag中。这个函数的主要目的就是将tarName、attrs、unary等数据取出来,然后调用钩子函数将这些数据放到参数中。

3.3  截取结束标签

只有html模板的第一个字符是<时,我们才需要确认它到底是不是结束标签。

正则匹配代码如下:

const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const endTagMatch = '</div>'.match(endTag) // ["</div>","div",index:0,input:"</div>"]
const endTagMatch2 = '<div>'.match(endTag) // null

分辨出剩余模板是否是结束标签后,还需要截取模板,和触发钩子函数。vue源码精简后:

const endTagMatch = html.match(endTag)
if (endTagMatch) {html = html.substring(endTagMatch[0].length)urlToHttpOptions.end(endTagMatch[1])continue
}

3.4  截取注释

const comment = /^<!--/
if(comment.test(html)){const commentEnd = html.indexOf("-->")if(commentEnd >= 0){if(options.shouldKeepComment){options.comment(html.substring(4,commentEnd))}html = html.substring(commentEnd + 3)continue}
}

3.5  截取条件注释

const conditionalComment = /^<!\[/
if(conditionalComment.test(html)){const conditionalEnd = html.indexOf("]>")if(conditionalEnd){html = html.substring(conditionalEnd + 2)continue}
}

3.6  截取DOCTYPE

const doctype = /^<!DOCTYPE [^>]+>/i
const doctypeMatch = html.match(doctype)
if(doctypeMatch){html = html.substring(doctypeMatch[0].length)continue
}

3.7  截取文本

如果html模板的第一个字符不是<,那么它一定是文本了。

while (html) {let textlet textEnd = html.indexOf("<")// 截取文本if(textEnd >= 0){rest = html.slice(textEnd)/*** while用于解决文本中存在<的问题,如果剩余的模板不符合任何被解析的类型,* 那么重复解析文本,知道剩余模板符号被解析的类型为止*/while (!endTag.test(rest) && // 结束标签!startTagOpen.test(rest) && // 开始标签!comment.test(rest) && // 注释!conditionalComment.test(rest) // 条件注释) {// 如果<在纯文本中,将它视为纯文本对待next = rest.indexOf("<",1)if(next < 0) breaktextEnd += nextrest = html.slice(0,textEnd)}text = html.substring(0,textEnd)html = html.substring(textEnd)}// 如果模板中找不到<,就说明整个模板都是文本if(textEnd < 0){text = htmlhtml = ""}// 触发钩子函数if(options.chars && text){options.chars(text)}
}

3.8  纯文本内容元素的处理

什么是纯文本内容元素呢?script、style、textarea这三种元素叫做纯文本内容元素。

while (html) {if (!lastTag || !isPlainTextElement(lastTag)) {// 父元素为正常元素的处理逻辑} else {// 父元素为script、style、textarea的处理逻辑const stackedTag = lastTag.toLowerCase()const reStackedTag =reCache[stackedTag] ||(reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)','i'))// 把文本截取出来并触发钩子函数charsconst rest = html.replace(reStackedTag, function (all, text) {if (options.chars) {options.chars(text)}return ''})html = restoptions.end(stackedTag)}
}

3.9  使用栈维护dom层级

每解析到开始标签,就向栈中推进去一个;每解析到标签结束,就弹出一个。同时,html解析器中的栈还有另一个作用,可以检测html标签是否正确闭合。

3.10  整体逻辑

如果通过<分辨出即将解析的这一小部分字符不是文本而是标签类,那么标签类有那么多类型,需要进一步分辨是哪种类型:

function parseHTML(html, options) {while (html) {if (!lastTag || !isPlainTextElement(lastTag)) {let textEnd = html.indexOf('<')if (textEnd === 0) {if (comment.test(html)) {// 注释的逻辑处理continue}if (conditionalComment.test(html)) {// 条件注释的逻辑处理continue}const doctypeMatch = html.match(doctype)if (doctypeMatch) {// doctype的逻辑处理continue}const endTagMatch = html.match(endTag)if (endTagMatch) {// 结束标签的逻辑处理continue}const startTagMatch = parseStartTag()if (startTagMatch) {// 开始标签的处理逻辑continue}}let text, rest, nextif (textEnd >= 0) {// 解析文本}if (textEnd < 0) {text = htmlhtml = ''}if (options.chars && text) {options.chars(text)}} else {// 父元素为script、style、textarea的处理逻辑}}
}

4  文本解析器

文本解析器得作用是解析文本。准确得说是对html解析器解析出来得文本进行二次加工

文本其实分两种类型,一种是纯文本(不需要任何处理),另一种是带变量得文本(需要解析器进一步解析)。

每当html解析器解析到文本时,都会触发chars函数,并且从参数中得到解析出来的文本。在chars函数中,需要构建文本类型的AST,并将它添加到父节点的children属性中。

如果遇到带变量的文本,需要进行二次加工,代码如下所示:

parseHTML(template, {// ...chars(text) {text = text.trim()if (text) {const children = currentParent.childrenlet expression/*** 执行parseText后又返回值,说明是带变量的文本,加工后添加到children中;* 否则说明是普通文本,直接添加到children中*/if ((expression = parseText(text))) {children.push({type: 2,expression,text,})} else {children.push({type: 3,text,})}}},// ...
})

例如:"Hello  {{ name }}"解析后:"Hello_" + _s(name)

上面代码中_s就是下面这个toString函数的别名:

function toString(val) {return val == null? '': typeof val === 'object'? JSON.stringify(val, null, 2): String(val)
}

案例:

var obj = { name: "Berwin"}
with(obj){function toString(val) {return val == null? '': typeof val === 'object'? JSON.stringify(val, null, 2): String(val)}console.log("Hello " + toString(name)) // "Hello  Berwin"
}

事实上,最终AST会转换成代码字符串放在with中执行。

在文本解析器中,先使用正则表达式来匹配是否为带变量的文本,纯文本返回undefined:

function parseText(text) {const tagRE = /\{\{((?:.|\n)+?)\}\}/gif (!tagRE(text)) {return}const tokens = []let lastIndex = tagRE.lastIndex = 0let match,indexwhile (match = tagRE.exec(text)) {index = match.index// 先把 {{ 前边的文本添加到token中if(index > lastIndex){tokens.push(JSON.stringify(text.slice(lastIndex,index)))}// 把变量改成_s(x)这样的形成也添加到数组中tokens.push(`_s(${match[1].trim()})`)// 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本lastIndex = index + match[0].length}// 当所有变量都处理完毕后,如果最后一个变量右边还有文本,就将文本添加到数组中if(lastIndex < text.length){tokens.push(JSON.stringify(text.slice(lastIndex)))}return tokens.join("+")
}

这段代码有个关键的地方在lastIndex:每处理完一个变量后,会重新设置lastIndex的位置,这样可以保证如果后面还有其他变量,那么在下一轮循环时可以从lastIndex的位置开始向后匹配,而lastIndex之前的文本将文本将不再被匹配。

5  总结

解析器的作用是通过模板得到AST。

生成AST的过程需要借助html解析器,当html解析器触发不同的钩子函数时,我们可以构建出不同的节点。

随后,我们通过栈来得到当前正在构建的节点的父节点,然后将构建出的节点添加到父节点的下面。

最终,当html解析器运行完毕后,可以得到一个完整的带dom层级关系的AST。

html解析器的内部原理:一小段一小段地截取模板字符串,每截取一小段字符串,就会根据截取出来的字符串类型触发不同的钩子函数,直到模板字符串截空停止运行。

文本分两种类型,不带变量的纯文本和带变量的文本,带变量的文本需要使用文本解析器进行二次加工。

注:本文章来自于《深入浅出vue.js》(人民邮电出版社)阅读后的笔记整理

Vue2源码解析 解析器相关推荐

  1. 【笔记-vue】《imooc-vue.js高仿饿了么》、《imooc-vue 音乐app》、《imooc-vue.js源码全方位解析》

    20170709 - 20171128:<imooc-vue.js高仿饿了么> 一.第一章 课程简介 1-1课程简介 1.需求分析-脚手架工具-数据mock-架构设计-代码编写-自测-编译 ...

  2. 【TarsosDSP】TarsosDSP 简介 ( TarsosDSP 功能 | 相关链接 | 源码和相关资源收集 | TarsosDSP 示例应用 | TarsosDSP 源码路径解析 )

    文章目录 I . TarsosDSP 函数库简介 II . TarsosDSP 功能 III . TarsosDSP 相关资源链接 ( 官方资料 ) IV . TarsosDSP 源码和相关资源收集 ...

  3. 从源码角度解析Android中APK安装过程

    从源码角度解析Android中APK的安装过程 1. Android中APK简介 Android应用Apk的安装有如下四种方式: 1.1 系统应用安装 没有安装界面,在开机时自动完成 1.2 网络下载 ...

  4. Go netpoll I/O 多路复用构建原生网络模型之源码深度解析

    原文 Go netpoll I/O 多路复用构建原生网络模型之源码深度解析 导言 Go 基于 I/O multiplexing 和 goroutine 构建了一个简洁而高性能的原生网络模型(基于 Go ...

  5. Spring源码深度解析(郝佳)-学习-源码解析-基于注解bean定义(一)

    我们在之前的博客 Spring源码深度解析(郝佳)-学习-ASM 类字节码解析 简单的对字节码结构进行了分析,今天我们站在前面的基础上对Spring中类注解的读取,并创建BeanDefinition做 ...

  6. 《Spring源码深度解析 郝佳 第2版》AOP

    往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...

  7. 《Spring源码深度解析 郝佳 第2版》ApplicationContext

    往期博客: <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度 ...

  8. 《Spring源码深度解析 郝佳 第2版》SpringBoot体系分析、Starter的原理

    往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...

  9. Spring源码深度解析(郝佳)-学习-源码解析-创建AOP静态代理实现(八)

    继上一篇博客,我们继续来分析下面示例的 Spring 静态代理源码实现. 静态 AOP使用示例 加载时织入(Load -Time WEaving,LTW) 指的是在虚拟机载入字节码时动态织入 Aspe ...

  10. Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析

      我们在看Spring Boot源码时,经常会看到一些配置类中使用了注解,本身配置类的逻辑就比较复杂了,再加上一些注解在里面,让我们阅读源码更加难解释了,因此,这篇博客主要对配置类上的一些注解的使用 ...

最新文章

  1. Android之获得内存剩余大小与总大小
  2. SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession作用域(Scope)和生命周期
  3. 安卓自定义View进阶-Matrix Camera
  4. 打印页面横向怎么设置_条码打印软件标签纸页面设置的方法
  5. 在Linux 环境下搭建 JDK 和 Tomcat
  6. mysql 单块读 多块读_求指点:STM32F103VC的SDIO读SD卡单块读成功,多块读却不行?...
  7. java paint的使用_java GUI编程之paint绘制操作示例
  8. 在浪漫的巴黎,他们举行了世界上首个无人机节
  9. Mac桌面上找不到或无法显示USB问题
  10. OpenCV 图像编解码操作【imencode/imdecode】使用
  11. python列表后面两个括号_python列表[]中括号
  12. 怎么样利用栅格数据分类后的结果以行政区域统计各个地类的面积
  13. 服务器软件firmware的作用(BIOS、BMC、PSOC、CPLD)
  14. 电子计算机上面的mrc是什么意思,计算器上 M MRC GT CE MU 键分别是什么意思?
  15. Python中条件判读语句if的使用详解
  16. Celery 分发任务
  17. Cantor 表 {C语言解法}
  18. 固态硬盘寿命不长?!快来看看12个固态硬盘优化技巧
  19. 2018年最新Python学习路线图(内含大纲+视频+工具)
  20. JavaScript 之 核心语法 [ 对象 ]

热门文章

  1. 量化实盘框架对比选择
  2. 企业软文怎么写?教你搞定各种类型软文
  3. python播放视频没有声音_下面这段代码播放的视频没有声音,怎样才能连声音也一起播放呢?...
  4. Nickel 28宣布2021年第三季度财务业绩
  5. 34岁美女相亲遭群嘲,女性物化和男性量化的不争事实
  6. 数据仓库之【商品订单数据数仓】05:需求2:电商GMV
  7. 黎明觉醒服务器维护什么时候结束,黎明觉醒终极测试什么时候结束 黎明觉醒最终测试多久...
  8. kettle引用外部脚本完成电话号码清洗、去重缩进
  9. 《实用机器学习》(孙亮 黄倩.著)笔记——第七章 基于内容的推荐算法
  10. 面试如何进行项目介绍