Vue.js设计与实现读书笔记十一 第十一章 快速Diff算法

本文深入探讨了Vue3中采用的快速Diff算法,与Vue2的双端Diff算法进行对比,详细解释了新算法如何处理新旧子节点的添加、删除和移动。内容包括预处理过程、如何确定节点状态、移动元素的操作,以及通过构造source数组和最长递增子序列优化DOM操作。同时,给出了关键代码片段,展示了如何在实际场景中应用此算法。
摘要由CSDN通过智能技术生成

vue2采用的是双端Diff算法,而vue3采用快速Diff算法,
快速Diff算法来自ivi,inferno两个框架
快速Diff算法速度比双端Diff算法更快

11.1 相同的前置元素和后置元素

以下的分析是快速Diff算法的预处理过程:

第一种情况:新子节点多于旧子节点,新子节点有添加的,旧子节点都是可以复用的

在这里插入图片描述

如图:
j是从索引0遍历到 索引1,从序列的前面开始遍历,当新旧子节点的key不相等停止,
如图,p-1相等,p-4与p-2不等所以遍历停止,j的索引为1

newEnd和oldEnd分别从新旧子节点数组最后向数组头开始遍历,也是key不等停止,图腾,p-3相等,p-2相对
面对三个问题
1.如何用代码逻辑确定这种情况?
2.如何找到新子节点中要添加的节点?
3.这些节点该添加到哪里?

(1)先用代码确第一种条件的情况,观察发现:
当j>oldEnd 表示 旧子节点都是可以复用的
当newEnd>=j 表示 新子节点至少有一个节点不是可复用节点
把这两个条件合在一起就是第一种情况的的条件
(2)找到这些节点可以用j++来遍历知道j<=newEnd停止,这中间的都是要添加
(3)应把j++遍历的节点都添加到,newEnd+1真实DOM的前面,这样j开始加的DOM
就是远离newEnd+1真实DOM的,
代码如下:

  // 新节点多余旧节点,旧节点都是可复用节点的情况  添加

    // 满足条件,则说明从 j -> newEnd 之间的节点应作为新节点插入
    if (j > oldEnd && j <= newEnd) { // >j>oldEnd说明旧所有子节点都是可复用的,j<= newEnd 表示 新节点没有处理完
      // 锚点的索引
      const anchorIndex = newEnd + 1 // 新子节点下面最上面的可复用子节点,
      // 锚点元素
      const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
      //如果anchorIndex >= newChildren.length说明可复用节点不存在,说明底部没有可复用节点:返回null,否则返回这个可复用节点的真实DOM

      // 采用 while 循环,调用 patch 函数逐个挂载新增的节点
      // 从j开始插入到 锚点元素前面,j这个会在上面
      while (j <= newEnd) {
        patch(null, newChildren[j++], container, anchor)
      }
    }

第二中种情况:新子节点少于旧子节点,新子节点都是可复用节点,旧子节有需要删除的节点

在这里插入图片描述

如图:

j是从索引0遍历到 索引1,从序列的前面开始遍历,当新旧子节点的key不相等停止,
如图,p-1相等,p-4与p-2不等所以遍历停止,j的索引为1

newEnd和oldEnd分别从新旧子节点数组最后向数组头开始遍历,也是key不等停止,图腾,p-3相等,然后没有相对 newEnd为0 oldEnd为1
面对三个问题
1.如何用代码逻辑确定这种情况?
2.如何找到要删除的子节点?

(1)先用代码确第一种条件的情况,观察发现:
当newEnd<j 表示 新子节点都是可以复用的
当oldEnd>=j表示 旧子节点至少有一个节点不是可复用节点
把这两个条件合在一起就是第二种情况的的条件
(2)找到这些节点可以用j++来遍历知道j<=oldEnd停止,这中间的都是要卸载
(3)应把j++遍历的节点都卸载掉
代码如下:

     // 新节点少于旧节点的,新节点都是可复用节点的请  卸载

    else if (j > newEnd && j <= oldEnd) {
      // j -> oldEnd 之间的节点应该被卸载
      while (j <= oldEnd) {  // 从旧节点的j开始卸载
        unmount(oldChildren[j++])
      }
    }

11.2 判断是否需要进行DOM移动操作

上面解决新旧子节点是有问题的,两个情况都是有一个Vnode的子节点是全部可复用的,没有考虑到两个Vnode子节点都有不可复用的节点,然后没有考虑到可复用子节点是乱序的的情况,所以他只是预处理

