虚拟DOM和diff算法原理和手写源码

2,189 阅读12分钟

access671233.gif

虚拟DOM和diff算法

wanglin2.github.io/VNode_visua…

虚拟DOM

背景

DOM全称文档对象模型,本质也是一个JS对象。每操作一次DOM都会对页面进行重新渲染,且新生成一颗DOM树。

缺点

  • 修改了某个数据,会直接渲染到真实dom上引起整个dom树的重绘和重排
  • 原生JS或JQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。

前端主流框架 vue 和 react 中都使用了虚拟DOM(virtual DOM)技术,因为渲染真实DOM的开销是很大的,性能代价昂贵,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,而我们只需要更新修改过的那一小块dom而不要更新整个dom,这时使用diff算法能够帮助我们。

VDOM

虚拟dom(JS模拟DOM中的真实节点对象), 通过VDom和真实DOM的比对,再通过特定的render方法将其渲染成真实的DOM节点。

  • 只会更新对应的节点。diff算法
  • 更新10个DOM节点,只会执行最后一次。例如批处理

img

diff策略

react

传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。

三大策略 将O(n^3)复杂度 转化为 O(n)复杂度

  • 策略一(tree diff):新旧DOM树,逐层对比的方式。DOM节点跨层级的操作不做优化,因为很少这么做

    1.只会对相同层级的节点进行比较;

    2.只有删除、创建操作,没有移动操作;

    img

    如图所示,react发现新树中,R节点下没有了A,那么直接删除A,在D节点下创建A以及下属节点。过程就是删除、创建,直接粗暴。

    3.由于没做性能优化,所以官方建议少做这样的跨层级操作

    img

    只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。

    当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

  • 策略二(component diff):

    如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。

    如果不是,那么直接删除旧的,创建新的;

    tips: 对于同一个类的组件,用户可以控制其不要进行diff运算,具体就是,用户可以使用shouldComponentUpdate()来告诉react要不要对此组件进行diff运算。

    img

  • 策略三(element diff):

    当节点处于同一层级时,拥有同层唯一的key值,来做删除、插入、移动的操作,这是针对element层级的策略;

    • 插入:INSERT_MARKUP,新的组件类型不在旧集合中,即全新的节点,需要对新节点进行插入操作。
    • 删除:REMOVE_NODE,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作。或者旧组件不在新集合里的,也需要执行删除操作。
    • 移动:MOVE_EXISTING,旧集合中有新组件类型,且element是可更新的类型,这时候就需要做移动操作,可以复用以前的DOM节点。

源码

www.jianshu.com/p/af0b39860…

juejin.cn/post/698493…

创建vNode对象

 const h = (tag, data = {}, children) => {
   let text = ''
   let el
   let key
   // 子元素是文本节点
   if (typeof children === 'string' || typeof children === 'number') {
     text = children
     children = undefined
   } else if (!Array.isArray(children)) {
     children = undefined
   }
   if (data && data.key) {
     key = data.key
   }
   return {
     tag, // 元素标签
     children, // 子元素
     text, // 文本节点的文本
     el, // 真实dom
     key,
     data
   }
 }

打补丁

