Vue.js设计与实现读书笔记九 第九章 简单Diff算法

9.1 减少DOM操作的性能开销

这一节要解决的问题,就是第八章的时候比较Vnode新与旧的的子节点,当新与旧的子节点都是一组的情况,之前的方法是直接将所以旧的子节点,调用unmount循环全部卸载掉,这样做会导致性能开销,要先卸载,然后再重新渲染,如果能复用节点,将节省性能,特别是如果子节点变化不大,如果能精准更新旧更好了,
这里做三种假设:
(1)新子节点数量和旧子节点数量一样,只是节点内容不同,只需要一个一个子节点的Vnode比较即可
(2)新子节点数量和旧子节点数量多,那么多的部分,就调用patch(null, newChildren[i], container)添加新的子节点
(3)新子节点数量和旧子节点数量少,那么少的部分,就调用unmount(oldChildren[i])删除多的旧的子节点
注意:这里没有考虑 新旧子节点之间是的顺序是乱的,预设他们子节点顺序是一致的

代码如下:

// 比较新旧DOM的子节点
  function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') { //        新的子节点为 文本
      if (Array.isArray(n1.children)) {   // 旧的为一组节点
        n1.children.forEach((c) => unmount(c))
      }
      setElementText(container, n2.children)

    } else if (Array.isArray(n2.children)) {          // 新子节点为一组
      const oldChildren = n1.children
      const newChildren = n2.children
      const oldLen = oldChildren.length
      const newLen = newChildren.length
      const commonLength = Math.min(oldLen, newLen) // 两个子节点的长度最小值
      for (let i = 0; i < commonLength; i++) {
        patch(oldChildren[i], newChildren[i]) //按顺序最小长度比较两个的虚拟DOM
      }
      // 如果 nextLen > prevLen,将多出来的元素添加
      if (newLen > oldLen) { //新子节点长度大,则要添加元素大于 commonLength
        for (let i = commonLength; i < newLen; i++) {
          patch(null, newChildren[i], container)
        }
      } else if (oldLen > newLen) {
        // 如果 prevLen > nextLen,将多出来的元素移除
        for (let i = commonLength; i < oldLen; i++) {
          unmount(oldChildren[i])
        }
      }
    } else {
      if (Array.isArray(n1.children)) {
        n1.children.forEach(c => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
    }
  }



 // 比较虚拟DOM
  function patch(n1, n2, container) {
    if (n1 && n1.type !== n2.type) {
      unmount(n1)
      n1 = null
    }

    const { type } = n2

    if (typeof type === 'string') {
      if (!n1) { // 第一次或类型,不同
        mountElement(n2, container)
      } else {
        patchElement(n1, n2)//新旧虚拟DOM类型一样,
      }
    } 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) { //新的是Fragment
      if (!n1) {
        n2.children.forEach(c => patch(null, c, container))
      } else {
        // 比较新旧DOM的子节点
        patchChildren(n1, n2, container)
      }
    }
  }

const Fragment = Symbol()
const newVnode = {
  type: 'div',
  children: [
    { type: 'p', children: '1' },
    { type: 'p', children: '2' },
  ]
}
renderer.render(newVnode, document.querySelector('#app'))

const oldVnode = {
  type: 'div',
  children: [
    { type: 'p', children: '4' },
    { type: 'p', children: '5' },
    { type: 'p', children: '6' }
  ]
}

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


9.2 DOM复用与key的作用

