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.keyreturn {sel,data,children,text,elm,key}}
// 手写h函数,引入vnode函数返回虚拟domimport 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就设置为undefinedif (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赋值给elmif (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) { // 新节点有textif (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:新节点的childrenexport default function updateChild(pdom, oldCh, newCh) {// 定义4指针,初始值分别是0,和length-1let newStarIdx = 0,newEndIdx = newCh.length - 1let 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 // 将旧后置为undefinedoldEndVnode = 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)}