patch函数是我们的主函数,主要用来进行新旧同级VNode的对比,找到差异来更新实际DOM,它接收两个参数,第一个参数可以是DOM元素或者是VNode,表示旧的VNode,第二参数表示新的VNode,一般只有第一次调用时才会传DOM元素,如果第一个参数为DOM元素的话我们直接忽略它的子元素把它转为一个VNode

  • 元素标签相同

    • 元素类型相同,那么新元素可以复用旧元素的dom节点

    • 执行class,style,事件的绑定等

    • 节点操作

      • 新节点的子节点是文本节点,那么就直接替换
      • 新旧节点都存在子节点,那么就要进行diff
      • 新节点存在子节点,旧节点不存在
      • 新节点不存在子节点,那么移除旧节点的所有子节点
      • 新节点啥也没有,旧节点存在文本节点
  • 标签不同,根据新的VNode创建新的dom节点,然后插入新节点,移除旧节点

 //打补丁,针对同级的节点处理
 const patchVNode = (oldVNode, newVNode) => {
   console.log('patchVNode', oldVNode, newVNode);
   if (oldVNode === newVNode) {
     return
   }
   // 元素标签相同
   if (oldVNode.tag === newVNode.tag) {
     // 元素类型相同,那么新元素可以复用旧元素的dom节点
     newVNode.el = oldVNode.el;
     let el = newVNode.el;
     console.log(oldVNode, newVNode);
     handleStyle.updateClass(el, newVNode)
     handleStyle.updateStyle(el, oldVNode, newVNode)
     handleStyle.updateAttr(el, oldVNode, newVNode)
     handleEvent.updateEvent(el, oldVNode, newVNode)
     // 新节点的子节点是文本节点,那么就直接替换
     if (newVNode.text) {
       // 移除旧节点的子节点
       if (oldVNode.children) {
         console.log(oldVNode.children);
         oldVNode.children.forEach((item) => {
           console.log(item);
           handleEvent.removeEvent(item)
         })
       }
       // 文本内容不相同则更新文本
       if (oldVNode.text !== newVNode.text) {
         el.textContent = newVNode.text
       }
     } else {
       // 新旧节点都存在子节点,那么就要进行diff
       if (oldVNode.children && newVNode.children) {
         diff(el, oldVNode.children, newVNode.children)
       } else if (newVNode.children) { // 新节点存在子节点,旧节点不存在
         // 旧节点存在文本节点则移除
         if (oldVNode.text) {
           el.textContent = ''
         }
         // 添加新节点的子节点
         newVNode.children.forEach((item, index) => {
           el.appendChild(createEl(newVNode.children[index]))
         })
       } else if (oldVNode.children) { // 新节点不存在子节点,那么移除旧节点的所有子节点
         oldVNode.children.forEach((item) => {
           handleEvent.removeEvent(item)
           el.removeChild(item.el)
         })
       } else if (oldVNode.text) { // 新节点啥也没有,旧节点存在文本节点
         el.textContent = ''
       }
     }
   } else { // 标签不同,根据新的VNode创建新的dom节点,然后插入新节点,移除旧节点
     let newEl = createEl(newVNode)
     updateClass(newEl, newVNode)
     updateStyle(newEl, null, newVNode)
     updateAttr(newEl, null, newVNode)
     handleEvent.removeEvent(oldNode)
     updateEvent(newEl, null, newVNode)
     let parent = oldVNode.el.parentNode
     parent.insertBefore(newEl, oldVNode.el)
     parent.removeChild(oldVNode.el)
   }
 }
 ​
 //入口方法
 const patch = (oldVNode, newVNode) => {
   console.log('patch', oldVNode, oldVNode.tag, oldVNode.tagName, 'oldVNode');
   // 初始化的时候,dom元素转换成vnode
   if (!oldVNode.tag) {
     let el = oldVNode
     el.innerHTML = ''
     oldVNode = h(oldVNode.tagName.toLowerCase())
     oldVNode.el = el
   }
   patchVNode(oldVNode, newVNode)
   return newVNode
 }

diff函数

image-20210629144314119.png转存失败,建议直接上传图片文件

