渲染
模板语法解析 https://github.com/M-cheng-web/vue-sound-snabbdom
ast抽象语法树 https://github.com/M-cheng-web/vue-sound-ast
模板字符串引擎 https://github.com/M-cheng-web/vue-sound-mustache
(这个并没有实际应用,只是扩展视野)
vue 的渲染过程
模板语法 => 抽象语法树ast => 渲染函数(h函数) => 虚拟节点 => 界面
AST抽象语法树
将模板语法转化为JS对象
在vue中所有的html代码会视为字符串,需要将这些类似html代码的字符串转换为ast
得到一个js对象后再进行渲染函数(h函数),再由patch函数上树,显示到页面
html
<div class="box">
<h3 class="title">我是一个标题</h3>
<ul>
<li v-for="item in arr" :key="index">
{{item}}
</li>
</ul>
</div>
会转化为
js
const ast = {
tag: "div",
attrs: [{ name: "class", value: "box" }],
type: 1,
children: [
{
tag: "h3",
attrs: [{ name: "class", value: "title" }],
type: 1,
children: [{ text: "我是一个标题", type: 3 }]
},
{
tag: "ul",
attrs: [],
type: 1,
children: [
{
tag: "li",
for: "arr",
key: "index",
alias: "item",
type: 1,
children: []
}
]
}
]
}
渲染函数(h函数)
根据模板语法得到的JS对象再由 h()
转化一下,主要是生成利于做 diff算法
比较的虚拟DOM,还有就是会将页面中用到的变量转为实际值
patch函数
对比新旧节点,遵循下面的流程图(会有递归调用到最底层)
diff算法示例
TIP
diff算法口诀
- 新前与旧前
- 新后与旧后
- 新后与旧前
- 新前与旧后
这里我示例一下程序是怎么走的(只示例一下复杂的情况),可能表达有点问题,自己跑一遍就很清晰了
oldCh: 旧子节点 newCh: 新子节点
- 新增的情况
js
oldCh = [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
]
newCh = [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E'),
]
- 删除的情况
js
oldCh = [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
]
newCh = [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'D' }, 'D'),
]
- 多删除的情况
js
oldCh = [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E'),
]
newCh = [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'D' }, 'D'),
]
- 复杂的情况
js
oldCh = [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E'),
]
newCh = [
h('li', { key: 'E' }, 'E'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'M' }, 'M'),
]
- 对比 newCh的第一位E,不满足新前与旧前相等,新后与旧后相等,新后与旧前相等
- 满足 新前与旧后相等
- 调用 patchVnode传入新旧vnode,如果新旧vnode还有children,会再次进入 updateChildren.js,间接递归,这里不进行深入
- 因为是新前与旧后相等,并且经过 patchVnode方法操作后,新vnode的属性是已经赋值给旧vnode的真实dom上了,但是旧vnode它的位置并不是在第一位,所以要调用 parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm) 把E这个节点放到第一个节点A之前
- 进行完上面的真实dom操作后,要对指针进行移位
- --oldEndIdx(旧后往上移一位) ++newStartIdx(新前往下移一位)
- 重新赋值 oldEndVnode 与 newStartVnode
- 对比 newCh的第二位C,不满足 新前与旧前相等,新后与旧后相等,新后与旧前相等,新前与旧后相等
- 四种情况都没有命中,创建 keyMap对象,并将oldCh当前的 oldStartIdx 至 oldEndIdx 的vnode的key作为 keyMap的键,值为vnode的下标
- 创建了 keyMap对象后查找 C这个vnode的key,发现存在,则把这个旧节点拿过来复用
- 复用了后要oldCh中这个 C代表的vnode置为undefined,代表这个节点已经被处理了,无需再次处理
- 然后还要处理位置关系,因为只是把旧节点内容换了,并没有调整位置
- 调用 parentElm.insertBefore(C.elm, oldStartVnode.elm) 把C节点位置移到旧A之前(注意是A之前,并不是E之前,这个E是之前插入的)
- 对比 newCh的第三位M,不满足 新前与旧前相等,新后与旧后相等,新后与旧前相等,新前与旧后相等
- 在 keyMap对象中查找 M这个vnode的key,发现不存在,则新建一个DOM再移位放入旧A之前
- 做完了上面的步骤后,newStartVnode 是大于 newEndVnode 的,所以退出while,此时旧节点数组是这样的 [E, C, M, A, B, D]
- 继续看看有没有剩余的,然后选择新增或者删除
- 发现 oldStartIdx <= oldEndIdx,也就是旧节点数组还没有处理完,但是新节点数组处理完了
- 这就代表旧节点数组从 oldStartIdx开始到 oldEndIdx 的节点都要删除
- 遍历删除,然后结果就变成了 [E, C, M],中间只新建了M这个真实DOM,精彩!!!!