【vue设计与实现】编译器 - parser的实现原理与状态机

parser的实现原理与状态机

Vuejs模板编译器的基本结构和工作流程,主要由三个部分组成:

  1. 用来将模板字符串解析为模板AST的解析器(parser);
  2. 用来将模板AST转换为 JavaScript AST的转换器(transformer);
  3. 用来根据JavaScriptAST生成染函数代码的生成器(generator)。

先讨论解析器 parser 的实现原理

解析器的入参是字符串模板,解析器会逐个读取字符串模板中的字符,并根据一定的规则将整个字符串切割为一个个Token。这里的 Token 可以视作词法记号,后续我们将使用Token一词来代表词法记号进行讲解。举例来说,假设有这样一段模板:

<p>Vue</p>

解析器会把这段字符模板切割为三个Token

  1. 开始标签:<p>
  2. 文本节点: Vue
  3. 结束标签: </p>

解析器是依据有限状态自动机对模板进行切割。所谓“有限状态”,就是指有限个状态,而自动机意味着随着字符的输入,解析器会自动地在不同状态间迁移。
有限状态自动机可以帮助我们完成对模板的标记化 (tokenized)、最终我们将得到一系列Token,状态机的实现如下:

// 定义状态机的状态
const State = {
	initial: 1, // 初始状态
	tagOpen: 2, //标签开始状态
	tagName: 3, // 标签名称状态
	text: 4, //文本状态
	tagEnd: 5, //结束标签状态
	tagEndName: 6 // 结束标签名称状态
}
//一个辅助函数,用于判断是否是字母
function isAlpha(char){
	return char>='a' && char<='z' || char>='A' && char<='Z'
}

// 接收模板宇符串作为参数,并将模板切割为 Token 返回
function tokenize(str){
	// 状态机的当前状态:初始状态
	let currentState = State.initial
	// 用于缓存字符
	const chart = []
	//生成的Token 会存储到 tokens 数组中,并作为函数的返回值返回
	const tokens = []
	// 使用 while 循环开启自动机,只要模板字符串没有被消费尽,自动机就会一直运行
	while(str){
		//查看第一个字符,注意,这里只是查看,没有消费
		const char = str[0]
		// switch 语句匹配当前状态
		switch(currentState){
			case State.initial:
				if (char === '<'){
					//1.状态机切换到标签开始状态
					currentState = State.tagOpen
					//2.消费字符<
					str = str.slice(1)
				}else if(isAplha(char)){
					//1.遇到字母,切换到文本状态
					currentState = State.text
					//2.将当前字母缓存到 chars 数组
					chars.push(char)
					//3.消费当前字符
					str = str.slice(1)
				}
				break;
			// 状态机当前处于标签开始状态
			case State.tagOpen:
				if (isAplha(char)){
					currentState = State.tagName
					chars.push(char)
					str = str.slice(1)
				}else if(char === '/'){
					currentState = State.tagEnd
					str = str.slice(1)
				}
				break;	
			// 状态机当前处于标签名称状态
			case State.tagName:
				if (isAplha(char)){
					//1.遇到字母,由于当前处于标签名称状态,所以不需要切换状态 
					//但需要将当前字符缓存到 chars 数组
					chars.push(char)
					str = str.slice(1)
				}else if(char === '>'){
					//1.遇到字符>,切换到初始状态
					currentState = State.initial
					// 2,同时创建一个标签 Token,并添加到 tokens 数组中
					// 注意,此时 chars 数组中缓存的字符就是标签名称
					tokens.push({
						type: 'tag',
						name: chars.join('')
					})
					chars.length = 0
					str = str.slice(1)
				}
				break;
			case State.text:
				if (isAplha(char)){
					chars.push(char)
					str = str.slice(1)
				}else if(char === '<'){
					// 1,遇到字符<,切换到标签开始状态
					currentState = State.tagOpen
					// 2.从文本状态-->标签开始状态,此时应该创建文本 Token,并添加到 tokens 数组中
					// 注意,此时 chars 数组中的字符就是文本内容
					tokens.push({
						type: 'tag',
						name: chars.join('')
					})
					chars.length = 0
					str = str.slice(1)
				}
				break;	
			//状态机当前处于标签结束状态
			case State.tagEnd:
				if (isAplha(char)){
					//遇到字母,切换到结束标签名称状态
					currentState = State.tagEndName
					chars.push(char)
					str = str.slice(1)
				}
				break;	
			// 状态机当前处于结束标签名称状态
			case State.tagEndName:
				if (isAplha(char)){
					// 1.遇到宇母,不需要切换状态,但需要将当前字符缓存到 chars 数组
					chars.push(char)
					// 2.消费当前字符
					str = str.slice(1)
				}else if(char === '>'){
					// 1.遇到字符 >,切换到初始状态
					currentState = State.initial
					// 2.从 结束标签名称状态 --> 初始状态,应该保存结束标签名称 Token
					// 注意,此时 chars 数组中缓存的内容就是标签名称
					tokens.push({
						type: 'tagEnd',
						name: chars.join('')
					})
					// 3.chars数组的内容已经被消费,清空它
					chars.length = 0
					// 4.消费当前字符
					str = str.slice(1)
				}
				break;	
		}
	}
	//最后,返回 tokens
	return tokens
}

便用上面给出的 tokenzie 函数来解析模板 <p>Vue</p>,我们将得到三个Token

const tokens = tokenzie('<p>Vue</p>')
//[ 
//{type: 'tag', name: 'p'}, // 开始标签
//{type:'text',content;'Vue'},//文本节点
//{type: 'tagEnd', name: p'} //结束节点
//]

总而言之,通过有限自动机,我们能够将模板解析为一个个Token,进而可以用它们构建一棵AST了。但在具体构建AST之前,我们需要思考能否简化 tokenzie 函数的代码。实际上,我们可以通过正则表达式来精简 tokenie 函数的代码。上文之所以没有从最开始就采用正则表达式来实现,是因为正则表达式的本质就是有限自动机,当你编写正则表达式的时候,其实就是在编写有限自动机

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值