vue模板编译
模板编译的概念
在底层实现上,Vue.js会将模板编译成虚拟DOM渲染函数,渲染函数的执行就会产生最新状态下的vnode,然后使用这个vnode进行重新渲染视图
模板编译的作用:输入模板,输出渲染函数
Vue.js中将模板编译成渲染函数的步骤:
- 将模板解析为AST 解析器完成
- 遍历AST标记静态节点 优化器完成
- 使用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中打补丁的过程中将跳过比对和更新过程
优化器内部实现主要分为两个步骤
- 在AST中找出所有静态节点并打上标记(static)
- 在AST中找出所有静态根节点并打上标记(staticRoot)
那么,标记是什么呢?
// 在AST对象中会新增如下两个属性
static: true,
staticRoot: false
优化器框架
export function optimize(root){
if(!root) return;
// 第一步:从根出发递归标记所有静态节点
markStatic(root);
// 第二部:标记所有静态根节点
markStaticRoots(root);
}
找出所有静态节点
补充知识:关于AST中type类型
- type===1 元素节点
- type===2 带变量的动态文本节点
- 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方法中有三个参数
- 标签名 String
- 属性 Object
- 子节点列表 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()可以包装一层字符串