snabbdom简单使用:
虚拟节点属性值:
- sel:元素的标签名,原版本可以输入:“div#id.class”的形式,本次低配版本只能输入标签名
- data:存储虚拟节点的key,和属性:如:props:{class:”aaa”}
- children:子节点(其中又包含了虚拟节点的各个属性)
- text:dom的innerText
- elm:如果虚拟节点已经上树,elm则指向该dom,如果没有上树,就是undefined ```javascript import { init, classModule, propsModule, styleModule, eventListenersModule, h, } from “snabbdom”;
const path = init([ classModule, propsModule, styleModule, eventListenersModule, ]) // 用h函数制作虚拟节点 const myTem = h(“div”,{props:{id:”ddd”},class:”hhh”},[ h(“li”,”hehe”), h(“li”,”hsssdehe”), h(“li”,”hessdshe”), h(“li”,”sdsdsc”), ]) const container = document.getElementById(“container”) // 使用diff算法,让虚拟节点上树,第一个参数可以是真实dom,也可以是虚拟dom path(container,myTem)
<a name="AtVgU"></a>
## 手写简版diff算法与虚拟dom
> 本项目不考虑将真实dom转换为虚拟dom
<a name="d5HWL"></a>
### 第一步,手写h函数
- 制作虚拟节点
> 本次手写是低配版h函数,必须接收三个参数。且只能处理三种参数情况:
> 1. h( tag , obj , text )
> 1. h( tag , obj , [ h(),h() ] )
> 1. h( tag , obj , h() )
```javascript
// 转换虚拟节点
export default function(sel,data,children,text,elm){
let key = data.key
return {
sel,data,children,text,elm,key
}
}
// 手写h函数,引入vnode函数返回虚拟dom
import vnode from "./vnode"
// 低配h函数,必须接收三个参数
export default function (sel, data, c) {
if (typeof data !== "object") throw new Error("第二个参数必须是object类型")
if (arguments.length != 3) throw new Error("必须是三个参数")
// 第三个参数是数字或文字,children就设置为undefined
if (typeof c == "string" || typeof c === "number") {
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) { // 如果有子节点
for (let i = 0; i < c.length; i++) {
if (!(typeof c[i] === "object" && c[i].hasOwnProperty("sel")))
throw new Error("数组中必须是h函数")
}
return vnode(sel, data, c, undefined, undefined)
} else if (typeof c === "object" && c.hasOwnProperty("sel")) {
// 第三个参数是h函数
return vnode(sel, data, [c], undefined, undefined)
} else {
throw new Error("第三个参数不合法")
}
}
第二步,将虚拟节点转换为DOM
此时返回的还是孤儿节点
// 将虚拟DOM转换为真实DOM,返回的是一个孤儿节点
export default function createElement(vnode) {
let dom = document.createElement(vnode.sel)
vnode.elm = dom // 将dom赋值给elm
if (vnode.text) {
dom.innerText = vnode.text
} else if (vnode.children && vnode.children.length > 0) {
// 虚拟dom有子节点,循环遍历
for (let i = 0; i < vnode.children.length; i++) {
let ch = vnode.children[i]
// 递归调用,将孤儿节点加到父节点上
dom.appendChild(createElement(ch))
}
}
return dom
}
第三步,diff算法并将虚拟节点上树
diff算法流程图
import vnode from "./vnode"
import patchVnode from "./patchVnode"
import createElement from "./createElement"
export default function (oldVnode, newVnode) {
// 老节点不是虚拟节点,将真实节点转换为虚拟节点
if (!oldVnode.sel) oldVnode = vnode(oldVnode.tagName, {}, [], oldVnode.innerText, oldVnode)
if (oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key) {
// 如果老节点与新节点key和标签名都一致,调用patchVnode方法
patchVnode(oldVnode, newVnode)
} else {
// 老节点和新节点不一样,直接替换老节点
let newDOM = createElement(newVnode)
oldVnode.elm.parentNode.insertBefore(newDOM, oldVnode.elm)
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
import createElement from "./createElement"
import updateChild from "./updateChild"
export default function patchVnode(oldVnode, newVnode) {
if (oldVnode === newVnode) return // 是同一个对象
if (newVnode.text != undefined) { // 新节点有text
if (oldVnode.text !== newVnode.text) oldVnode.elm.innerText = newVnode.text
} else {
if (oldVnode.children && oldVnode.children.length > 0) {
// 老节点和新节点都有children,调用updateChild方法
updateChild(oldVnode.elm, oldVnode.children, newVnode.children)
} else {
// 老节点没有children,新节点有children,把新节点的children添加到老节点上
oldVnode.elm.innerText = ""
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i]);
oldVnode.elm.appendChild(dom)
}
}
}
}
四指针对比方法
- 四指针分别包括:新节点前,新节点后,旧节点前,旧节点后
- 用于对比老节点的children与新节点的children
对比顺序依次是:
- 新前与旧前
- 如果相同就调用patchVnode方法
- 新前指针++,旧前指针++
- 新后与旧后
- 如果相同,调用patchVnode方法
- 新后指针—,旧后指针—
- 新后与旧前
- 如果相同,调用patchVnode方法
- 将旧前dom节点,调整顺序到旧后节点之后(使用insertBefore方法,可以调整顺序)
- 将旧前虚拟节点置为undefined
- 新后指针—,旧前指针++
- 新前与旧后
- 如果相同,调用patchVnode方法
- 将旧后dom节点,调整顺序到旧前节点之前
- 将旧后虚拟节点置为undefined
- 新前指针++,旧后指针—
- 如果按照以上顺序都找不到,就遍历旧节点,与新前节点进行比较
- 如果存在相同节点
- 调用patchVnode方法
- 将新前对于的旧节点,调整顺序到,旧前节点之前
- 将旧节点中相同的虚拟节点置为undefined
- 新前指针++
- 如果不存在
- 新增孤儿节点,并插入到旧前节点之前
- 新前指针++
- 如果存在相同节点
- 新前与旧前
import patchVnode from "./patchVnode"
import createElement from "./createElement"
// 参数:pdom:父节点,oldCh:旧节点的children,newCh:新节点的children
export default function updateChild(pdom, oldCh, newCh) {
// 定义4指针,初始值分别是0,和length-1
let newStarIdx = 0,
newEndIdx = newCh.length - 1
let oldStarIdx = 0,
oldEndIdx = oldCh.length - 1
// 定义4指针对应的4个虚拟节点
let newStarVnode = newCh[newStarIdx],
newEndVnode = newCh[newEndIdx]
let oldStarVnode = oldCh[oldStarIdx],
oldEndVnode = oldCh[oldEndIdx]
let keyMap = null // 用于存储key对于的索引值,不用每次都去遍历
// 循环,新前小于等于新后,并且旧前小于等于旧后
while (newStarIdx <= newEndIdx && oldStarIdx <= oldEndIdx) {
// 如果旧虚拟节点中有undefined,则改变旧前或旧后,并跳过该次循环
if (oldStarVnode == undefined) {
oldStarVnode = oldCh[++oldStarIdx] // 先自加,再运算
} else if (oldEndVnode == undefined) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStarVnode.sel == oldStarVnode.sel && newStarVnode.key == oldStarVnode.key) {
console.log("1.新前与旧前比较相同")
patchVnode(oldStarVnode, newStarVnode)
oldStarVnode = oldCh[++oldStarIdx]
newStarVnode = newCh[++newStarIdx]
} else if (newEndVnode.sel == oldEndVnode.sel && newEndVnode.key == oldEndVnode.key) {
console.log("2.新后与旧后比较相同")
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (newEndVnode.sel == oldStarVnode.sel && newEndVnode.key == oldStarVnode.key) {
console.log("3.新后与旧前比较相同")
patchVnode(oldStarVnode, newEndVnode)
// 将旧前插入到旧后之后
pdom.insertBefore(oldStarVnode.elm, oldEndVnode.elm.nextSibling)
oldCh[oldStarIdx] = undefined // 将旧前置为undefined,否则可能会出现顺序不对的情况
newEndVnode = newCh[--newEndIdx]
oldStarVnode = oldCh[++oldStarIdx]
} else if (newStarVnode.sel == oldEndVnode.sel && newStarVnode.key == oldEndVnode.key) {
console.log("4.新前与旧后比较相同")
patchVnode(oldEndVnode, newStarVnode)
pdom.insertBefore(oldEndVnode.elm, oldStarVnode.elm) // 将旧后插入到旧前之前
oldCh[oldEndIdx] = undefined // 将旧后置为undefined
oldEndVnode = oldCh[--oldEndIdx]
newStarVnode = newCh[++newStarIdx]
} else {
console.log("5.四指针都不匹配")
if (!keyMap) { // 如果遍历过旧节点,就不用再遍历了
keyMap = new Map()
// 遍历旧虚拟节点,并将key与索引对应
for (let i = oldStarIdx; i <= oldEndIdx; i++) {
if (oldCh[i].key) keyMap.set(oldCh[i].key, i)
}
}
let index = keyMap.get(newStarVnode.key) // 获取与新前节点对应的旧节点的索引
if (index && newStarVnode.sel == oldCh[index].sel) {
// 旧节点中有节点与新前节点一致
patchVnode(oldCh[index], newStarVnode)
// 将对应旧节点调整到旧前之前
pdom.insertBefore(oldCh[index].elm, oldStarVnode.elm)
newStarVnode = newCh[++newStarIdx]
oldCh[index] = undefined // 将调整位置后的,旧虚拟节点置为undefined
} else {
// 旧节点中没有匹配的,则新增节点到旧前之前
let dom = createElement(newStarVnode)
pdom.insertBefore(dom, oldStarVnode.elm)
newStarVnode = newCh[++newStarIdx]
}
}
}
// 循环结束
if (newStarIdx > newEndIdx) {
// 新前大于新后,则新节点先循环完,需要删除剩余的老节点
for (let i = oldStarIdx; i <= oldEndIdx; i++) {
if (oldCh[i]) {
pdom.removeChild(oldCh[i].elm)
}
}
} else {
// 旧节点先循环完,则需要新增剩余的新节点
for (let i = newStarIdx; i <= newEndIdx; i++) {
let dom = createElement(newCh[i]) // 创建孤儿节点
// 设置标杆,默认是旧前,如果旧前不存在就以旧后作为标杆,插入节点在旧后到旧前之间
let pole = oldStarVnode ? oldStarVnode.elm : (oldEndVnode ? oldEndVnode.elm.nextSibling : null)
pdom.insertBefore(dom, pole) // 若标杆为null,则插入到最后
}
}
}
index.js测试代码
// 测试代码
import patch from "./ylz_snabbdom/patch";
import h from "./ylz_snabbdom/h"
const container = document.getElementById("container");
const btn = document.getElementById("btn")
const vnode = h("ul", { dataOne: "one" }, [
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'),
]);
patch(container, vnode);
const vnode1 = h("ul", { dataOne: "one" }, [
h('li', {key: 'E'}, 'E'),
h('li', {key: 'Q'}, 'Q'),
h('li', {key: 'C'}, 'C'),
h('li', {key: 'D'}, 'D'),
h('li', {key: 'B'}, 'B'),
]);
btn.onclick = function () {
patch(vnode, vnode1)
}