【vue设计与实现】解析器 - 解析标签节点

在上一节给出的 parseElement 函数的实现中,无论是解析开始标签还是用闭合标签,都调用了parseTag 函数,同时,使用parseChildren函数来解析开始标签与闭合标签中间的部分,如下面的代码及注释所示:

function parseElement(context, ancestors){
	const element = parseTag(context)
	if(element.isSelfClosing) return element
	
	ancestors.push(element)
	element.children = parseChildren(context, ancestors)
	ancestors.pop()

	if(context.source.startsWith(`</${element.tag}`)){
		// 再次调用parseTag函数解析结束标签,传递了第二个参数:'end' 
		parseTag(context, 'end')
	}else{
		console.error(`${element.tag}标签缺少闭合标签`)
	}
	
	return element
}

无论处理的是开始标签还是结束标签,parseTag函数都会消费对应的内容,为了实现对模板内容的消费,需要再上下文对象中新增两个工具函数,如下面代码所示:

function parse(str){
	// 上下文对象
	const context = {
		// 模板内容
		source: str,
		mode: TextModes.DAT,
		// advanceBy函数用来消费指定数量的字符,它接收一个数字作为参数
		advanceBy(num){
			// 根据给定字符数num, 截取位置num后的模板内容,并替换当前模板内容
			context.source = context.source.slice(num)
		},
		// 无论是开始标签还是结束标签,都可能存在无用的空白字符,例如 <div    >
		advanceSpaces(){
			// 匹配空白字符
			const match = /^[\t\r\n\f ]+/.exec(context.source)
			if(match){
				// 调用advanceBy函数消费空白字符
				context.advanceBy(match[0].length)
			}
		}
	}
	const nodes = parseChildren(context, [])
	return {
		type: 'Root',
		children: nodes
	}
	
}

有了advanceBy 和 advanceSpaces 函数后,我们就可以给出 parseTag 函数的实现了,如下面的代码所示:

// 由于 parseTag 既用来处理开始标签,也用来处理结束标签,因此我们设计第二个参数 type,
// 用来代表当前处理的是开始标签还是结束标签,type 的默认值为'start',即默认作为开始标签处理
function parseTag(context, type='start'){
	//从上下文对象中拿到 advanceBy 函数
	const {advanceBy, advanceSpaces} = context
	// 处理开始标签和结束标签的正则表达式不同
	const match = type === 'start'
		// 匹配开始标签
		?  /^<([a-z][^\t\r\n\f />]*)/i.exec(context.source)
		// 匹配结束标签
		:  /^<\/([a-z][^\t\r\n\f />]*)/i.exec(context.source)
	// 匹配成功后,正则表达式的第一个捕获组的值就是标签名称	
	const tag = match[1]
	// 消费正则表达式匹配的全部内容,例如‘<div’这段内容
	advanceBy(match[0].length)
	advanceSpaces()
	// 在消费匹配的内容后,如果字符串以'/>’开头,则说明这是一个自闭合标签
	const isSelfClosing = context.source.startsWith('/>')
	// 如果是自闭合标签,则消费'/>', 否则消费'>'
	advanceBy(isSelfClosing?2:1)
	// 返回标签节点
	return {
		type: 'Element',
		// 标签名称
		tag,
		// 标签的属性暂时留空
		props:[],
		// 子节点留空
		children:[],
		// 是否自闭合
		isSelfClosing
	}

}

在经过上述处理后,parseTag 函数会返回一个标签节点。parseElement 函数在得到由parseTag 函数产生的标签节点后,需要根据节点的类型完成文本模式的切换,如下面的代码所示

function parseElement(context, ancestors){
	const element = parseTag(context)
	if(element.isSelfClosing) return element
	// 切换到正确的文本模式
	if(element.tag === 'textarea' || element.tag === 'title'){
		//如果由 parseTag 解析得到的标签是 <textarea> 或 <title>,则切换到 RCDATA 模式
		context.mode = TextModes.RCDATA
	}else if(/style|xmp|iframe|noembed|noframes|noscript/.test(element.tag)){
		// 如果由 parseTag 解析得到的标签是:
		// <style>、<xmp>、<iframe>、<noembed>、<noframes>、<noscript>
		//则切换到 RAWTEXT 模式
		context.mode = TextModes.RAWTEXT
	}else{
		//否则切换到 DATA 模式
		context.mode = TextModes.DATA
	}

	ancestors.push(element)
	element.children = parseChildren(context, ancestors)
	ancestors.pop()

	if(context.source.startsWith(`</${element.tag}`)){
		parseTag(context, 'end')
	}else{
		console.error(`${element.tag}标签缺少闭合标签`)
	}
	
	return element
}

至此,就实现了对标签节点的解析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值