上述四个位置的排列组合:oldStartIdxnewStartIdxoldEndIdxnewEndIdx,每当发现所比较的两个节点可能可以复用的话,那么就对这两个节点进行patch和相应操作,并更新指针进入下一轮比较,那怎么判断两个节点是否能复用呢?这就需要使用到key

  • 比较首尾节点:Vue 从新旧虚拟 DOM 树的头和尾进行双端比较:

    • 相同

      • 旧头-新头:只更新指针虚拟节点和指针
      • 旧尾-新尾:只更新指针虚拟节点和指针
      • 旧头-新尾:操作dom把旧指针节点移动到后面,并更新指针虚拟节点和指针
      • 旧尾-新头:操作dom把旧指针节点移动到前面,并更新指针虚拟节点和指针
    • 不同:通过遍历找到中间虚拟节点是否有可复用的

      • 复用:操作dom把旧指针节点移动到前面,并更新指针虚拟节点和指针,并将对应旧虚拟节点置为null
      • 不复用:根据vdom创建dom并移动到前面
  • 旧列表里存在新列表里没有的节点,需要删除

 const diff = (el, oldChildren, newChildren) => {
   console.log('diff');
   // 指针
   let oldStartIdx = 0
   let oldEndIdx = oldChildren.length - 1
   let newStartIdx = 0
   let newEndIdx = newChildren.length - 1
   // 节点
   let oldStartVNode = oldChildren[oldStartIdx]
   let oldEndVNode = oldChildren[oldEndIdx]
   let newStartVNode = newChildren[newStartIdx]
   let newEndVNode = newChildren[newEndIdx]
   //子节点个数都要大于1
   while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
     if (oldStartVNode === null) {
       oldStartVNode = oldChildren[++oldStartIdx]
     } else if (oldEndVNode === null) {
       oldEndVNode = oldChildren[--oldEndIdx]
     } else if (newStartVNode === null) {
       newStartVNode = oldChildren[++newStartIdx]
     } else if (newEndVNode === null) {
       newEndVNode = oldChildren[--newEndIdx]
     } else if (isSameNode(oldStartVNode, newStartVNode)) { // 头-头
       console.log('头-头', oldStartVNode, newStartVNode);
       // 更新指针
       oldStartVNode = oldChildren[++oldStartIdx]
       newStartVNode = newChildren[++newStartIdx]
     } else if (isSameNode(oldStartVNode, newEndVNode)) { // 头-尾
       console.log('头-尾');
       patchVNode(oldStartVNode, newEndVNode)
       // 把oldStartVNode节点移动到最后
       el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
       // 更新指针
       oldStartVNode = oldChildren[++oldStartIdx]
       newEndVNode = newChildren[--newEndIdx]
     } else if (isSameNode(oldEndVNode, newStartVNode)) { // 尾-头
       console.log('尾-头');
 ​
       patchVNode(oldEndVNode, newStartVNode)
       // 把oldEndVNode节点移动到oldStartVNode前
       el.insertBefore(oldEndVNode.el, oldStartVNode.el)
       // 更新指针
       oldEndVNode = oldChildren[--oldEndIdx]
       newStartVNode = newChildren[++newStartIdx]
     } else if (isSameNode(oldEndVNode, newEndVNode)) { // 尾-尾
       console.log('尾-尾');
 ​
       patchVNode(oldEndVNode, newEndVNode)
       // 更新指针
       oldEndVNode = oldChildren[--oldEndIdx]
       newEndVNode = newChildren[--newEndIdx]
     } else {
       console.log('insertBefore');
       let findIndex = findSameNode(oldChildren, newStartVNode)
       // newStartVNode在旧列表里不存在,那么是新节点,创建插入
       if (findIndex === -1) {
         el.insertBefore(createEl(newStartVNode), oldStartVNode.el)
       } else { // 在旧列表里存在,那么进行patch,并且移动到oldStartVNode前
         let oldVNode = oldChildren[findIndex]
         patchVNode(oldVNode, newStartVNode)
         el.insertBefore(oldVNode.el, oldStartVNode.el)
         oldChildren[findIndex] = null
       }
       newStartVNode = newChildren[++newStartIdx]
     }
   }
   // 旧列表里存在新列表里没有的节点,需要删除
   if (oldStartIdx <= oldEndIdx) {
     for (let i = oldStartIdx; i <= oldEndIdx; i++) {
       handleEvent.removeEvent(oldChildren[i])
       oldChildren[i] && el.removeChild(oldChildren[i].el)
     }
   } else if (newStartIdx <= newEndIdx) {
     let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
     for (let i = newStartIdx; i <= newEndIdx; i++) {
       el.insertBefore(createEl(newChildren[i]), before)
     }
   }
 }