这一节就是要解决上一节中,预设Vnode新旧子节点的顺序是一致的,如果他们是不一致的那么其实上面的代码和直接全部卸载,再全部添加的性能可能好一点,但不是最优解。
这里要解决的问题是如何判断新旧子节点中那些节点是共有的?
一种方法是使用新旧子节点的type来遍历新旧子节点,如果可复用(可复用不代表完全一样,也不一定是不需要更新的) 就表示他们是共有的,
但这种方法不靠谱,例如:
新子节点有个type是div,而旧的子节点中可能有多个type为div的,他不具有唯一性。
而且还有一个麻烦的问题,新旧子节点的这个相同,并不是完全相同,所以也不能直接比较对象是否相同



  function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      }
      setElementText(container, n2.children)
    } else if (Array.isArray(n2.children)) {
      const oldChildren = n1.children
      const newChildren = n2.children

      // 遍历新的 children
      for (let i = 0; i < newChildren.length; i++) {
        const newVNode = newChildren[i]
        let j = 0
        // 遍历旧的 children
        for (j; j < oldChildren.length; j++) {
          const oldVNode = oldChildren[j]
          // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
          if (newVNode.key === oldVNode.key) {
            patch(oldVNode, newVNode, container)
            break // 这里需要 break,这个比较完了,说明之后的不用比较了,所以跳出去
          }
        }
      }
      
    } else {
      if (Array.isArray(n1.children)) {
        n1.children.forEach(c => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
    }
  }

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

const oldVnode = {
  type: 'div',
  children: [
    { type: 'p', children: 'world', key: 3 },
    { type: 'p', children: '1', key: 1 },
    { type: 'p', children: '2', key: 2 }
  ]
}


注意:这里的代码只是预设新旧子节点个数相对,都是可复用的,只是位置不同,然后执行完后,新的真实的DOM的位置与旧的Vnode的位置是一样的,顺序没有更新

9.3 找到需要移动的元素

这一节解决上一节,更新的问题,
在这里插入图片描述
如图,新子节点p-3对应的可复用的旧子节点的索引最大为2,而新的子节点p-1,p-2都比
p-3的小,所以p-1 p-2需要移动,注意:这里是找到最大索引p-3,后后面的节点需要移动,前面的不需要,找最大代码如下:

function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      }
      setElementText(container, n2.children)
    } else if (Array.isArray(n2.children)) {
      const oldChildren = n1.children
      const newChildren = n2.children

      // 遍历新的 children
      for (let i = 0; i < newChildren.length; i++) {
        const newVNode = newChildren[i]
        let j = 0
        // 遍历旧的 children
        for (j; j < oldChildren.length; j++) {
          const oldVNode = oldChildren[j]
          // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
          if (newVNode.key === oldVNode.key) {
            patch(oldVNode, newVNode, container);//更新旧的子节点el的内容
            if (j < lastIndex) { 
              // 需要移动
            } else {
              // 更新 lastIndex
              lastIndex = j
            }
            break // 这里需要 break
          }
        }
      }
      
    } else {
      if (Array.isArray(n1.children)) {
        n1.children.forEach(c => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
    }
  }


const newVnode = {
  type: 'div',
  children: [
    { type: 'p', children: 'A', key: 1 },
    { type: 'p', children: 'F', key: 6 },
    { type: 'p', children: 'C', key: 3 },
    { type: 'p', children: 'E', key: 5 },
    { type: 'p', children: 'B', key: 2 },
    { type: 'p', children: 'D', key: 4 },
  ]
}


const oldVnode = {
  type: 'div',
  children: [
    { type: 'p', children: 'A', key: 1 },
    { type: 'p', children: 'B', key: 2 },
    { type: 'p', children: 'C', key: 3 },
    { type: 'p', children: 'D', key: 4 },
    { type: 'p', children: 'E', key: 5 },
    { type: 'p', children: 'F', key: 6 }
  ]
}

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

测试数据,

旧子节点新子节点索引
AA0
BF1
CC2
DE3
EB4
FD5

需要移动的Vnode如下
在这里插入图片描述
注意是新的子节点的key是乱的,旧的是按顺序的

9.4 如何移动元素

只要遇到需要移动的就移动将虚拟DOM对应的真实DOM插入到上一个新子节点上一个节点的后面

function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      }
      setElementText(container, n2.children)
    } else if (Array.isArray(n2.children)) {
      const oldChildren = n1.children
      const newChildren = n2.children

      let lastIndex = 0
      // 遍历新的 children
      for (let i = 0; i < newChildren.length; i++) {
        const newVNode = newChildren[i]
        let j = 0
        // 遍历旧的 children
        for (j; j < oldChildren.length; j++) {
          const oldVNode = oldChildren[j]
          // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
          if (newVNode.key === oldVNode.key) {
            patch(oldVNode, newVNode, container)
            if (j < lastIndex) {
              // 需要移动
              const prevVNode = newChildren[i - 1]
              if (prevVNode) {
                const anchor = prevVNode.el.nextSibling // 当新Vnode上一个Vnode的真实DOM的下一个DOM,
                insert(newVNode.el, container, anchor) // 当前新Vonde对应的DOM,插入到 当新Vnode上一个Vnode的真实DOM的下一个DOM之前
              }
            } else {
              // 更新 lastIndex
              lastIndex = j
            }
            break // 这里需要 break
          }
        }
      }
      
    } else {
      if (Array.isArray(n1.children)) {
        n1.children.forEach(c => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
    }
  }


insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)
  },