(1)首先构造source数组,

如下图:
source数组是指新子节点中未处理的节点在整个新节点中对应旧节点的索引
如下图:第一个未处理的新子节点p-3d节点对应的旧节点索引2,那么source数组第一就存储2
下面一次类推,最后p-7没有在旧子节点中,他在source中旧是-1;
在这里插入图片描述
得到source数组有两种方法:他们的时间复杂的分别是O(n^2)和O(n),代码如下:

  // 构造 source 数组
  const count = newEnd - j + 1  // 新的一组子节点中剩余未处理节点的数量
  const source = new Array(count)
  source.fill(-1);//用-1填充

  //以j为起始索引
  const oldStart = j
  const newStart = j
  for(let i= oldStart;i<=newEnd;k++)
  {
    const oldVNode=newChildren[i]
    for(let k=newStart; k<=newEnd;k++)
    {
      //找到拥有相同key值的可复用节点
      if(oldVNode.key === newVNode.key)
      {
        patch(prevVnode,nextVNode.key)
        source[k-newStart]=i;
      }
    }

  }

 // 构造 source 数组
      const count = newEnd - j + 1  // 新的一组子节点中剩余未处理节点的数量
      const source = new Array(count)
      source.fill(-1);//用-1填充

      const oldStart = j
      const newStart = j
      let moved = false
      let pos = 0
      const keyIndex = {}
      // 生成连续可复用索引表即source数组的范围
      for(let i = newStart,b=0; i <= newEnd; i++,b++) {
        keyIndex[newChildren[i].key] = i //key对应是新子节点的key,value对应的新子节点在整个新子节点中的索引
      }
      let patched = 0

      // 遍历初旧Vnode来填充 source表,将可以复用的项,patch一下,
      for(let i = oldStart; i <= oldEnd; i++) {
        oldVNode = oldChildren[i]
        if (patched < count) {
          const k = keyIndex[oldVNode.key]

          if (typeof k !== 'undefined') { // patched相同key的数量
            newVNode = newChildren[k]
            patch(oldVNode, newVNode, container)
            patched++
            source[k - newStart] = i // 填充将新子节点,对应旧节点的索引

            // 判断是否需要移动
            if (k < pos) {
              moved = true
            } else {
              pos = k
            }
          } else {
            // 没找到
            unmount(oldVNode)
          }
        } else {
          unmount(oldVNode)
        }
      }

第一种方法是直接外层遍历旧子节点的j到oldEnd,内层遍历新子节点,找到可复用的节点,存入对应的索引source[k-newStart]=i;
第二种方法是引入一个索引表keyIndex对象,key对应是新子节点的key,value对应的新子节点在整个新子节点中的索引

我们选取第二种优化版的

(2)判断那些需要移动,

这个地方有点不太懂的,patched的是当遍历旧未处理的子节点,如果是可复用的,patched就++,count是新未处理子节点的总数, if (patched < count)这里的patched似乎不可能>count???
pos是遍历旧未处理子节点,中对应可复用子节点,在新可复用子节点的索引的最值,当i<pos表示当前的新可复用子节点需要移动,这个和普通Diff算法差不多,但普通Diff算法是直接处理,这里是用move做一个标记,表示需要移动

11.3 如何移动元素

上一节,知道需要移动,并且将可复用的节点已经用patch函数修补,即新子节点对应真实DOM内容已经改变,但是他们的位置还是旧子节点的位置

(1)计算sources的最长递增子序列

最长递增子序列是指 递增的数列,然后是最长的,例如:[0,8,4,12] 最长递增子序列就是[0,4,12]和[0,8,12] ,最长递增子序列可能有多组,这些序列不需要是数值连续和位置连续,
我们要计算sources的最长子序列,const seq = lis(source)这lis就是计算最长子序列的算法,他会得到sources最长子序列,在souurces中的索引,例如sources为:[2,3,1,-1],最长子序列是[2,3],对应他们的索引, 那么lis返回的结果是[0,1],
最长子序列对应新子节点就是不需要移动的

(2)建立i和s的索引

i的索引表示指向新的一组子节点中的最后一个节点;
s指向最长递增子序列中的最后一个元素;

如下图:
在这里插入图片描述
然后让i向上移动,如果source[i]为-1表示这新子节点是一个需要添加的子节点,那么该添加到什么对方呢?
应添加到这个节点,在整个新子节点的索引的下一个Vnode节点的真是DOM的前面,图中就是插入到p-5的真是DOM的前面
如果i没到最长子序列,说明这些都是需要移动的子节点,那么该节点需要移动到什么地方呢?其实和添加字节一样,也是将当前子节点的真是DOM,插入到下面节点真实DOM之前,只是它是直接移动,因为DOM的属性之前已经更新了,代码如下:

moved判断是否需要移动

function patchKeyedChildren(n1, n2, container) {
    const newChildren = n2.children
    const oldChildren = n1.children
    // 更新相同的前缀节点
    // 索引 j 指向新旧两组子节点的开头
    let j = 0
    let oldVNode = oldChildren[j]
    let newVNode = newChildren[j]
    // while 循环向后遍历,直到遇到拥有不同 key 值的节点为止
    while (oldVNode.key === newVNode.key) {
      // 调用 patch 函数更新
      patch(oldVNode, newVNode, container)
      j++
      oldVNode = oldChildren[j]
      newVNode = newChildren[j]
    }

    // 更新相同的后缀节点
    // 索引 oldEnd 指向旧的一组子节点的最后一个节点
    let oldEnd = oldChildren.length - 1
    // 索引 newEnd 指向新的一组子节点的最后一个节点
    let newEnd = newChildren.length - 1

    oldVNode = oldChildren[oldEnd]
    newVNode = newChildren[newEnd]

    // while 循环向前遍历,直到遇到拥有不同 key 值的节点为止
    while (oldVNode.key === newVNode.key) {
      // 调用 patch 函数更新
      patch(oldVNode, newVNode, container)
      oldEnd--
      newEnd--
      oldVNode = oldChildren[oldEnd]
      newVNode = newChildren[newEnd]
    }


    // 新节点多余旧节点,旧节点都是可复用节点的情况  添加

    // 满足条件,则说明从 j -> newEnd 之间的节点应作为新节点插入
    if (j > oldEnd && j <= newEnd) { // >j>oldEnd说明旧所有子节点都是可复用的,j<= newEnd 表示 新节点没有处理完
      // 锚点的索引
      const anchorIndex = newEnd + 1 // 新子节点下面最上面的可复用子节点,
      // 锚点元素
      const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
      //如果anchorIndex >= newChildren.length说明可复用节点不存在,说明底部没有可复用节点:返回null,否则返回这个可复用节点的真实DOM

      // 采用 while 循环,调用 patch 函数逐个挂载新增的节点
      // 从j开始插入到 锚点元素前面,j这个会在上面
      while (j <= newEnd) {
        patch(null, newChildren[j++], container, anchor)
      }
    }

    // 新节点少于旧节点的,新节点都是可复用节点的请  卸载

    else if (j > newEnd && j <= oldEnd) {
      // j -> oldEnd 之间的节点应该被卸载
      while (j <= oldEnd) {  // 从旧节点的j开始卸载
        unmount(oldChildren[j++])
      }
    }

    else { //处理新旧都不是全部可复用节点

      // 构造 source 数组
      const count = newEnd - j + 1  // 新的一组子节点中剩余未处理节点的数量
      const source = new Array(count)
      source.fill(-1);//用-1填充

      const oldStart = j
      const newStart = j
      let moved = false
      let pos = 0
      const keyIndex = {}
      // 生成连续可复用索引表即source数组的范围
      for(let i = newStart,b=0; i <= newEnd; i++,b++) {
        keyIndex[newChildren[i].key] = i //key对应是新子节点的key,value对应的新子节点在整个新子节点中的索引
      }
      let patched = 0

      // 遍历初旧Vnode来填充 source表,将可以复用的项,patch一下,
      for(let i = oldStart; i <= oldEnd; i++) {
        oldVNode = oldChildren[i]
        if (patched < count) {
          const k = keyIndex[oldVNode.key]

          if (typeof k !== 'undefined') { // patched相同key的数量
            newVNode = newChildren[k]
            patch(oldVNode, newVNode, container)
            patched++
            source[k - newStart] = i // 填充将新子节点,对应旧节点的索引

            // 判断是否需要移动
            if (k < pos) {
              moved = true
            } else {
              pos = k
            }
          } else {
            // 没找到
            unmount(oldVNode)
          }
        } else {
          unmount(oldVNode)
        }
      }
     // 上面循环完后,数据更新了,但DOM还旧Vode的顺序

      if (moved) {
        const seq = lis(source)// 递归子序列对应的索引
        // s 指向最长递增子序列的最后一个值
        let s = seq.length - 1
        let i = count - 1
        for (i; i >= 0; i--) {
          if (source[i] === -1) {
            // 说明索引为 i 的节点是全新的节点,应该将其挂载
            // 该节点在新 children 中的真实位置索引
            const pos = i + newStart
            const newVNode = newChildren[pos]
            // 该节点下一个节点的位置索引
            const nextPos = pos + 1
            // 锚点
            const anchor = nextPos < newChildren.length
              ? newChildren[nextPos].el
              : null
            // 挂载
            patch(null, newVNode, container, anchor)

          } else if (i !== seq[j]) {
            // 说明该节点需要移动
            // 该节点在新的一组子节点中的真实位置索引
            const pos = i + newStart
            const newVNode = newChildren[pos]
            // 该节点下一个节点的位置索引
            const nextPos = pos + 1
            // 锚点
            const anchor = nextPos < newChildren.length
              ? newChildren[nextPos].el
              : null
            // 移动
            insert(newVNode.el, container, anchor)
          } else {
            // 当 i === seq[j] 时,说明该位置的节点不需要移动
            // 并让 s 指向下一个位置
            s--
          }
        }
      }
    }

  }

