阅读 55

vue-domDiff

采用虚拟dom,可以避免大量操作dom时引起的性能消耗;虚拟dom,其实使用一个对象来描述dom元素,它包含tag、type、data、children、text等属性;domDiff过程其实是比较两个对象的过程,大致如下

patch

  • 节点比对的特点:平级对比
  • 比对规则:标签不同,直接替换;如果是文本,直接替换;标签相同,更新属性和孩子
function patch(oldVnode, vnode) {
  const isRealElement = oldVnode.nodeType;
  if (isRealElement) { // 渲染
    const oldElm = oldVnode;
    const parentElm = oldVnode.parentNode;

    let el = createElm(vnode)
    parentElm.insertBefore(el, oldElm.nextsibling);
    parentElm.removeChild(oldElm)
    return el
  }
  // 1. 标签不同,找到父级进行替换
  if(oldVnode.tag !== vnode.tag) {
    oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)
  }
  // 2.都是文本  不同时,进行替换
  if (!oldVnode.tag) { 
    if (oldVnode.text !== vnode.text) {
      oldVnode.el.textContent = vnode.text
    }
  }
  // 3.标签相同 其他 标签一样  先比较属性,然后比较孩子
  let el = vnode.el = oldVnode.el;
  updateProperties(vnode, oldVnode.data)
  let oldChildren = oldVnode.children || []
  let newChildren = vnode.children || []
  // 3.1 老和新都有孩子  3.2 老的有 新的没有  3.3 老的没有 新的有
  if (oldChildren.length > 0 && newChildren.length > 0) {
    updateChildren(el, oldChildren, newChildren)
  } else if (oldChildren.length > 0) {// 将老的删除
    el.innerHTML = ''
  } else if (newChildren.length > 0) {// 将新的插入
    for(let i = 0;i<newChildren.length;i++){
      let child = newChildren[i]
      el.appendChild(createElm(child))
    }
  }
}
复制代码

createElm

function createElm(vnode) {
  let {tag, children, key, data, text} = vnode
  if (typeof tag === 'string') {
    vnode.el = document.createElement(tag);
    updateProperties(vnode);
    children.forEach(child => {
      return vnode.el.appendChild(createElm(child))
    })
  } else {
    vnode.el = document.createTextNode(text);
  }
  return vnode.el
}
复制代码

updateProperties:更新属性

function updateProperties(vnode, oldProps={}) {
  let newProps = vnode.data || {};
  let el = vnode.el;
  // 2.style不存在,赋值为‘’
  let newStyle = newProps.style || {}
  let oldStyle = oldProps.style || {}
  for (const key in oldStyle) {
    if(!newStyle[key]) {
      el.style[key] = ''
    }
  }
  // 1. 如果新属性中不存在,就直接删除
  for (const key in oldProps) {
    if(!newProps[key]) { 
      delete el[key]
    }
  }
  // 3. 属性存在,进行替换
  for (const key in newProps) {
    if (key === 'style') {
      for (const styleName in newProps.style) {
        el.style[styleName] = newProps.style[styleName]
      }
    } else if (key === 'class') {
      el.className = newProps[key]
    } else {
      el.setAttribute(key, newProps[key])
    }
  }
}
复制代码

updateChildren:更新children

  • 正序比较:开头节点相同,新老指针后移;如果新节点比老节点多,将多的节点进行插入;如果老节点比新节点多,删除多余节点
  • 倒序比较:结尾节点相同,新老指针迁移;...
  • 交叉比较:存在两种情况,新头=老尾 | 老头=新尾
  • 乱序比较:将老节点的index和key组成索引表,判断新节点是否存在key;如果存在,就移动;不存在就插入
function updateChildren(parent, oldChildren, newChildren) {
  let oldStartIndex = 0;
  let oldStartVnode = oldChildren[0];
  let oldEndIndex = oldChildren.length - 1;
  let oldEndVnode = oldChildren[oldEndIndex];

  let newStartIndex = 0;
  let newStartVnode = newChildren[0];
  let newEndIndex = newChildren.length - 1;
  let newEndVnode = newChildren[oldEndIndex];
  function makeIndexByKey(children) { //创建索引表
    let map = {};
    children.forEach((item, index) => {
      item.key && (map[item.key] = index);
    });
    return map
  }
  let map = makeIndexByKey(oldChildren)
  while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){
    if(!oldStartVnode) { // 老指针向后移动时可能遇到null,就跳过去
      oldStartVnode = oldChildren[++oldStartIndex]
    } else if (!oldEndVnode) {
      oldEndVnode = oldChildren[--oldEndIndex]
    } else if (isSameNode(oldStartVnode, newStartVnode)) {// 正序  abc -- abcd 从前往后比
      patch(oldStartVnode, newStartVnode)
      oldStartVnode=oldChildren[++oldStartIndex]
      newStartVnode=newChildren[++newStartIndex]
    } else if (isSameNode(oldEndVnode, newEndVnode)) { // 倒序 从后往前比 abc -- dabc
      patch(oldEndVnode, newEndVnode)
      oldEndVnode=oldChildren[--oldEndIndex]
      newEndVnode=newChildren[--newEndIndex]
    } else if (isSameNode(oldStartVnode, newEndVnode)) { // 老头和新尾一样 abc--bca 交叉比
      patch(oldStartVnode, newEndVnode)
      parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
      oldStartVnode=oldChildren[++oldStartIndex]
      newEndVnode=newChildren[--newEndIndex]
    } else if (isSameNode(oldEndVnode, newStartVnode)) { // 老尾和新头一样 abc--cab 交叉比
      patch(oldEndVnode, newStartVnode)
      parent.insertBefore(oldEndVnode.el, oldStartVnode.el)
      oldEndVnode=oldChildren[--oldEndIndex]
      newStartVnode=newChildren[++newStartIndex]
    } else {// 两个列表乱序 abcd--eafcn 
      let moveIndex = map[newStartVnode.key]
      if (!moveIndex) { // 新元素在老队列中不存在 直接插入老队列头指针的前面
        parent.insertBefore(createElm(newStartVnode), oldStartVnode.el)
      } else { // 存在,移动
        let moveNode = oldChildren[moveIndex];
        patch(moveNode, newStartVnode)
        oldChildren[moveIndex] = undefined // 避免数组塌陷
        parent.insertBefore(moveNode.el, oldStartVnode.el)
      }
      newStartVnode = newChildren[++newStartIndex] // 指针后移
    }
  }
  if (newStartIndex <= newEndIndex) { // 新的元素比旧的多就插入;可能是向后插入也可能是向前插入,将元素插入到指针的前面
    for(let i = newStartIndex;i<=newEndIndex;i++) {
      let ele = newChildren[newEndIndex+1] === null ? null : newChildren[newEndIndex+1].el;
      parent.insertBefore(createElm(newChildren[i]), ele)
    }
  }
  if (oldStartIndex <= oldEndIndex) {
    for (let i = oldStartIndex;i<= oldEndIndex; i++) {
      let child = oldChildren[i]
      if (child != undefined) {
        parent.removeChild(child.el)
      }
    }
  }

}

// 比较两个节点是否相同 标签+key都一样
function isSameNode(oldVnode, newVnode) {
  return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
复制代码