以上的代码就完成了顺序不一样的情况下,更新内容,并且让真实DOM顺序跟新,但是这里没有考虑新增节点,和删除多余节点

9.5 添加新元素

考虑到上面的添加新元素,
有两个问题要解决,1.如何找到新元素,2.他对应的Dom该添加到哪里?
对于1,可以利用newChildre这一层循环开始,设置一个标记,oldChildren结束,看是否是可复用的节点,
这样就可以找到新元素了
对于2,对应的DOM和旧元素一样,添加到他上面Vnode的真实的DOM的后面
如下代码
如果prevVNode存在就插入到他上面Vnode的Dom下面的兄弟节点的上面
如果prevVNode不存在就插入到他父节点的第一个,说明自己就是第一个

 function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      }
      setElementText(container, n2.children)
    } else if (Array.isArray(n2.children)) {
      const oldChildren = n1.children
      const newChildren = n2.children

      let lastIndex = 0
      // 遍历新的 children
      for (let i = 0; i < newChildren.length; i++) {
        const newVNode = newChildren[i]
        let j = 0
        let find = false
        // 遍历旧的 children
        for (j; j < oldChildren.length; j++) {
          const oldVNode = oldChildren[j]
          // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
          if (newVNode.key === oldVNode.key) {
            find = true // 这个newVnode是一个
            patch(oldVNode, newVNode, container)
            if (j < lastIndex) {
              // 需要移动
              const prevVNode = newChildren[i - 1]
              if (prevVNode) {
                const anchor = prevVNode.el.nextSibling ;// 为null就插入到最后,
                insert(newVNode.el, container, anchor) // anchor不为null,el插入到anchor之前,也就是prevVNode之后
              }
            } else {
              // 更新 lastIndex
              lastIndex = j
            }
            break // 这里需要 break
          }
        }
        // 这个newVnode循环完了
        if (!find) {
          const prevVNode = newChildren[i - 1] //上一个
          let anchor = null
          if (prevVNode) { //由上一个
            anchor = prevVNode.el.nextSibling
          } else {
            anchor = container.firstChild //容器的第一子节点
          }
          patch(null, newVNode, container, anchor)//挂载新节点,根据anchor插入到合适的位置
        }
      }

    } else {
      if (Array.isArray(n1.children)) {
        n1.children.forEach(c => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
    }
  }

9.6 移除不存在的元素

这一节,要解决的就是当新子节点中没有,旧子节点有的节点的删除,主要的问题是如何找到旧不可复用的子节点
解决方法很简单,当前面DOM调准完成,并且添加新的DOM后,我们将旧子节点,遍历新子节点,有不可复用的
删除即可
代码如下:

  function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      }
      setElementText(container, n2.children)
    } else if (Array.isArray(n2.children)) {
      const oldChildren = n1.children
      const newChildren = n2.children

      let lastIndex = 0
      // 遍历新的 children
      for (let i = 0; i < newChildren.length; i++) {
        const newVNode = newChildren[i]
        let j = 0
        let find = false
        // 遍历旧的 children
        for (j; j < oldChildren.length; j++) {
          const oldVNode = oldChildren[j]
          // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
          if (newVNode.key === oldVNode.key) {
            find = true
            patch(oldVNode, newVNode, container)
            if (j < lastIndex) {
              // 需要移动
              const prevVNode = newChildren[i - 1]
              if (prevVNode) {
                const anchor = prevVNode.el.nextSibling
                insert(newVNode.el, container, anchor)
              }
            } else {
              // 更新 lastIndex
              lastIndex = j
            }
            break // 这里需要 break
          }
        }
        if (!find) {
          const prevVNode = newChildren[i - 1]
          let anchor = null
          if (prevVNode) {
            anchor = prevVNode.el.nextSibling
          } else {
            anchor = container.firstChild
          }
          patch(null, newVNode, container, anchor)
        }
      }

      // 上面已经将DOM顺序调整了,添加的行DOM,遍历旧的节点
      for (let i = 0; i < oldChildren.length; i++) {
        const oldVNode = oldChildren[i]
        // 拿着旧 VNode 去新 children 中寻找相同的节点
        const has = newChildren.find(
          vnode => vnode.key === oldVNode.key
        )
        if (!has) {
          // 如果没有找到相同的节点,则移除
          unmount(oldVNode)
        }
      }
      
    } else {
      if (Array.isArray(n1.children)) {
        n1.children.forEach(c => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
    }
  }

总结:以上就实现了新,旧节点的比较,事件添加,class,style特殊处理,并添加给DOM,新旧元素的子元素使用Diff算法进行复用DOM,
简单Diff算法简单来说:
就是遍历新Vnode的子节点,找到与旧子节点一样可以复用的节点(用key来鉴别)
如过当前子节点,比之前节点索引值小,就说明要移动,移动到他上面节点真实Dom的下面,如果是新增的子节点也是一样的移动方式,旧节点等移动完和添加完真实DOM,用旧Vnode遍历比较新Vnoe,就可以找到了需要删除的DOM了

简单Diff算法移动DOM流程,并且添加多新节点
在这里插入图片描述
完整代码如下:

<div id="app"></div>

<script>

  // 识别是不是特别的属性(例如只读),或者不能使用props添加的属性
function shouldSetAsProps(el, key, value) {
  if (key === 'form' && el.tagName === 'INPUT') return false
  return key in el
}

function createRenderer(options) {

  const {
    
    // createElement,
    // insert,
    // setElementText,
    // patchProps,
    // createText,
    // setText
    createElement,// createElement(vnode.type) 根据字符串创建DOM
    insert,     // insert(el, container) 将el添加到夫标签上面
    setElementText, // setElementText(el, vnode.children) 为el内添加文字
    patchProps, //  patchProps(el, key, oldProps[key], newProps[key]) 给标签添加属性
    createText, //  createText(text)  文本节点 text是字符串
    createComment,
    setText  // 给文本节点设置el
  } = 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)
  }

  function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
      if (Array.isArray(n1.children)) {
        n1.children.forEach((c) => unmount(c))
      }
      setElementText(container, n2.children)
    } else if (Array.isArray(n2.children)) {
      const oldChildren = n1.children
      const newChildren = n2.children

      let lastIndex = 0
      // 遍历新的 children
      for (let i = 0; i < newChildren.length; i++) {
        const newVNode = newChildren[i]
        let j = 0
        let find = false
        // 遍历旧的 children
        for (j; j < oldChildren.length; j++) {
          const oldVNode = oldChildren[j]
          // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
          if (newVNode.key === oldVNode.key) {
            find = true
            patch(oldVNode, newVNode, container)
            if (j < lastIndex) {
              // 需要移动
              const prevVNode = newChildren[i - 1]
              if (prevVNode) {
                const anchor = prevVNode.el.nextSibling
                insert(newVNode.el, container, anchor)
              }
            } else {
              // 更新 lastIndex
              lastIndex = j
            }
            break // 这里需要 break
          }
        }
        if (!find) {
          const prevVNode = newChildren[i - 1]
          let anchor = null
          if (prevVNode) {
            anchor = prevVNode.el.nextSibling
          } else {
            anchor = container.firstChild
          }
          patch(null, newVNode, container, anchor)
        }
      }

      // 遍历旧的节点
      for (let i = 0; i < oldChildren.length; i++) {
        const oldVNode = oldChildren[i]
        // 拿着旧 VNode 去新 children 中寻找相同的节点
        const has = newChildren.find(
          vnode => vnode.key === oldVNode.key
        )
        if (!has) {
          // 如果没有找到相同的节点,则移除
          unmount(oldVNode)
        }
      }
      
    } else {
      if (Array.isArray(n1.children)) {
        n1.children.forEach(c => unmount(c))
      } else if (typeof n1.children === 'string') {
        setElementText(container, '')
      }
    }
  }

  function patchElement(n1, n2) {
    const el = n2.el = n1.el
    const oldProps = n1.props
    const newProps = n2.props
    
    for (const key in newProps) {
      if (newProps[key] !== oldProps[key]) {
        patchProps(el, key, oldProps[key], newProps[key])
      }
    }
    for (const key in oldProps) {
      if (!(key in newProps)) {
        patchProps(el, key, oldProps[key], null)
      }
    }

    patchChildren(n1, n2, el)
  }

  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)
    }
  }

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)
  },
  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 = {})
      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
          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)) {
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      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: 'hello', key: 3 }
  ]
}
renderer.render(VNode1, document.querySelector('#app'))

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

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




</script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值