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)

  1. <a name="AtVgU"></a>
  2. ## 手写简版diff算法与虚拟dom
  3. > 本项目不考虑将真实dom转换为虚拟dom
  4. <a name="d5HWL"></a>
  5. ### 第一步,手写h函数
  6. - 制作虚拟节点
  7. > 本次手写是低配版h函数,必须接收三个参数。且只能处理三种参数情况:
  8. > 1. h( tag , obj , text )
  9. > 1. h( tag , obj , [ h(),h() ] )
  10. > 1. h( tag , obj , h() )
  11. ```javascript
  12. // 转换虚拟节点
  13. export default function(sel,data,children,text,elm){
  14. let key = data.key
  15. return {
  16. sel,data,children,text,elm,key
  17. }
  18. }
  1. // 手写h函数,引入vnode函数返回虚拟dom
  2. import vnode from "./vnode"
  3. // 低配h函数,必须接收三个参数
  4. export default function (sel, data, c) {
  5. if (typeof data !== "object") throw new Error("第二个参数必须是object类型")
  6. if (arguments.length != 3) throw new Error("必须是三个参数")
  7. // 第三个参数是数字或文字,children就设置为undefined
  8. if (typeof c == "string" || typeof c === "number") {
  9. return vnode(sel, data, undefined, c, undefined)
  10. } else if (Array.isArray(c)) { // 如果有子节点
  11. for (let i = 0; i < c.length; i++) {
  12. if (!(typeof c[i] === "object" && c[i].hasOwnProperty("sel")))
  13. throw new Error("数组中必须是h函数")
  14. }
  15. return vnode(sel, data, c, undefined, undefined)
  16. } else if (typeof c === "object" && c.hasOwnProperty("sel")) {
  17. // 第三个参数是h函数
  18. return vnode(sel, data, [c], undefined, undefined)
  19. } else {
  20. throw new Error("第三个参数不合法")
  21. }
  22. }

第二步,将虚拟节点转换为DOM

此时返回的还是孤儿节点

  1. // 将虚拟DOM转换为真实DOM,返回的是一个孤儿节点
  2. export default function createElement(vnode) {
  3. let dom = document.createElement(vnode.sel)
  4. vnode.elm = dom // 将dom赋值给elm
  5. if (vnode.text) {
  6. dom.innerText = vnode.text
  7. } else if (vnode.children && vnode.children.length > 0) {
  8. // 虚拟dom有子节点,循环遍历
  9. for (let i = 0; i < vnode.children.length; i++) {
  10. let ch = vnode.children[i]
  11. // 递归调用,将孤儿节点加到父节点上
  12. dom.appendChild(createElement(ch))
  13. }
  14. }
  15. return dom
  16. }

第三步,diff算法并将虚拟节点上树

diff算法流程图

diff算法和虚拟dom,参考snabbdom - 图1

  1. import vnode from "./vnode"
  2. import patchVnode from "./patchVnode"
  3. import createElement from "./createElement"
  4. export default function (oldVnode, newVnode) {
  5. // 老节点不是虚拟节点,将真实节点转换为虚拟节点
  6. if (!oldVnode.sel) oldVnode = vnode(oldVnode.tagName, {}, [], oldVnode.innerText, oldVnode)
  7. if (oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key) {
  8. // 如果老节点与新节点key和标签名都一致,调用patchVnode方法
  9. patchVnode(oldVnode, newVnode)
  10. } else {
  11. // 老节点和新节点不一样,直接替换老节点
  12. let newDOM = createElement(newVnode)
  13. oldVnode.elm.parentNode.insertBefore(newDOM, oldVnode.elm)
  14. oldVnode.elm.parentNode.removeChild(oldVnode.elm)
  15. }
  16. }
  1. import createElement from "./createElement"
  2. import updateChild from "./updateChild"
  3. export default function patchVnode(oldVnode, newVnode) {
  4. if (oldVnode === newVnode) return // 是同一个对象
  5. if (newVnode.text != undefined) { // 新节点有text
  6. if (oldVnode.text !== newVnode.text) oldVnode.elm.innerText = newVnode.text
  7. } else {
  8. if (oldVnode.children && oldVnode.children.length > 0) {
  9. // 老节点和新节点都有children,调用updateChild方法
  10. updateChild(oldVnode.elm, oldVnode.children, newVnode.children)
  11. } else {
  12. // 老节点没有children,新节点有children,把新节点的children添加到老节点上
  13. oldVnode.elm.innerText = ""
  14. for (let i = 0; i < newVnode.children.length; i++) {
  15. let dom = createElement(newVnode.children[i]);
  16. oldVnode.elm.appendChild(dom)
  17. }
  18. }
  19. }
  20. }

四指针对比方法

  • 四指针分别包括:新节点前,新节点后,旧节点前,旧节点后
  • 用于对比老节点的children与新节点的children

    对比顺序依次是:

    1. 新前与旧前
      1. 如果相同就调用patchVnode方法
      2. 新前指针++,旧前指针++
    2. 新后与旧后
      1. 如果相同,调用patchVnode方法
      2. 新后指针—,旧后指针—
    3. 新后与旧前
      1. 如果相同,调用patchVnode方法
      2. 将旧前dom节点,调整顺序到旧后节点之后(使用insertBefore方法,可以调整顺序)
      3. 将旧前虚拟节点置为undefined
      4. 新后指针—,旧前指针++
    4. 新前与旧后
      1. 如果相同,调用patchVnode方法
      2. 将旧后dom节点,调整顺序到旧前节点之前
      3. 将旧后虚拟节点置为undefined
      4. 新前指针++,旧后指针—
    5. 如果按照以上顺序都找不到,就遍历旧节点,与新前节点进行比较
      1. 如果存在相同节点
        1. 调用patchVnode方法
        2. 将新前对于的旧节点,调整顺序到,旧前节点之前
        3. 将旧节点中相同的虚拟节点置为undefined
        4. 新前指针++
      2. 如果不存在
        1. 新增孤儿节点,并插入到旧前节点之前
        2. 新前指针++
  1. import patchVnode from "./patchVnode"
  2. import createElement from "./createElement"
  3. // 参数:pdom:父节点,oldCh:旧节点的children,newCh:新节点的children
  4. export default function updateChild(pdom, oldCh, newCh) {
  5. // 定义4指针,初始值分别是0,和length-1
  6. let newStarIdx = 0,
  7. newEndIdx = newCh.length - 1
  8. let oldStarIdx = 0,
  9. oldEndIdx = oldCh.length - 1
  10. // 定义4指针对应的4个虚拟节点
  11. let newStarVnode = newCh[newStarIdx],
  12. newEndVnode = newCh[newEndIdx]
  13. let oldStarVnode = oldCh[oldStarIdx],
  14. oldEndVnode = oldCh[oldEndIdx]
  15. let keyMap = null // 用于存储key对于的索引值,不用每次都去遍历
  16. // 循环,新前小于等于新后,并且旧前小于等于旧后
  17. while (newStarIdx <= newEndIdx && oldStarIdx <= oldEndIdx) {
  18. // 如果旧虚拟节点中有undefined,则改变旧前或旧后,并跳过该次循环
  19. if (oldStarVnode == undefined) {
  20. oldStarVnode = oldCh[++oldStarIdx] // 先自加,再运算
  21. } else if (oldEndVnode == undefined) {
  22. oldEndVnode = oldCh[--oldEndIdx]
  23. } else if (newStarVnode.sel == oldStarVnode.sel && newStarVnode.key == oldStarVnode.key) {
  24. console.log("1.新前与旧前比较相同")
  25. patchVnode(oldStarVnode, newStarVnode)
  26. oldStarVnode = oldCh[++oldStarIdx]
  27. newStarVnode = newCh[++newStarIdx]
  28. } else if (newEndVnode.sel == oldEndVnode.sel && newEndVnode.key == oldEndVnode.key) {
  29. console.log("2.新后与旧后比较相同")
  30. patchVnode(oldEndVnode, newEndVnode)
  31. oldEndVnode = oldCh[--oldEndIdx]
  32. newEndVnode = newCh[--newEndIdx]
  33. } else if (newEndVnode.sel == oldStarVnode.sel && newEndVnode.key == oldStarVnode.key) {
  34. console.log("3.新后与旧前比较相同")
  35. patchVnode(oldStarVnode, newEndVnode)
  36. // 将旧前插入到旧后之后
  37. pdom.insertBefore(oldStarVnode.elm, oldEndVnode.elm.nextSibling)
  38. oldCh[oldStarIdx] = undefined // 将旧前置为undefined,否则可能会出现顺序不对的情况
  39. newEndVnode = newCh[--newEndIdx]
  40. oldStarVnode = oldCh[++oldStarIdx]
  41. } else if (newStarVnode.sel == oldEndVnode.sel && newStarVnode.key == oldEndVnode.key) {
  42. console.log("4.新前与旧后比较相同")
  43. patchVnode(oldEndVnode, newStarVnode)
  44. pdom.insertBefore(oldEndVnode.elm, oldStarVnode.elm) // 将旧后插入到旧前之前
  45. oldCh[oldEndIdx] = undefined // 将旧后置为undefined
  46. oldEndVnode = oldCh[--oldEndIdx]
  47. newStarVnode = newCh[++newStarIdx]
  48. } else {
  49. console.log("5.四指针都不匹配")
  50. if (!keyMap) { // 如果遍历过旧节点,就不用再遍历了
  51. keyMap = new Map()
  52. // 遍历旧虚拟节点,并将key与索引对应
  53. for (let i = oldStarIdx; i <= oldEndIdx; i++) {
  54. if (oldCh[i].key) keyMap.set(oldCh[i].key, i)
  55. }
  56. }
  57. let index = keyMap.get(newStarVnode.key) // 获取与新前节点对应的旧节点的索引
  58. if (index && newStarVnode.sel == oldCh[index].sel) {
  59. // 旧节点中有节点与新前节点一致
  60. patchVnode(oldCh[index], newStarVnode)
  61. // 将对应旧节点调整到旧前之前
  62. pdom.insertBefore(oldCh[index].elm, oldStarVnode.elm)
  63. newStarVnode = newCh[++newStarIdx]
  64. oldCh[index] = undefined // 将调整位置后的,旧虚拟节点置为undefined
  65. } else {
  66. // 旧节点中没有匹配的,则新增节点到旧前之前
  67. let dom = createElement(newStarVnode)
  68. pdom.insertBefore(dom, oldStarVnode.elm)
  69. newStarVnode = newCh[++newStarIdx]
  70. }
  71. }
  72. }
  73. // 循环结束
  74. if (newStarIdx > newEndIdx) {
  75. // 新前大于新后,则新节点先循环完,需要删除剩余的老节点
  76. for (let i = oldStarIdx; i <= oldEndIdx; i++) {
  77. if (oldCh[i]) {
  78. pdom.removeChild(oldCh[i].elm)
  79. }
  80. }
  81. } else {
  82. // 旧节点先循环完,则需要新增剩余的新节点
  83. for (let i = newStarIdx; i <= newEndIdx; i++) {
  84. let dom = createElement(newCh[i]) // 创建孤儿节点
  85. // 设置标杆,默认是旧前,如果旧前不存在就以旧后作为标杆,插入节点在旧后到旧前之间
  86. let pole = oldStarVnode ? oldStarVnode.elm : (oldEndVnode ? oldEndVnode.elm.nextSibling : null)
  87. pdom.insertBefore(dom, pole) // 若标杆为null,则插入到最后
  88. }
  89. }
  90. }

index.js测试代码

  1. // 测试代码
  2. import patch from "./ylz_snabbdom/patch";
  3. import h from "./ylz_snabbdom/h"
  4. const container = document.getElementById("container");
  5. const btn = document.getElementById("btn")
  6. const vnode = h("ul", { dataOne: "one" }, [
  7. h('li', {key: 'A'}, 'A'),
  8. h('li', {key: 'B'}, 'B'),
  9. h('li', {key: 'C'}, 'C'),
  10. h('li', {key: 'D'}, 'D'),
  11. h('li', {key: 'E'}, 'E'),
  12. ]);
  13. patch(container, vnode);
  14. const vnode1 = h("ul", { dataOne: "one" }, [
  15. h('li', {key: 'E'}, 'E'),
  16. h('li', {key: 'Q'}, 'Q'),
  17. h('li', {key: 'C'}, 'C'),
  18. h('li', {key: 'D'}, 'D'),
  19. h('li', {key: 'B'}, 'B'),
  20. ]);
  21. btn.onclick = function () {
  22. patch(vnode, vnode1)
  23. }