处理函数

处理样式
 //处理样式
 const handleStyle = {
   updateClass: (el, newVNode) => {
     el.className = ''
     if (newVNode.data && newVNode.data.class) {
       let className = ''
       Object.keys(newVNode.data.class).forEach((cla) => {
         if (newVNode.data.class[cla]) {
           className += cla + ' '
         }
       })
       el.className = className
     }
   },
   updateStyle: (el, oldVNode, newVNode) => {
     let oldStyle = oldVNode && oldVNode.data && oldVNode.data.style || {}
     let newStyle = newVNode && newVNode.data && newVNode.data.style || {}
     // 移除旧节点里存在新节点里不存在的样式
     Object.keys(oldStyle).forEach((item) => {
       if (newStyle[item] === undefined || newStyle[item] === '') {
         el.style[item] = ''
       }
     })
     // 添加旧节点不存在的新样式
     Object.keys(newStyle).forEach((item) => {
       if (oldStyle[item] !== newStyle[item]) {
         el.style[item] = newStyle[item]
       }
     })
   },
   updateAttr: (el, oldVNode, newVNode) => {
     let oldAttr = oldVNode && oldVNode.data && oldVNode.data.attr ? oldVNode.data.attr : {}
     let newAttr = newVNode && newVNode.data && newVNode.data.attr || {}
     // 移除旧节点里存在新节点里不存在的属性
     Object.keys(oldAttr).forEach((item) => {
       if (newAttr[item] === undefined || newAttr[item] === '') {
         el.removeAttribute(item)
       }
     })
     // 添加旧节点不存在的新属性
     Object.keys(newAttr).forEach((item) => {
       if (oldAttr[item] !== newAttr[item]) {
         el.setAttribute(item, newAttr[item])
       }
     })
   }
 }
处理事件
 //处理事件
 const handleEvent = {
   removeEvent: (oldVNode) => {
     console.log();
     if (oldVNode && oldVNode.data && oldVNode.data.event) {
       Object.keys(oldVNode.data.event).forEach((item) => {
         oldVNode.el.removeEventListener(item, oldVNode.data.event[item])
       })
     }
   },
   updateEvent: (el, oldVNode, newVNode) => {
     let oldEvent = oldVNode && oldVNode.data && oldVNode.data.event ? oldVNode.data.event : {}
     let newEvent = newVNode && newVNode.data && newVNode.data.event || {}
     // 移除旧节点里存在新节点里不存在的事件
     Object.keys(oldEvent).forEach((item) => {
       if (newEvent[item] === undefined || oldEvent[item] !== newEvent[item]) {
         el.removeEventListener(item, oldEvent[item])
       }
     })
     // 添加旧节点不存在的新事件
     Object.keys(newEvent).forEach((item) => {
       if (oldEvent[item] !== newEvent[item]) {
         el.addEventListener(item, newEvent[item])
       }
     })
   }
 }

渲染dom

 //渲染dom
 const createEl = (vnode) => {
   console.log(vnode);
   let el = document.createElement(vnode.tag)
   vnode.el = el;
   if (Array.isArray(vnode) && vnode.children && vnode.children.length > 0) {
     vnode.children.forEach((item) => {
       el.appendChild(createEl(item))
     })
   } else {//重点:初始化的时候需要给虚拟节点赋值text,children。下次比较新旧节点的时候才是正确的。这里需要优化
     vnode.text = vnode.children
     vnode.children = undefined
   }
   if (vnode.text) {
     el.appendChild(document.createTextNode(vnode.text))
   }
   console.log('createEl:vnode', vnode);
   return el
 }

