【vue设计与实现】解析器 - 文本模式及其对解析器的影响 & 递归下降算法构造模板 AST

文本模式指的是解析器在工作时所进人的一些特殊状态,在不同的特殊状态下,解析器对文本的解析行为会有所不同
当解析器遇到一些特殊标签时,会切换模式,这些特殊标签是:
遇到<title>、<textarea>,会切换到 RCDATA模式
遇到<style>、<xmp>、<iframe>、<noembed>、<noframes>、<noscript>会切换到 RAWTEXT模式
遇到<![CDATA[,会切换到 CDATA模式

解析器的初始模式则是DATA模式。对于 Vue.js 的模板 DSL来说,模板中不允许出现<script>标签,因此 Vue.is 模板解析器在遇到<script>标签时也会换到 RAWTEXT 模式

解析器的行为会因工作模式的不同而不同
DATA模式下,解析器能够处理HTML字符实体

当解析器处于 RCDATA 状态时,解析器不能识别标签元素。

解析器在RAWTEXT模式下的工作方式与在RCDATA模式下类似。唯一不同的是,在RAWTEXT模式下解析器将不再支持HTML实体。在该模式下,解所器将HTML实体字符作为普通字符处理

CDATA模式在RAWTEXT模式的基础上更进一步。在 CDATA 模式下,解析器将把任何字符都作为普通字符处理,直到遇到 CDATA 的结束标志为止。

后续编写解析器代码时,我们会将上述模式定义为状态表,如下面的代码所示:

const TextModes ={
	DATA:'DATA',
	RCDATA:'RCDATA',
	RAWTEXT:'RAWTEXT',
	CDATA: 'CDATA'
}

递归下降算法构造模板 AST

解析器的基本架构如下:

// 定义文本模式,作为一个状态表
const TextModes = {
	DATA: 'DATA',
	RCDATA: 'RCDATA',
	RAWTEXT: 'RAWTEXT',
	CDATA: 'CDATA'
}

// 解析器函数,接收模板作为参数
function parse(str){
	const context = {
		// source 是模板内容,用于在解析过程中进行消费
		source: str,
		// 解析器当前处于文本模式,初始模式为 DATA
		mode: TextModes.DATA
	}

	// 调用 parseChildren 函数开始进行解析,它返回解析后得到的子节点
	// parseChildren 函数接收两个参数:
	// 第一个参数是上下文对象 context
	// 第二个参数是由父代节点构成的节点栈 初始时栈为空
	const nodes = parseChildren(context,[])

	// 解析器返回 Root 根节点
	return {
		type: 'Root',
		//使用nodes作为根节点的 children
		children: nodes
	}
}

在上面这段代码中,parseChildren 函数是整个解析器的核心。parseChildren 函数会返回解析后得到的子节点,例如下面代码:

<p>1</p>
<p>2</p>

parseChildren 函数在解析这段模板后,会得到由这两个

节点组成的数组:

[
	{ type: 'Element', tag: 'p', children: [/*...*/]},
	{ type: 'Element', tag: 'p', children:[/*...*/] }	
]

parseChildren 函数接收两个参数。
第一个参数: 上下文对象 context。
第二个参数: 由父代节点构成的栈,用于维护节点间的父子级关系

parseChildren 函数本质上也是一个状态机,该状态机有多少种状态取决于子节点的类型数量。在模板中,元素的子节点可以是以下几种。

  • 标签节点,例如<div>
  • 文本插值节点,例如 {{val}}
  • 普通文本节点,例如:text
  • 注释节点,例如<!---->
  • CDATA节点,例如<![CDATA[ xxx]]>

为了降低复杂度,目前仅考虑上述类型的节点。

parseChildren 的代码实现如下:

function parseChildren(context,ancestors){
	// 定义 nodes 数组存储子节点,它将作为最终的返回值
	let nodes = []
	//从上下文对象中取得当前状态,包括模式 mode 和模板内容 source
	const {mode, source} = context
	//开启 while循环,只要满足条件就会一直对字苻串进行解析
	while(!isEnd(context, ancestors)){
		let node
		//只有DATA模式和RCDATA模式才支持插值节点的解析
		if(mode === TextModes.DATA || mode === TextModes.RCDATA){
			//只有 DATA 模式才支持标签节点的解析
			if(mode === TextModes.DATA && source[0] === '<'){
				if(source[1] === '!'){
					if(source.startsWith('<!--')){
						// 注释
						node = parseComment(context)
					}else if(source.startsWith('<![CDATA[')){
					// CDATA
					node = parseCDATA(context,ancestors)
						
					}
				}else if(source[1] === '/'){
					// 结束标签,这里需要抛出错误,后文会详细解释原因
				}else if(/[a-z]i.text(source[1])){
					// 标签
					node = parseElement(context, ancestors)
				}
			}else if(source.startsWith('{{')){
				// 解析插值
				node = parseInterpolation(context)
			}
		}
		// node 不存在,说明处于其他模式,即非 DATA 模式且非 RCDATA 模式
		// 这时一切内容都作为文本处理
		if(!node){
			// 解析文本节点
			node = parseText(context)
		}
		// 将节点添加到 nodes 数组中
		nodes.push(node)
	}
	// 当 while 循环停止后,说明子节点解析完毕,返回子节点
	return nodes
}

注意上面代码里,解析过程中需要判断当前的文本模式。只有处于 DATA 模式或 RCDATE模式时,解析器才支持插值节点的解析。并且,只有处于 DATA模式时,解析器才支持标签节点、注释节点和CDATA 节点的解析。

同时要注意的是while循环停止的条件,以及isEnd()函数的用处,这里简单解释下,parseChildren函数是用来解析自己诶单的,因此while循环一定要遇到父级节点的结束标签才停止,这是正常思路,但是这个思路现在有问题,这里先忽略,后面再详细讨论。

在parseChildren函数中,如果解析器一开始处于 DATA 模式。开始执行解析后,解析器遇到的第一个字符为<,并且第二字符能够匹配正则表达式 /a-z,解析器会进人标签节点状态,并调用 parseElement 函数进行解析。
parseElement 会做三件事: 解析开始标签,解析子节点,解析结束标签。
因此,在 parseElement 函数内,要分别调用三个解析函数来处这三部分内容。

  • parseTag 解析开始标签
  • 递归地调用 parseChildren 函数解析子节点。
  • parseEndTag 处理结束标签。

经过上述三个步骤的处理后,这段模板就被解析完毕了,最终得到了模板AST。
但这里值得注意的是,为了解析标签的子节点,我们递归地调用了 parseChildren 函数。这意味着,一个新的状态机开始运行了,我们称其为“状态机 2”。

在“状态机2”运行期间,为了处理标签节点,我们又调用 parseElement 函数。parseElement 函数会递归地调用 parseChildren 函数完成子节点的解析,这就意味着解析器会再开启了两个新的状态机。

因此,parseChildren 解析函数是整个状态机的核心,状态迁移操作都在该函数内完成
在 parseChildren 函数运行过程中,为了处理标签节点,会调用parseElement 解析函数,这会间接地调用 parseChildren 函数,并产生一个新的状态机。随着标签嵌套层次的增加,新的状态机会随着 parseChildren 函数被递归地调用而不断创建,这就是“递下降”中“递归”二字的含义。
而上级 parseChildren 函数的调用用于构造上级模板AST节点,被递归调用的下级 parseChildren 函数则用于构造下级模板AST节点。最终,会构造出一棵树型结构的模板AST,这就是“递归下降”中“下降”二字的含义

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值