求给定数组,递归子序列的代码:

function lis(arr) {
  const p = arr.slice()
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      u = 0
      v = result.length - 1
      while (u < v) {
        c = ((u + v) / 2) | 0
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

完整代码:

function shouldSetAsProps(el, key, value) { // 判断是否用el[]方式添加属性的函数
  if (key === 'form' && el.tagName === 'INPUT') return false //特殊的属性返回false
  return key in el  // el不存在的返回fasle
}

function createRenderer(options) {

  const {
    createElement,
    insert,
    setElementText,
    patchProps,
    createText,
    setText
  } = options

  function mountElement(vnode, container, anchor) {
    const el = vnode.el = createElement(vnode.type)
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }

    if (vnode.props) {
      for (const key in vnode.props) {
        patchProps(el, key, null, vnode.props[key])
      }
    }

    insert(el, container, anchor)
  }

  // n1 是旧,n2是新Vnode
  function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {  // 新子Vnode是一个文本节点
      if (Array.isArray(n1.children)) { // 旧的是一组子节点
        n1.children.forEach((c) => unmount(c)) //卸载这些子节点
      }
      setElementText(container, n2.children) // 给el添加文本
    } else if (Array.isArray(n2.children)) { // 新子Vnode是一一组节点
      patchKeyedChildren(n1, n2, container)
    } else {
      if (Array.isArray(n1.children)) {
        n1.children.forEach(c => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
    }
  }

  function patchKeyedChildren(n1, n2, container) {
    const newChildren = n2.children
    const oldChildren = n1.children
    // 更新相同的前缀节点
    // 索引 j 指向新旧两组子节点的开头
    let j = 0
    let oldVNode = oldChildren[j]
    let newVNode = newChildren[j]
    // while 循环向后遍历,直到遇到拥有不同 key 值的节点为止
    while (oldVNode.key === newVNode.key) {
      // 调用 patch 函数更新
      patch(oldVNode, newVNode, container)
      j++
      oldVNode = oldChildren[j]
      newVNode = newChildren[j]
    }

    // 更新相同的后缀节点
    // 索引 oldEnd 指向旧的一组子节点的最后一个节点
    let oldEnd = oldChildren.length - 1
    // 索引 newEnd 指向新的一组子节点的最后一个节点
    let newEnd = newChildren.length - 1

    oldVNode = oldChildren[oldEnd]
    newVNode = newChildren[newEnd]

    // while 循环向前遍历,直到遇到拥有不同 key 值的节点为止
    while (oldVNode.key === newVNode.key) {
      // 调用 patch 函数更新
      patch(oldVNode, newVNode, container)
      oldEnd--
      newEnd--
      oldVNode = oldChildren[oldEnd]
      newVNode = newChildren[newEnd]
    }


    // 新节点多余旧节点,旧节点都是可复用节点的情况  添加

    // 满足条件,则说明从 j -> newEnd 之间的节点应作为新节点插入
    if (j > oldEnd && j <= newEnd) { // >j>oldEnd说明旧所有子节点都是可复用的,j<= newEnd 表示 新节点没有处理完
      // 锚点的索引
      const anchorIndex = newEnd + 1 // 新子节点下面最上面的可复用子节点,
      // 锚点元素
      const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null
      //如果anchorIndex >= newChildren.length说明可复用节点不存在,说明底部没有可复用节点:返回null,否则返回这个可复用节点的真实DOM

      // 采用 while 循环,调用 patch 函数逐个挂载新增的节点
      // 从j开始插入到 锚点元素前面,j这个会在上面
      while (j <= newEnd) {
        patch(null, newChildren[j++], container, anchor)
      }
    }

    // 新节点少于旧节点的,新节点都是可复用节点的请  卸载

    else if (j > newEnd && j <= oldEnd) {
      // j -> oldEnd 之间的节点应该被卸载
      while (j <= oldEnd) {  // 从旧节点的j开始卸载
        unmount(oldChildren[j++])
      }
    }

    else { //处理新旧都不是全部可复用节点

      // 构造 source 数组
      const count = newEnd - j + 1  // 新的一组子节点中剩余未处理节点的数量
      const source = new Array(count)
      source.fill(-1);//用-1填充

      const oldStart = j
      const newStart = j
      let moved = false
      let pos = 0
      const keyIndex = {}
      // 生成连续可复用索引表即source数组的范围
      for(let i = newStart,b=0; i <= newEnd; i++,b++) {
        keyIndex[newChildren[i].key] = i //key对应是新子节点的key,value对应的新子节点在整个新子节点中的索引
      }
      let patched = 0

      // 遍历初旧Vnode来填充 source表,将可以复用的项,patch一下,
      for(let i = oldStart; i <= oldEnd; i++) {
        oldVNode = oldChildren[i]
        if (patched < count) {
          const k = keyIndex[oldVNode.key]

          if (typeof k !== 'undefined') { // patched相同key的数量
            newVNode = newChildren[k]
            patch(oldVNode, newVNode, container)
            patched++
            source[k - newStart] = i // 填充将新子节点,对应旧节点的索引

            // 判断是否需要移动
            if (k < pos) {
              moved = true
            } else {
              pos = k
            }
          } else {
            // 没找到
            unmount(oldVNode)
          }
        } else {
          unmount(oldVNode)
        }
      }
     // 上面循环完后,数据更新了,但DOM还旧Vode的顺序

      if (moved) {
        const seq = lis(source) // 递归子序列对应的索引
        // s 指向最长递增子序列的最后一个值
        let s = seq.length - 1
        let i = count - 1
        for (i; i >= 0; i--) {
          if (source[i] === -1) {
            // 说明索引为 i 的节点是全新的节点,应该将其挂载
            // 该节点在新 children 中的真实位置索引
            const pos = i + newStart
            const newVNode = newChildren[pos]
            // 该节点下一个节点的位置索引
            const nextPos = pos + 1
            // 锚点
            const anchor = nextPos < newChildren.length
              ? newChildren[nextPos].el
              : null
            // 挂载
            patch(null, newVNode, container, anchor)

          } else if (i !== seq[j]) {
            // 说明该节点需要移动
            // 该节点在新的一组子节点中的真实位置索引
            const pos = i + newStart
            const newVNode = newChildren[pos]
            // 该节点下一个节点的位置索引
            const nextPos = pos + 1
            // 锚点
            const anchor = nextPos < newChildren.length
              ? newChildren[nextPos].el
              : null
            // 移动
            insert(newVNode.el, container, anchor)
          } else {
            // 当 i === seq[j] 时,说明该位置的节点不需要移动
            // 并让 s 指向下一个位置
            s--
          }
        }
      }
    }

  }
// n1 是旧的 n2是新的
  function patchElement(n1, n2) {
    const el = n2.el = n1.el  // 把旧的el赋值给新的
    const oldProps = n1.props
    const newProps = n2.props

    for (const key in newProps) {
      if (newProps[key] !== oldProps[key]) {   // 新与旧都有的key 但值不相等
        patchProps(el, key, oldProps[key], newProps[key]) // 旧值替换为新值
      }
    }
    for (const key in oldProps) {
      if (!(key in newProps)) { // 旧的有新的没有
        patchProps(el, key, oldProps[key], null); //删除
      }
    }
    // 这一轮将el的属性给更新,

    patchChildren(n1, n2, el) // 更新el的child
  }

  // 卸载虚拟DOM下所有节点
  function unmount(vnode) {
    if (vnode.type === Fragment) {
      vnode.children.forEach(c => unmount(c))
      return
    }
    const parent = vnode.el.parentNode
    if (parent) {
      parent.removeChild(vnode.el)
    }
  }

  // n1是旧的,n2是新的

function patch(n1, n2, container, anchor) {
  if (n1 && n1.type !== n2.type) { // 
    unmount(n1)
    n1 = null
  }
  
  // 下面的是新旧节点的类型一样或者

  const { type } = n2

  if (typeof type === 'string') { //新节点是普通的标签
    if (!n1) { // 旧的不存在
      mountElement(n2, container, anchor)
    } else { // 旧的存在
      patchElement(n1, n2)
    }
  } else if (type === Text) { // 新节点是文本节点 
    if (!n1) {
      const el = n2.el = createText(n2.children)
      insert(el, container)
    } else {       // 旧节点存在
      const el = n2.el = n1.el
      if (n2.children !== n1.children) { //不一样更新文本
        setText(el, n2.children)
      }
    }
  } else if (type === Fragment) {  // 新节点是一组节点
    if (!n1) {
      n2.children.forEach(c => patch(null, c, container))
    } else {
      patchChildren(n1, n2, container) // 直接比较子节点
    }
  }
}

  function render(vnode, container) {
    if (vnode) {
      // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数进行打补丁
      patch(container._vnode, vnode, container)
    } else {
      if (container._vnode) {
        // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
        unmount(container._vnode)
      }
    }
    // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
    container._vnode = vnode
  }

  return {
    render
  }
}

const renderer = createRenderer({
  createElement(tag) {
    return document.createElement(tag)
  },
  setElementText(el, text) {
    el.textContent = text
  },
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)//el插入到anchor前面,如果anchoor不存在,就插入到最后
  },
  createText(text) {
    return document.createTextNode(text)
  },
  setText(el, text) {
    el.nodeValue = text
  },
  patchProps(el, key, prevValue, nextValue) {

    if (/^on/.test(key)) { // 这个属性是事件
      const invokers = el._vei || (el._vei = {})  el._vei存储这个Vnode的所有事件的对象,key表示事件名字,值是伪造回调函数
     // 即每一个事件有一个invoker函数,他的.value存储者多个回调函数
      let invoker = invokers[key]
      const name = key.slice(2).toLowerCase()
      if (nextValue) {  //新值存在
        if (!invoker) { // 新绑定这是事件
          invoker = el._vei[key] = (e) => {
            console.log(e.timeStamp)
            console.log(invoker.attached)
            if (e.timeStamp < invoker.attached) return //如果是绑定事件之前的触发,不执行时间
            if (Array.isArray(invoker.value)) {
              invoker.value.forEach(fn => fn(e))
            } else {
              invoker.value(e)
            }
          }
          invoker.value = nextValue // nextValue可以是一个回调函数,或者是一个回调函数的数组
          invoker.attached = performance.now();// 给元素绑定事件的时间
          el.addEventListener(name, invoker)
        } else {
          invoker.value = nextValue // 替换掉原来的回调函数
        }
      }
      else if (invoker) { // 没有新值,并且旧值存,就说明该删除这个事件对应的伪函数
        el.removeEventListener(name, invoker)
      }

    } else if (key === 'class') {
      el.className = nextValue || '' // 没有新值,为空,删除之前的
    } else if (shouldSetAsProps(el, key, nextValue)) { //如果是可写,el上存在的属性
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') { // 这个类型的值为boolean,并且新值为空,强制转为true
        el[key] = true
      } else {     // 其他情况直接添加
        el[key] = nextValue
      }
    } else { // 不可写,el上不存在的类型
      el.setAttribute(key, nextValue)
    }
  }
})

const Fragment = Symbol()
const VNode1 = {
  type: 'div',
  children: [
    { type: 'p', children: '1', key: 1 },
    { type: 'p', children: '2', key: 2 },
    { type: 'p', children: '3', key: 3 },
    { type: 'p', children: '4', key: 4 },
    { type: 'p', children: '6', key: 6 },
    { type: 'p', children: '5', key: 5 },
  ]
}
renderer.render(VNode1, document.querySelector('#app'))

const VNode2 = {
  type: 'div',
  children: [
    { type: 'p', children: '1', key: 1 },
    { type: 'p', children: '3', key: 3 },
    { type: 'p', children: '4', key: 4 },
    { type: 'p', children: '2', key: 2 },
    { type: 'p', children: '7', key: 7 },
    { type: 'p', children: '5', key: 5 },
  ]
}

setTimeout(() => {
  console.log('update')
  renderer.render(VNode2, document.querySelector('#app'))
}, 400);

function lis(arr) {
  const p = arr.slice()
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      u = 0
      v = result.length - 1
      while (u < v) {
        c = ((u + v) / 2) | 0
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值