调用

 //调用
 let preVNode = patch(
   document.getElementById("app"),
   h(
     "div",
     {
       class: {
         btn: true,
       },
       style: {
         fontSize: "30px",
       },
       attr: {
         id: "oldId",
       },
       event: {
         mouseover: () => {
           setTimeout(() => {
             let newVNode = h(
               "div",
               {
                 class: {//类名改变
                   btn: true,
                   warning: false,
                   bg: true,
                 },
                 style: {//样式改变
                   fontWeight: "bold",
                   fontStyle: "italic",
                 },
                 attr: {
                   id: "newId",//id改变
                 },
                 event: {
                   click: () => {
                     alert("点了我");
                   },
                 },
               },
               [//reorder 移动/增加/删除 子节点
                 {
                   tag: 'h1',
                   children: '已经移入'//text 文本变了 此时不会触发节点卸载和装载,而是节点更新
                 },
                 {
                   tag: 'h3',//replace 节点类型变了 直接将旧节点卸载并装载新节点
                   children: 'item3'
                 },
               ]
             );
             console.log('preVNode:', preVNode, 'newVNode:', newVNode);
             patch(preVNode, newVNode);
           }, 1000);
         },
       },
     },
     [
       {
         tag: 'h1',
         children: '移入我'
       },
       {
         tag: 'h2',
         children: 'item1'
       },
       {
         tag: 'h2',
         children: 'item2'
       }
     ]
   )
 );

实现代码

//创建vNode对象
const h = (tag, data = {}, children) => {
  let text = ''
  let el
  let key
  // 文本节点
  if (typeof children === 'string' || typeof children === 'number') {
    text = children
    children = undefined
  } else if (!Array.isArray(children)) {
    children = undefined
  }
  if (data && data.key) {
    key = data.key
  }
  return {
    tag, // 元素标签
    children, // 子元素
    text, // 文本节点的文本
    el, // 真实dom
    key,
    data //样式,class类,事件等
  }
}

//处理样式
const handleStyle = {
  updateClass: (el, newVNode) => {
    el.className = ''
    if (newVNode.data && newVNode.data.class) {
      let className = ''
      Object.keys(newVNode.data.class).forEach((cla) => {
        if (newVNode.data.class[cla]) {
          className += cla + ' '
        }
      })
      el.className = className
    }
  },
  updateStyle: (el, oldVNode, newVNode) => {
    let oldStyle = oldVNode && oldVNode.data && oldVNode.data.style || {}
    let newStyle = newVNode && newVNode.data && newVNode.data.style || {}
    // 移除旧节点里存在新节点里不存在的样式
    Object.keys(oldStyle).forEach((item) => {
      if (newStyle[item] === undefined || newStyle[item] === '') {
        el.style[item] = ''
      }
    })
    // 添加旧节点不存在的新样式
    Object.keys(newStyle).forEach((item) => {
      if (oldStyle[item] !== newStyle[item]) {
        el.style[item] = newStyle[item]
      }
    })
  },
  updateAttr: (el, oldVNode, newVNode) => {
    let oldAttr = oldVNode && oldVNode.data && oldVNode.data.attr ? oldVNode.data.attr : {}
    let newAttr = newVNode && newVNode.data && newVNode.data.attr || {}
    // 移除旧节点里存在新节点里不存在的属性
    Object.keys(oldAttr).forEach((item) => {
      if (newAttr[item] === undefined || newAttr[item] === '') {
        el.removeAttribute(item)
      }
    })
    // 添加旧节点不存在的新属性
    Object.keys(newAttr).forEach((item) => {
      if (oldAttr[item] !== newAttr[item]) {
        el.setAttribute(item, newAttr[item])
      }
    })
  }
}

