vue模板编译原理—有源码

vue模板编译

模板编译的概念

在底层实现上,Vue.js会将模板编译成虚拟DOM渲染函数,渲染函数的执行就会产生最新状态下的vnode,然后使用这个vnode进行重新渲染视图

模板编译的作用:输入模板,输出渲染函数

Vue.js中将模板编译成渲染函数的步骤:

  1. 将模板解析为AST 解析器完成
  2. 遍历AST标记静态节点 优化器完成
  3. 使用AST生成渲染函数 代码生成器完成

备注:AST即抽象语法树,是用于描述一个节点信息的JavaScript对象

整体pipeline:模板->解析器->优化器->代码生成器->渲染函数

解析器

解析器的作用:解析器将模板解析成AST,解析器内部又有很多小的解析器,比如HTML解析器,文本解析器,过滤解析器

AST(abstract syntax tree):是用JavaScript中的对象来描述一个节点(对象中的属性保存了节点的各种数据),一个对象表示一个节点

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

以上的html代码转换成AST后

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

当很多个独立的节点通过parent属性和children属性连在一起,就变成了一棵树,而这样一个用对象描述的节点树其实就是AST

用栈来构建AST层级关系

将节点连成一棵树,或者说构建AST层级关系我们只需要维护一个栈即可,用栈来记录层级关系,栈顶元素一定是下一个入栈元素的父元素,下一个入栈元素就是栈顶元素的子元素

这个层级关系也可以理解为DOM的深度

基于HTML解析器的逻辑,当开始解析一个标签时,把当前节点入栈,当解析结束时,把栈顶元素弹出

那么,我们如何知道什么时候开始解析,什么时候解析结束?-----> 钩子函数

解析器中的钩子函数

解析器中HTML解析器是最主要的,用于解析HTML,并且在解析过程中会不断触发各种钩子函数

start end chars commet
  • start 解析到一个元素标签开始标签文本节点时触发
  • end 解析元素结束标签时触发
  • chars 解析文本时触发
  • comment 解析到注释时触发
function createASTElement(tag, attrs, parent){
	return {
		type: 1,
		tag,
		attrsList: attrs,
		parent,
		children: []
	}
}
parseHTML(template, {
	start(tag, attrs, unary){
		// 参数分别为 标签名,标签的属性以及是否是自闭和标签
		let element = createASTElement(tag, attrs, currentParent)
	}
	end(){}
	chars(text){
		let element = {type: 3, text}
	}
	comment(text){
		let element = {type: 3, text, isComment: true}
	}
})

当解析到一个开始标签或文本节点时,会产生一个AST节点对象并把其压入栈中

当解析到一个结束标签时,将栈顶元素弹出

解析HTML过程就是循环匹配的过程,简单来说就是用HTML模板字符串来循环,每轮循环都从HTML代码中截取一小段字符串

如何精确地匹配或截取那些字符串呢? -----> 需要正则表达式的帮助

优化器

优化器的作用:遍历AST,检测出所有静态子树(即永远都不会发生变化的DOM节点)并给其打上标记

静态子树是指那些在AST中永远都不会发生变化的节点,比如纯文本节点,并且静态节点的特征是除了自身是静态节点外,它的子节点必须是静态节点

标记静态子树的好处

  • 每次重新渲染时,不需要为静态子树创建新节点,而是克隆已存在的静态子树
  • 在虚拟DOM中打补丁的过程中将跳过比对和更新过程

优化器内部实现主要分为两个步骤

  1. 在AST中找出所有静态节点并打上标记(static)
  2. 在AST中找出所有静态根节点并打上标记(staticRoot)

那么,标记是什么呢?

// 在AST对象中会新增如下两个属性
static: true,
staticRoot: false
优化器框架
export function optimize(root){
	if(!root) return;
	// 第一步:从根出发递归标记所有静态节点
	markStatic(root);
	// 第二部:标记所有静态根节点
	markStaticRoots(root);
}
找出所有静态节点

补充知识:关于AST中type类型

  1. type===1 元素节点
  2. type===2 带变量的动态文本节点
  3. type===3 不带变量的纯文本节点
function isStatic(node){
	if (node.type === 2){
		return false;
	}
	if (node.type === 3){  //不带变量的纯文本节点直接返回是静态节点
		return true;
	}
	return !!(node.pre || (
		!node.hasBindings &&
		!node.if && !node.for &&
		!isBuiltInTag(node.tag) &&
		isPlatformReversedTag(node.tag) &&
		!isDerectChildOfTemplateFor(node) &&
		Object.keys(node).every(isStaticKey)
	))
}
function markStatic(node){
	node.static = isStatic(node)
	if(node.type === 1){ //即元素节点 可能存在子节点
		for(let i = 0, l = node.children.length; i < l; i++){  //遍历孩子
			const child = node.children[i];
			markStatic(child);  //递归mark
		}
		if (!child.static){ // 如果子节点是动态节点,则其父节点也修改成动态节点
			node.static = false;
		}
	}
}
找出所有静态根节点

静态节点有个特点就是静态节点的所有子节点也都必须为静态节点,那么我们找到第一个静态节点,就一定是一颗静态子树的根节点

如果找到了第一个静态节点,将其判定为静态根节点,标记staticRoot为true,那么不会继续向他的子级继续寻找

另外:除了节点不是静态节点我们不会标记为静态根节点,还有

  • 如果一个静态节点的子节点只有一个文本节点
  • 没有子节点的静态节点

我们不会将它标记为静态根节点,因为这两种情况,优化成本大于收益

代码生成器

代码生成器的作用:是模板编译的最后一步,它将AST转换成渲染函数中的内容(这个内容也可以称作代码字符串

首先看个例子

<div id="el">
	<p>Hello {{name}}</p>
</div>
with(this){return _c("div", {attrs: {"id": "el"}}, [_c("p", [_v("Hello" + _s(name))])])}

代码生成器通过AST递归生成代码字符串,可以发现代码字符串是嵌套的函数调用,函数_c中又有执行其他函数

三种节点创建方法与别名

  • _c createElement 创建元素节点
  • _v createTextVNode 创建文本节点
  • _e createEmptyVNode 创建注释节点

其中,_c方法中有三个参数

  1. 标签名 String
  2. 属性 Object
  3. 子节点列表 Array

代码生成器步骤:genElement->genData->genChildren->genNode(genNode中调用genElement,genComment,genText)

// el是AST节点
function genElement(el, state){
	// plain是在编译过程中发现的,当节点没有属性,将设置plain为true
	const data = el.plain ? undefined : genData(el, state);
	const children = genChildren(el, state)
}
function genData(el, state){
	let data = "{";
	if(el.key){
		data += `key:${el.key}`
	}
	//... 将el中的各属性进行拼接
}
function genChildren(el, state){
	const children = el.children;
	if(children.length){
		// 生成子节点并以都好拼接
		// 模板字符串
		return `[${children.map(c=>genNode(c, state)).join(',')}]`
	}
}
function genNode(node, state){
	if(node.type === 1){
		return genElement(node, state);
	}
	if(node.type === 3 && node.isComment){
		return genComment(node); 
	}
	if(node.type === 2){
		return genText(node);
	}
}
function genComment(node){
	return `_e(${JSON.stringify(node.text)})`;
}
function genText(node){
	return `_v(${text.type===2? node.expression : JSON.stringify(node.text)})`
}

JSON.stringify()可以包装一层字符串

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值