虚拟DOM和Diff算法

108 阅读4分钟

虚拟DOM和Diff算法

1. 什么是虚拟DOM和Diff算法

  • 真实DOM:

    <div class="container">
      <h1>这是一个标签</h1>
      <ul>
        <li>vue</li>
        <li>react</li>
        <li>webpack</li>
      </ul>
    </div>
    
  • 虚拟DOM:

    {
      "sel": "div",
      "data": {
        "class": { "container": true }
      },
      "childern": [
        {
          "sel": "h1",
          "data": {},
          "text": "这是一个标签"
        },
        {
          "sel": "ul",
          "data": {},
          "children": [
            { "sel": "li", "data": {}, "text": "vue" },
            { "sel": "li", "data": {}, "text": "react" },
            { "sel": "li", "data": {}, "text": "webpack" }
          ]
        }
      ]
    }
    
  • 虚拟DOM是用JavaScript对象来描述一个DOM的层次结构,DOM中的一切属性在虚拟DOM中都有相关对应。

  • Diff算法实现的是最小量更新虚拟DOM。

  • 注意:Diff实现的最小量更新仅发生在虚拟DOM上。

2. 认识Diff算法

Diff算法是对虚拟节点进行对比,通过patch函数来对比两个虚拟DON节点,从而实现最小量更新。

diff(1)

diff(2)

patch函数

import createElement from './createElement'
import patchVNode from './patchVNode'
import VNode from './vnode'
//patch用于对新旧DOM进行比较
export default function ( oldVNode, newVNode ) {
  //如果旧的DOM是一个真实DOM,那么我们把他转化为虚拟DOM(VNode)
  if ( oldVNode.sel === '' || oldVNode.sel === undefined ) {
    oldVNode = VNode ( oldVNode.tagName.toLowerCase (), {}, [], undefined, oldVNode )
  }
  //如果newVNode和oldVNode是同一个DOM,那么我们对其进行精细化比较计算出最小更新量
  if ( oldVNode.key === newVNode.key && oldVNode.sel === newVNode.sel ) {
    patchVNode ( oldVNode, newVNode )
  } else {
    //如果newVNode和oldVNode是不同的DOM,那么我们暴力拆除oldVNode,添加newVNode
    //构造新DOM的elm属性(elm是虚拟DOM对应的真实DOM)
    let newVNodeElm = createElement ( newVNode )
    //将newVNodeElm添加到oldVNode.elm之前
    oldVNode.elm.parentNode.insertBefore ( newVNodeElm, oldVNode.elm )
    //删除oldVNode.elm
    oldVNode.elm.parentNode.removeChild ( oldVNode.elm )
  }
}

patchVNode函数

import createElement from './createElement'
import updateChildren from './updateChildren'

export default function patchVNode ( oldVNode, newVNode ) {
  //如果oldVNode === newVNode则退出函数
  if ( oldVNode === newVNode ) return;
  //如果newVNode有text属性并且newVNode没有children属性
  if ( newVNode.text !== undefined && ( newVNode.children === undefined || newVNode.children.length === 0 ) ) {
    //如果newVNode的text属性不等于oldVNode的text属性,则更改text的值
    if ( newVNode.text !== oldVNode.text ) {
      oldVNode.elm.innerText = newVNode.text
    }
  } else {
    //如果newVNode有children属性,且oldVNode也有children属性,那么需要执行更新子节点策略updateChildren
    if ( oldVNode.children !== undefined && oldVNode.children.length > 0 ) {
      updateChildren ( oldVNode.elm, oldVNode.children, newVNode.children )
    } else {
      //如果newVNode有children属性,但是oldVNode没有children属性
      //清空oldVNode.elm的text属性
      oldVNode.elm.innerHTML = ''
      //创建newVNode.children的真实DOM,并添加到oldVNode.elm中
      for ( let i = 0; i < newVNode.children.length; i++ ) {
        let childrenVNodeElm = createElement ( newVNode.children[i] )
        oldVNode.elm.appendChild ( childrenVNodeElm )
      }
    }
  }
  //函数结束时,将老节点的elm属性赋值给新节点,保持elm属性的一致性
  newVNode.elm = oldVNode.elm
}

createElement函数

//createElement用于创建一颗真实DOM树
export default function createElement ( VNode ) {
  //创建dom节点
  let domNode = document.createElement ( VNode.sel )
  //如果虚拟dom没有子节点只有文本,我们直接向节点中添加文字,同时中止递归
  if ( VNode.text !== '' && ( VNode.children === undefined || VNode.length === 0 ) ) {
    //添加文本
    domNode.innerText = VNode.text
  } else if ( Array.isArray ( VNode.children ) && VNode.children.length > 0 ) {
    //递归子节点
    for ( let i = 0; i < VNode.children.length; i++ ) {
      let ch = VNode.children[i]
      //创建子节点dom,开始递归
      let chDom = createElement ( ch )
      //向dom节点中添加子节点
      domNode.appendChild ( chDom )
    }
  }
  //向虚拟DOM中追加elm属性
  VNode.elm = domNode
  return VNode.elm
}