//处理事件
const handleEvent = {
  removeEvent: (oldVNode) => {
    console.log();
    if (oldVNode && oldVNode.data && oldVNode.data.event) {
      Object.keys(oldVNode.data.event).forEach((item) => {
        oldVNode.el.removeEventListener(item, oldVNode.data.event[item])
      })
    }
  },
  updateEvent: (el, oldVNode, newVNode) => {
    let oldEvent = oldVNode && oldVNode.data && oldVNode.data.event ? oldVNode.data.event : {}
    let newEvent = newVNode && newVNode.data && newVNode.data.event || {}
    // 移除旧节点里存在新节点里不存在的事件
    Object.keys(oldEvent).forEach((item) => {
      if (newEvent[item] === undefined || oldEvent[item] !== newEvent[item]) {
        el.removeEventListener(item, oldEvent[item])
      }
    })
    // 添加旧节点不存在的新事件
    Object.keys(newEvent).forEach((item) => {
      if (oldEvent[item] !== newEvent[item]) {
        el.addEventListener(item, newEvent[item])
      }
    })
  }
}

//渲染dom
const createEl = (vnode) => {
  console.log(vnode);
  let el = document.createElement(vnode.tag)
  vnode.el = el;
  if (Array.isArray(vnode) && vnode.children && vnode.children.length > 0) {
    vnode.children.forEach((item) => {
      el.appendChild(createEl(item))
    })
  } else {
    vnode.text = vnode.children
    vnode.children = undefined
  }
  if (vnode.text) {
    el.appendChild(document.createTextNode(vnode.text))
  }
  console.log('createEl:vnode', vnode);
  return el
}

//判断是否是同一个节点
const isSameNode = (a, b) => {
  return a.key === b.key && a.tag === b.tag
}

//在节点列表里寻找同个节点,返回索引
const findSameNode = (list, node) => {
  return list.findIndex((item) => {
    return item && isSameNode(item, node)
  })
}

//diff算法
const diff = (el, oldChildren, newChildren) => {
  console.log('diff');
  // 指针
  let oldStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  let newStartIdx = 0
  let newEndIdx = newChildren.length - 1
  // 节点
  let oldStartVNode = oldChildren[oldStartIdx]
  let oldEndVNode = oldChildren[oldEndIdx]
  let newStartVNode = newChildren[newStartIdx]
  let newEndVNode = newChildren[newEndIdx]
  //子节点个数都要大于1
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (oldStartVNode === null) {
      oldStartVNode = oldChildren[++oldStartIdx]
    } else if (oldEndVNode === null) {
      oldEndVNode = oldChildren[--oldEndIdx]
    } else if (newStartVNode === null) {
      newStartVNode = oldChildren[++newStartIdx]
    } else if (newEndVNode === null) {
      newEndVNode = oldChildren[--newEndIdx]
    } else if (isSameNode(oldStartVNode, newStartVNode)) { // 头-头
      console.log('头-头', oldStartVNode, newStartVNode);
      // 更新指针
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldStartVNode, newEndVNode)) { // 头-尾
      console.log('头-尾');
      patchVNode(oldStartVNode, newEndVNode)
      // 把oldStartVNode节点移动到最后
      el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
      // 更新指针
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (isSameNode(oldEndVNode, newStartVNode)) { // 尾-头
      console.log('尾-头');

      patchVNode(oldEndVNode, newStartVNode)
      // 把oldEndVNode节点移动到oldStartVNode前
      el.insertBefore(oldEndVNode.el, oldStartVNode.el)
      // 更新指针
      oldEndVNode = oldChildren[--oldEndIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (isSameNode(oldEndVNode, newEndVNode)) { // 尾-尾
      console.log('尾-尾');

      patchVNode(oldEndVNode, newEndVNode)
      // 更新指针
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else {
      console.log('insertBefore');
      let findIndex = findSameNode(oldChildren, newStartVNode)
      // newStartVNode在旧列表里不存在,那么是新节点,创建插入
      if (findIndex === -1) {
        el.insertBefore(createEl(newStartVNode), oldStartVNode.el)
      } else { // 在旧列表里存在,那么进行patch,并且移动到oldStartVNode前
        let oldVNode = oldChildren[findIndex]
        patchVNode(oldVNode, newStartVNode)
        el.insertBefore(oldVNode.el, oldStartVNode.el)
        oldChildren[findIndex] = null
      }
      newStartVNode = newChildren[++newStartIdx]
    }
  }
  // 旧列表里存在新列表里没有的节点,需要删除
  if (oldStartIdx <= oldEndIdx) {
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      handleEvent.removeEvent(oldChildren[i])
      oldChildren[i] && el.removeChild(oldChildren[i].el)
    }
  } else if (newStartIdx <= newEndIdx) {
    let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      el.insertBefore(createEl(newChildren[i]), before)
    }
  }
}

