课程内容:
- snabbdom简介 (介绍宏观背景、历史沿革)
- snabbdom的h函数如何工作 (先学会怎么用)
- diff算法原理(再研究它底层机理)
- 手写diff算法(最后手写它,彻底掌握它!)
要解决的问题:
研究1:虚拟DOM如何被渲染函数(h函数)产生?
——我们要手写h函数
研究2:diff算法原理?
——我们要手写diff算法
研究3:虚拟DOM如何通过diff变为真正的DOM的
——事实上,虚拟DOM变回真正的DOM,是涵盖在diff算法里面的
虚拟DOM如何被渲染函数(h函数)产生
虚拟dom就是用js对象来描述dom的层次结构,dom中的一切属性都可以在虚拟dom上有对应的属性!实现一个低配版本的h函数,调用的形态为以下的三种之一:
h('div', {}, 'text')
h('div', {}, [])
h('div', {}, h())
function vnode(sel, data, children, text, elm) {
const key = data && data.key;
return { sel, data, children, text, elm, key };
}
export default function h(sel, data, c) {
// 检查入参熟练
if (arguments.length !== 3) {
throw new Error('请传入三个参数')
}
// 检查c参数类型 是text,h('div', {}, 'text')
if (typeof c == 'string' || typeof c == 'number' ) {
return vnode(sel,data,undefined,c,undefined)
// 检查c参数类型 是h数组,h('div', {}, [])
}else if(Array.isArray(c)) {
let children = []
for (let i = 0; i < c.length; i++) {
if (!(typeof c[i] == 'object' )) {
throw new Error('传入的数组函数不是一个h对象')
}
children.push(c[i])
}
return vnode(sel,data,children,undefined,undefined)
// 检查c参数类型 是h对象,h('div', {}, h())
}else if(typeof c == 'object' && c.hasOwnProperty('sel')) {
let children = c
return vnode(sel,data,children,undefined,undefined)
}else{
throw new Error('传入的第三个参数类型错误')
}
}
diff算法
diff算法进行精细化对比,可以实现最小量更新
- 只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。key是这个节点的唯一标识,告诉diff算法,前后为同一个节点
- 延伸问题:如何定义是同一个虚拟节点?答:选择器相同且key相同。
- 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你。而是暴力删除旧的、然后插入新的。
- diff并不是那么的“无微不至”啊!真的影响效率么??答:上面的操作在实际Vue开发中,基本不会遇见,所以这是合理的优化机制。
遇到的bug记录
- index.js中无法获取html页面的dom节点,是因为在模板渲染时js已经全部执行完了,导致js获取不到dom元素... 所以把script标签放index页面最后一行
- 传入都是虚拟节点,patch上树时获取不到elm原生dom元素。
需要patch两次:
const container = window.document.getElementById('box')
patch(container, node1)
patch(node1, node2)
diff算法的逻辑策略
- 四种命中查找:新前与旧前、新后与旧后、新后与旧前、新前与旧后
- 是从上往下的命中顺序,命中一种就不再进行命中判断了!
- 如果都没有命中,就用循环来查找。循环条件:
while (新前<=新后 && 旧前<=旧后) { }
手写diff算法
diff算法的核心,当新旧节点都有children时,如何实现最小量的更新策略
import patchVnode from "./patchVnode"
import createElement from "./createElement"
// 检查是否是同一节点
function checkSameVnode(a, b) {
return a.sel === b.sel && a.key === b.key
}
export default function (parentElm, oldCn, newCn) {
// 定义指针 新前、新后,旧前、旧后
let newStartIdx = 0
let newEndIdx = newCn.length - 1
let oldStartIdx = 0
let oldEndIdx = oldCn.length - 1
// 定义新旧节点
let newStartVnode = newCn[0]
let newEndVnode = newCn[newEndIdx]
let oldStartVnode = oldCn[0]
let oldEndVnode = oldCn[oldEndIdx]
// 寻找keymap
let keyMap = null
// 开始循环语句 四种命中查找:
// 新前与旧前
// 新后与旧后
// 新后与旧前
// 新前与旧后
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 先略过加了undefined标记的那项
if (oldStartVnode == null || oldCn[oldStartIdx] == undefined) {
oldStartVnode = oldCn[++oldStartIdx]
} else if (oldEndVnode == null || oldCn[oldEndIdx] == undefined) {
oldEndVnode = oldCn[--oldEndIdx]
} else if (newStartVnode == null || newCn[newStartIdx] == undefined) {
newStartVnode = newCn[++newStartIdx]
} else if (newEndVnode == null || newCn[newEndIdx] == undefined) {
newEndVnode = newCn[--newEndIdx]
}
// 判断 新旧 前面的节点是否相同
else if (checkSameVnode(newStartVnode, oldStartVnode)) {
console.log('1) 新前与旧前');
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCn[++oldStartIdx]
newStartVnode = newCn[++newStartIdx]
} else if (checkSameVnode(newEndVnode, oldEndVnode)) {
console.log('2) 新后与旧后');
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCn[--oldEndIdx]
newEndVnode = newCn[--newEndIdx]
} else if (checkSameVnode(newEndVnode, oldStartVnode)) {
console.log('3) 新后与旧前');
// 先移动 后++
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
patchVnode(oldStartVnode, newEndVnode)
oldStartVnode = oldCn[++oldStartIdx]
newEndVnode = newCn[--newEndIdx]
} else if (checkSameVnode(newStartVnode, oldEndVnode)) {
console.log('4) 新前与旧后');
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
patchVnode(oldEndVnode, newStartVnode)
oldEndVnode = oldCn[--oldEndIdx]
newStartVnode = newCn[++newStartIdx]
} else {
console.log('四种查找都都没有命中的时候');
// 寻找 key 的 map
// 这个map很好理解呀,把old节点的存入对象中,然后看看new中节点的key是否可以在对象中找到
if (!keyMap) {
keyMap = {}
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCn[i].key
if (key !== undefined) {
keyMap[key] = i
}
}
}
const idxInOld = keyMap[newStartVnode.key]
if (idxInOld == undefined) {
// 是undefined,表示是全新的项,目前是虚拟节点不是真正的dom节点
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
} else {
// 不是undefined,不是全新的项,需要移动, 而不是删除后的新增
const elmToMove = oldCn[idxInOld]
patchVnode(elmToMove, newStartVnode)
oldCn[idxInOld] = undefined
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)
}
// 指针下移,只移动新的头
newStartVnode = newCn[++newStartIdx]
}
}
// 查找循环结束后,有没有剩余的节点
if (newStartIdx <= newEndIdx) {
console.log('新的节点上 有剩余的节点,需要新增')
const before = newCn[newEndIdx + 1] == null ? null : newCn[newEndIdx + 1].elm
for (let i = newStartIdx; i <= newEndIdx; i++) {
parentElm.insertBefore(createElement(newCn[i]), before)
}
} else if (oldStartIdx <= oldEndIdx) {
console.log('旧的节点上 有剩余的节点,需要批量移除')
// 批量删除oldStartIdx 与 oldEndIdx之间剩余的项
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCn[i]) {
parentElm.removeChild(oldCn[i].elm)
}
}
}
}