在vue2-AST抽象语法树 https://www.jianshu.com/p/d7822b42a3e3
中 说了一下 把模板 --> ast抽象语法树 --> 渲染函数;
其实在里面 详细 是 模板 ——> 模板编译 ——> 渲染函数
其中 模板编译 包括 三个模块: 解析器 -> 优化器 -> 代码生成器
- 解析器: 把模板解析成AST
- 优化器:遍历AST 标记静态节点
- 代码生成器: 使用AST 生成渲染函数
由于 把模板转 AST语法树 以及讲过。现在看一下 优化器。
优化器
优化器的目标 是遍历AST, 检测出所有 静态子树(永远不会发生变化的DOM节点) 并打上标记
当AST的静态子树 被打上 标记后, 每次重新渲染时, 就不需要为打上标记的 静态节点创建 新的虚拟节点, 而是直接克隆 已存在的虚拟节点。 在虚拟DOM 的更新操作中,如果发现两个节点是 同一个节点,正常情况下会对两个节点进行更新, 但是如果这两个节点是静态节点, 那么直接跳过 更新节点流程。
比如:
转换为AST如下
{
'type': 1,
'tag': 'div',
'attrsList': [
{
'name': 'id',
'value': 'el'
}
],
'attsMap': {
'id': 'el'
},
'children': [
{
'type': 2,
'expression': '"Hello "+_s(name)',
'text': 'Hello {{name}}'
}
],
'plain': false, // true 则没有属性
'attrs': [
{
'name': 'id',
'value': '"el"'
}
]
}
经过优化器优化之后,AST如下
{
'type': 1,
'tag': 'div',
'attrsList': [
{
'name': 'id',
'value': 'el'
}
],
'attsMap': {
'id': 'el'
},
'children': [
{
'type': 2,
'expression': '"Hello "+_s(name)',
'text': 'Hello {{name}}',
'static': false // 静态节点标记
}
],
'plain': false, // true 则没有属性
'attrs': [
{
'name': 'id',
'value': '"el"'
}
],
'static': false, // 静态节点标记
'staticRoot': false // 静态根节点标记
}
其中_s 是下面toString函数的别名 (跟优化器没啥联系,代码生成器中的)
function toString(val) {
return val === null ?
'' :
typeof val === 'object' ?
JSON.stringify(val, null, 2) : // 返回值文本在每个级别缩进2个的空格
String(val)
}
优化器内部实现 分为两个步骤:
- 在AST中找出所有 静态节点并打上标记
- 在AST中找出所有的静态根几点并打上标记
静态节点:永远不会变化的节点
我是永远不会变的 的节点
落实到AST中,静态节点指的是static 属性为true的节点
{
type: 1,
tag: 'p',
staticRoot: false,
static: true
....
}
静态根节点 : 如果一个节点下面所有的子节点都是静态节点
,并且 它的父级 是动态节点
。
比如
- lkjkjljkjlj
- asdf;lksda;l
- adskl;fka
落实到AST中静态根节点只是的 staticRoot属性为true的节点
{
type: 1,
tag: 'ul',
staticRoot: true,
static: true,
....
}
代码的实现如下
export function optimize(root){
if(!root) return;
// 第一步: 标记所有的静态节点
markStatic(root)
// 第二部: 标记所有的静态根节点
markStaticRoots(root)
}
找出所有的静态节点并标记
使用isStatic函数判断节点 是否是静态节点, 然后如果节点的类型为1, 说明节点是元素节点,那么循环改节点的子节点, 递归调用markStatic函数处理子节点
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)
}
}
}
先看一下 node.type取值范围
type值 | 说明 |
---|---|
1 | 元素节点 |
2 | 带变量的动态文本节点 |
3 | 不带变量的纯文本节点 |
实现isStatic函数,判断是否是静态节点
function isStatic(node){
// 带变量的动态文本节点 比如 我的名字是{{name}}
if(node.type === 2){
return false
}
// 不带变量的纯文本节点 比如 我的名字是张三
if(node.type ===3){
return true
}
return !!(ndoe.pre || ( // v-pre 标记的静态节点
!node.hasBindings && // 没有动态绑定 不能有 v- @ :开头的属性
!node.if && !node.for && // 没有 v-if 或 v-for或 v-else 指令
!isBuildInTag(node.tag)&& // 不是内置标签,就是说标签名不能是slot或 component
isPlatformReservedTag(node.tag) && // 不是组件,标签名必须是保留标签 比如 div,p,h1,h2,ul,li,img,hr,ol,svg,table,html,body等
!isDirectChildOfTemplateFor(node) && // 当前节点的父节点不能是带v-for指令的tempalte标签
Object.keys(node).every(isStaticKey) // 节点中不存在动态节点才会有的属性
))
}
其中最后一条 节点中不存在动态节点才会有的属性,事实上, 一个元素节点如果是静态节点,那么这个节点的属性其实是有范围的。如果这个节点是静态节点,那么它的属性是可以在这个范围内找到的。这个范围是 type, tag, attrsList,attrsMap,plain,parent,children, attrs,staticClass和staticStyle
.
如果一个元素节点上的属性在上面这个范围内找不到相同的属性名,就说明这个节点不是静态节点。
还有一个问题,递归从上向下标记静态节点。 如果父节点被标记为静态节点后,子节点却被标记为动态节点,这个会发生矛盾。所有要改一下
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)
// 如果子节点不是静态节点
if(!child.static){
node.static = false
}
}
}
}
找出所有的静态根节点并标记
function markStaticRoots(node) {
if (node.type === 1) {
// 要使节点符合静态根节点的要求,它必须有子节点
// 这个子节点不能是只有一个静态文本 的子节点, 否则优化成本将超过收益
if (node.static && node.children.length && !(node.children.length !== 1 && node.childern[0].type === 3)) {
node.staticRoot = upTask.onProgressUpdate((result) => {
return
});
} else {
// 其他情况 不是静态根节点
node.staticRoot = false
}
// 如果 不是静态根节点 并且有 子节点 ,那么递归 遍历 子节点
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i])
}
}
}
}