//打补丁,针对同级的节点处理
const patchVNode = (oldVNode, newVNode) => {
  console.log('patchVNode', oldVNode, newVNode);
  if (oldVNode === newVNode) {
    return
  }
  // 元素标签相同
  if (oldVNode.tag === newVNode.tag) {
    // 元素类型相同,那么新元素可以复用旧元素的dom节点
    newVNode.el = oldVNode.el;
    let el = newVNode.el;
    console.log(oldVNode, newVNode);
    handleStyle.updateClass(el, newVNode)
    handleStyle.updateStyle(el, oldVNode, newVNode)
    handleStyle.updateAttr(el, oldVNode, newVNode)
    handleEvent.updateEvent(el, oldVNode, newVNode)
    // 新节点的子节点是文本节点,那么就直接替换
    if (newVNode.text) {
      // 移除旧节点的子节点
      if (oldVNode.children) {
        console.log(oldVNode.children);
        oldVNode.children.forEach((item) => {
          console.log(item);
          handleEvent.removeEvent(item)
        })
      }
      // 文本内容不相同则更新文本
      if (oldVNode.text !== newVNode.text) {
        el.textContent = newVNode.text
      }
    } else {
      // 新旧节点都存在子节点,那么就要进行diff
      if (oldVNode.children && newVNode.children) {
        diff(el, oldVNode.children, newVNode.children)
      } else if (newVNode.children) { // 新节点存在子节点,旧节点不存在
        // 旧节点存在文本节点则移除
        if (oldVNode.text) {
          el.textContent = ''
        }
        // 添加新节点的子节点
        newVNode.children.forEach((item, index) => {
          el.appendChild(createEl(newVNode.children[index]))
        })
      } else if (oldVNode.children) { // 新节点不存在子节点,那么移除旧节点的所有子节点
        oldVNode.children.forEach((item) => {
          handleEvent.removeEvent(item)
          el.removeChild(item.el)
        })
      } else if (oldVNode.text) { // 新节点啥也没有,旧节点存在文本节点
        el.textContent = ''
      }
    }
  } else { // 标签不同,根据新的VNode创建新的dom节点,然后插入新节点,移除旧节点
    let newEl = createEl(newVNode)
    updateClass(newEl, newVNode)
    updateStyle(newEl, null, newVNode)
    updateAttr(newEl, null, newVNode)
    handleEvent.removeEvent(oldNode)
    updateEvent(newEl, null, newVNode)
    let parent = oldVNode.el.parentNode
    parent.insertBefore(newEl, oldVNode.el)
    parent.removeChild(oldVNode.el)
  }
}

//入口方法
const patch = (oldVNode, newVNode) => {
  console.log('patch', oldVNode, oldVNode.tag, oldVNode.tagName, 'oldVNode');
  // 初始化的时候,dom元素转换成vnode
  if (!oldVNode.tag) {
    let el = oldVNode
    el.innerHTML = ''
    oldVNode = h(oldVNode.tagName.toLowerCase())
    oldVNode.el = el
  }
  patchVNode(oldVNode, newVNode)
  return newVNode
}