updateChildren函数

import createElement from './createElement'
import patchVNode from './patchVNode'

function checkSameVNode ( a, b ) {
  return a.sel === b.sel && a.key === b.key
}

export default function updateChildren ( parentElm, oldCh, newCh ) {
  //旧前指针
  let oldStartIdx = 0
  //新前指针
  let newStartIdx = 0
  //旧后指针
  let oldEndIdx = oldCh.length - 1
  //新后指针
  let newEndIdx = newCh.length - 1
  //旧前节点
  let oldStartVNode = oldCh[oldStartIdx]
  //新前节点
  let newStartVNode = newCh[newStartIdx]
  //旧后节点
  let oldEndVNode = oldCh[oldEndIdx]
  //新后节点
  let newEndVNode = newCh[newEndIdx]
  //开始while
  while ( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx ) {
    if ( !oldStartVNode ) {
      oldStartVNode = [ ++oldStartIdx ]
    } else if ( !oldEndVNode ) {
      oldEndVNode = [ --oldEndIdx ]
    } else if ( !newStartVNode ) {
      newStartVNode = [ ++newStartIdx ]
    } else if ( !newEndVNode ) {
      newEndVNode = [ --newEndIdx ]
    } else if ( checkSameVNode ( newStartVNode, oldStartVNode ) ) {
      //1.新前和旧前命中
      //对比新旧节点
      patchVNode ( oldStartVNode, newStartVNode )
      //指针移动
      oldStartVNode = oldCh[++oldStartIdx]
      newStartVNode = newCh[++newStartIdx]
    } else if ( checkSameVNode ( newEndVNode, oldEndVNode ) ) {
      //2新后和旧后命中
      patchVNode ( oldEndVNode, newEndVNode )
      oldEndVNode = oldCh[--oldEndIdx]
      newEndVNode = newCh[--newEndIdx]
    } else if ( checkSameVNode ( newEndVNode, oldStartVNode ) ) {
      //3新后和旧前命中
      patchVNode ( oldStartVNode, newEndVNode )
      //将旧前指针指向的DOM移动到旧后指针指向的DOM的下一个DOM之前
      parentElm.insertBefore ( oldStartVNode.elm, oldEndVNode.elm.nextSibling )
      oldStartVNode = oldCh[++oldStartIdx]
      newEndVNode = newCh[--newEndIdx]
    } else if ( checkSameVNode ( newStartVNode, oldEndVNode ) ) {
      //4新前和旧后命中
      patchVNode ( oldEndVNode, newStartVNode )
      //将旧后指针指向的DOM移动到旧前指针指向的DOM之前
      parentElm.insertBefore ( oldEndVNode.elm, oldStartVNode.elm )
      oldEndVNode = oldCh[--oldEndIdx]
      newStartVNode = newCh[++newStartIdx]
    } else {
      //四种命中都没有命中,遍历寻找
      //将旧的虚拟DOM的key作为键,列表的下标作为值
      const keyMap = new Map ()
      for ( let i = oldStartIdx; i <= oldEndIdx; i++ ) {
        if ( oldCh[i] ) {
          keyMap.set ( oldCh[i].key, i )
        }
      }
      //查找旧节点中是否有新前指针指向的节点
      const idxInOld = keyMap.get ( newStartVNode.key )
      if ( !idxInOld ) {
        //若没有,则在旧前指针指向的DOM前添加新DOM
        parentElm.insertBefore ( createElement ( newStartVNode ), oldStartVNode.elm )
      } else {
        //若有,则patch两个DOM,将找到的DOM移动到旧前指针指向的DOM之前,将原位置赋值为undefined
        const elmToMove = oldCh[idxInOld]
        patchVNode ( elmToMove, newStartVNode )
        oldCh[idxInOld] = undefined
        parentElm.insertBefore ( elmToMove.elm, oldStartVNode.elm )
      }
      //遍历结束,移动新前指针
      newStartVNode = newCh[++newStartIdx]
    }
  }
  //如果遍历结束后,newStartIdx <= newEndIdx,说明新节点列表的长度大于旧节点列表,需要在旧节点列表中新增
  if ( newStartIdx <= newEndIdx ) {
    const before = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null
    console.log ( before )
    for ( let i = newStartIdx; i <= newEndIdx; i++ ) {
      parentElm.insertBefore ( createElement ( newCh[i] ), before )
    }
  } else if ( oldStartIdx <= oldEndIdx ) {
    //如果遍历结束后,oldStartIdx <= oldEndIdx,说明新节点列表的长度小于旧节点列表,需要在旧节点列表中删除
    for ( let i = oldStartIdx; i <= oldEndIdx; i++ ) {
      if ( oldCh[i] )
        parentElm.removeChild ( oldCh[i].elm )
    }
  }
}