在这里探究的DOM 和diff 算法是通俗易懂的,简易版的。
虚拟DOM 是什么样的结构
比如以下这个dom节点 他的虚拟节点
<ul>
<li>hello</li>
</ul>
{
sel: 'div',
data: {},
children: [
{
sel: 'li',
data: {},
children: undefined,
test: 'hello',
elm: undefined, // 会定义为当前虚拟节点的dom
key: undefined
}
],
test: undefined,
elm: undefined, // 会定义为当前虚拟节点的dom
key: undefined
}
创建一个生成虚拟dom 的函数
export default function vnode (sel, data, children, text, elm) {
let key = data.key
return {
sel, data, children, text, elm, key
}
}
创建一个h函数 用来创建虚拟dom 节点
import vnode from './vnode.js';
// 编写一个低配版的h 函数 必须接受3个参数
// 前两个参数是固定的 , 第三个参数是不固定的
// 三种型态 '文本' [] h()
export default function h (sel, data, c) {
if (arguments.length < 3) {
throw new Error('对比起,h函数必须传三个参数')
}
if (typeof c == 'string' || typeof c == 'number') {
// 型态一
return vnode (sel, data, undefined, c ,undefined)
}else if (Array.isArray(c)) {
// 型态二
let children = [];
for (let i = 0; i< c.length; i++) {
if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error('传入的数组参数中有不是 h 函数的存在')
// 这里不用调用 c[i] 执行
// 此时只需要去收集 c[i]
}else{
children.push(c[i])
}
}
// 循环结束 说明 children 收集完毕 返回虚拟节点
return vnode(sel, data, children, undefined, undefined)
}else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
// 型态三
// 传入的是唯一的children
let children = [c]
return vnode(sel, data, children, undefined, undefined)
}else {
throw new Error('对不起参数错误')
}
}
创建完虚拟的dom 节点,就需要将虚拟节点上树。这里先梳理下流程
创建一个patch 函数,用来将虚拟dom 节点上树
import vnode from './vnode.js'
import createElement from './createElement.js'
import patchVnode from './patchVnode.js'
export default function (oldVnode, newVnode) {
// 判断第一个节点是DOM节点 还是 虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 传入的DOM 节点 要包装为 虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}
// 判断oldVnode 和 newVnode 是不是同一个节点
if (oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key) {
console.log('同一个节点')
patchVnode(oldVnode, newVnode)
}else {
console.log('不是同一个节点,暴力插入新节点,删除旧的节点')
let cnode = createElement(newVnode)
oldVnode.elm.parentNode.insertBefore(cnode, oldVnode.elm)
// 删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
精细化比较 这里创建了一个 patchVnode函数,再梳理下流程
import vnode from './vnode.js'
import createElement from './createElement.js'
import updateChildren from './updateChildren.js'
export default function patchVnode (oldVnode, newVnode) {
// 判断新旧节点是不是 同一个对象
if (newVnode == oldVnode) return ;
// 判断新newVnode 有没有 text
if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length==0)) {
// newVnode 有 text 属性
console.log('newVnode 有 text 属性')
if (newVnode.text != oldVnode.text) {
// 判断新的虚拟节点 text 和旧的不同, 那么将旧的innerText 改为新的text
oldVnode.elm.innerText = newVnode.text
}
}else {
// newVnode 有 children 属性
console.log('newVnode 有 children 属性')
// 判断 oldVnode 有没有children
if (oldVnode.children != undefined && oldVnode.children.length > 0) {
// 老的虚拟节点 有children 新老都有 最复杂的
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
} else {
// 老的没有 children 新的有children
// 清空 老的的节点
oldVnode.elm.innerHTML = '';
// 增加新的节点 遍历新的节点的子节点
for (let i=0; i< newVnode.children.length; i++) {
let ch = createElement(newVnode.children[i])
oldVnode.elm.appendChild(ch)
}
}
}
}
前面用到的createElement 函数也贴下
// 真正创建节点, 将vnode 创建为dom 不插入
export default function createElement (vnode) {
let domNode = document.createElement(vnode.sel);
// 这些的是 傻瓜版的 只能有子元素 或者 文本
if (vnode.text !== '' && (vnode.children == undefined || vnode.children.length == 0)) {
// 文本
domNode.innerText = vnode.text;
// 将孤儿节点上树 标杆节点的父节点 插入
// pivot.parentNode.insertBefore(domNode, pivot)
console.log('上树了没')
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 递归去调用 子节点 1. 什么时候结束递归 2
for (let i=0; i< vnode.children.length; i++ ){
let ch = createElement(vnode.children[i])
domNode.appendChild(ch)
}
}
vnode.elm = domNode
return vnode.elm
}
最复杂的就是新老节点都有 children 属性, 这里就会用到 diff算法
先将diff 算法中涉及到的 用语普及下
了解了算法中的 基本用语 再来看下 diff 算法的思路
具体代码如下
import createElement from './createElement.js';
import patchVnode from './patchVnode.js'
export function checkSameVnode (a, b) {
return a.sel==b.sel && a.key==b.key
}
export default function updateChildren (parentElm, oldCh, newCh) {
console.log('我是updateChildren')
console.log(oldCh, newCh)
let oldStartIdx = 0; // 旧前
let oldEndIdx = oldCh.length-1; // 旧后
let newStartIdx = 0; // 新前
let newEndIdx = newCh.length-1; // 新后
let oldStartVnode = oldCh[0]; // 旧前节点
let oldEndVnode = oldCh[oldEndIdx] // 旧后节点
let newStartVnode = newCh[0]; // 新前节点
let newEndVnode = newCh[newEndIdx]; // 新后节点
let keyMap = null
while (oldStartIdx<=oldEndIdx && newStartIdx<=newEndIdx) {
// 首先不是判断四个判断 应该先滤掉已经加undefined 标记的
if (oldStartVnode==null || oldCh[oldStartIdx]==undefined) {
oldStartVnode = oldCh[++ oldStartIdx]
} else if (oldEndVnode== null || oldCh[oldEndIdx]==undefined) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode==null || newCh[newStartIdx]==undefined) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode ==null || newCh[newEndIdx] == undefined) {
newEndVnode = newCh[--newEndIdx]
}else
if (checkSameVnode(newStartVnode, oldStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
newStartVnode = newCh[++ newStartIdx]
oldStartVnode = oldCh[++ oldStartIdx]
} else if (checkSameVnode(newEndVnode, oldEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[-- oldEndIdx]
newEndVnode = newCh[-- newEndIdx]
} else if (checkSameVnode(newEndVnode, oldStartVnode)) {
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
patchVnode(oldStartVnode, newEndVnode)
newEndVnode= newCh[--newEndIdx]
oldStartVnode = oldCh[++oldStartIdx]
} else if (checkSameVnode(newStartVnode, oldEndVnode)) {
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
patchVnode(oldEndVnode,newStartVnode)
newStartVnode = newCh[++ newStartIdx]
oldEndVnode = oldCh[-- oldEndIdx]
}else {
console.log('都没有找到')
// 寻找 key 的map
if (!keyMap) {
keyMap = {}
for(let i=oldStartIdx; i<=oldEndIdx; i++ ) {
let key = oldCh[i].key;
if (key) {
keyMap[key] = i
}
}
console.log(keyMap)
}
// 寻找当前 newStartIdx 在keyMap 中映射的序号
const idxInOld = keyMap[newStartVnode.key]
console.log(idxInOld)
if (idxInOld==undefined) {
// 如果 idxInOld 是 undefined 说明是一个全新的项
// 被加入的项就是 newStartVnode 这项 还不是真正的节点
console.log('如果 idxInOld 是 undefined 说明是一个全新的项')
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
} else {
// 如果不是 就是需要移动
// 调用patch 更新这个节点 然后移动
let elmToMove = oldCh[idxInOld];
patchVnode(elmToMove,newStartVnode)
// 把这个项设置为 undefined 表示已经处理过了
oldCh[idxInOld] = undefined
// 移动 到 oldStartVnode 之前
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)
}
// 指针移动
newStartVnode = newCh[++ newStartIdx]
}
}
// 循环结束后,看下是不是有剩余
if (newStartIdx<= newEndIdx) {
console.log('new还剩余几个节点没处理')
// insertBefore 的标杆等于 null 的时候 就是在最后面插入 和 appendChildren 一致
for (let i= newStartIdx; i<= newEndIdx; i++) {
// 新的节点是虚拟节点, 还没有创建 dom
console.log('新的节点是虚拟节点, 还没有创建 dom'+oldStartIdx)
parentElm.insertBefore(createElement(newCh[i]) ,oldCh[oldStartIdx].elm)
}
} else if (oldStartIdx<=oldEndIdx) {
console.log('old还剩余几个节点没处理')
// 批量删除 oldStart 和 oldEnd 之间的节点
for (let i = oldStartIdx; i<= oldEndIdx; i++) {
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm)
}
}
}
}
我的代码结构如下
index.js 中入口文件
import h from './my-snabbdom/h.js';
import patch from './my-snabbdom/patch.js'
const container = document.getElementById('container')
const btn = document.getElementById('btn')
var myNode = h ('ul',{}, [
h ('li', {'key': 'a'}, 'a'),
h ('li', {'key': 'b'}, 'b'),
h ('li', {'key': 'c'}, 'c'),
h ('li', {'key': 'd'}, 'd')
])
var myNode1 = h ('ul',{}, [
h ('li', {'key': 'e'}, 'e'),
h ('li', {'key': 'd'}, 'd'),
h ('li', {'key': 'c'}, 'c'),
h ('li', {'key': 'b'}, 'b'),
h ('li', {'key': 'a'}, 'a'),
])
patch(container, myNode)
btn.onclick = function () {
patch(myNode, myNode1)
}