框架原理(一) dom diff

101 阅读2分钟

1、react dom diff的实现方式

const oldArr = [
  {
    tag: 'A',
    key: 'v-1',
    mountIndex: 0,
    children: [{ value: 'hello old v-1' }]
  },
  {
    tag: 'A',
    key: 'v-2',
    mountIndex: 1,
    children: [{ value: 'hello old v-2' }]
  },
  {
    tag: 'A',
    key: 'v-3',
    mountIndex: 2,
    children: [{ value: 'hello old v-3' }]
  }
];
const newArr = [
  {
    tag: 'A',
    key: 'v-3',
    mountIndex: 0,
    children: [{ value: 'hello new v-2' }]
  },
  {
    tag: 'A',
    key: 'v-2',
    mountIndex: 1,
    children: [{ value: 'hello new v-1' }]
  },
  {
    tag: 'A',
    key: 'v-4',
    mountIndex: 2,
    children: [{ value: 'hello new v-3' }]
  }
];
// react diff 的简单实现
function updateChildren(parentDOM = {}, oldVChildren, newVChildren) {
  let keyedOldMap = {};
  oldVChildren.forEach((oldVChild, index) => {
    let oldKey = oldVChild.key ? oldVChild.key : index;
    keyedOldMap[oldKey] = oldVChild;
  });
  let patch = []; //表示我们要打的补丁,也就是我们要进行的操作
  let lastPlacedIndex = 0;
  newVChildren.forEach((newVChild, index) => {
    newVChild.mountIndex = index;
    let newKey = newVChild.key ? newVChild.key : index;
    let oldVChild = keyedOldMap[newKey];
    if (oldVChild) {
      //如果说明老节点找到了,可以复用老节点
      //先更新
      // updateElement(oldVChild, newVChild)
      // 如果 mountIndex < lastPlacedIndex 说明要移动,位置已经变了
      if (oldVChild.mountIndex < lastPlacedIndex) {
        patch.push({
          type: 'MOVE',
          oldVChild, //把oldVChild移动互当前的索引处
          newVChild,
          mountIndex: index
        });
      }
      delete keyedOldMap[newKey]; //从Map中删已经复用好的节点
      lastPlacedIndex = Math.max(oldVChild.mountIndex, lastPlacedIndex);
    } else {
      // 老的里面没有,说明要新建
      patch.push({
        type: 'PLACEMENT',
        newVChild,
        mountIndex: index
      });
    }
  });

  //获取需要移动的元素
  let moveChildren = patch.filter(action => action.type === 'MOVE').map(action => action.oldVChild);
  // 需要删掉的dom
  const deleteChildren = Object.values(keyedOldMap).concat(moveChildren); // 移动的老元素,也要先删掉
  //遍历完成后在map留下的元素就是没有被 复用到的元素,需要全部删除
  deleteChildren.forEach(oldVChild => {
    // 从真实dom 里面删除
    // let currentDOM = findDOM(oldVChild)
    // parentDOM.removeChild(currentDOM)
  });

  console.log('deleteChildren:  ', deleteChildren);
  console.log('patch:  ', patch);
  // 更新 真实dom
  patch.forEach(action => {
    let { type, oldVChild, newVChild, mountIndex } = action;
    let childNodes = parentDOM.childNodes; //真实DOM节点集合
    // if (type === PLACEMENT) {
    //   let newDOM = createDOM(newVChild) //根据新的虚拟DOM创建新的真实DOM
    //   let childNode = childNodes[mountIndex] //获取 原来老的DOM中的对应的索引处的真实DOM
    //   if (childNode) {
    //     parentDOM.insertBefore(newDOM, childNode)
    //   } else {
    //     parentDOM.appendChild(newDOM)
    //   }
    // } else if (type === MOVE) {
    //   let oldDOM = findDOM(oldVChild)
    //   let childNode = childNodes[mountIndex] //获取 原来老的DOM中的对应的索引处的真实DOM
    //   if (childNode) {
    //     parentDOM.insertBefore(oldDOM, childNode)
    //   } else {
    //     parentDOM.appendChild(oldDOM)
    //   }
    // }
  });
}

2、vue3 dom diff的实现方式

  • 和react 完全不同,直接参照了珠峰老师的
 const patchKeydChildren = (c1, c2, el) => {
    // 内部有优化策略
    // abc    i = 0
    // abde  从头比
    let i = 0;
    let e1 = c1.length - 1; // 老儿子中最后一项的索引
    let e2 = c2.length - 1; // 新儿子中最后一项的索引
    // 从头开始 一个一个的比,遇到不同的就停止
    while (i <= e1 && i <= e2) {
      const n1 = c1[i];
      const n2 = c2[i];
      if (isSameVnodeType(n1, n2)) {
        patch(n1, n2, el); // 会递归比对子元素
      } else {
        break;
      }
      i++;
    }
    // abc // e1 = 2
    //eabc // e2 = 3 // 从后 向前 一个一个的比较,遇到不同的就停止
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1];
      const n2 = c2[e2];
      if (isSameVnodeType(n1, n2)) {
        patch(n1, n2, el);
      } else {
        break;
      }
      e1--;
      e2--;
    }
    //  只考虑 元素新增和删除的情况
    // abc => abcd  (i=3  e1=2  e2=3 )    abc  => dabc (i=0  e1=-1  e2=0 )
    // 只要i 大于了 e1 表示新增属性
    if (i > e1) {
      // 说明有新增
      if (i <= e2) {
        // 表示有新增的部分
        // 先根据e2 取他的下一个元素  和 数组长度进行比较
        const nextPos = e2 + 1;
        const anchor = nextPos < c2.length ? c2[nextPos].el : null;
        while (i <= e2) {
          patch(null, c2[i], el, anchor);
          i++;
        }
      }
      // abcd  abc (i=3  e1=3  e2=2)
    } else if (i > e2) {
      // 删除
      while (i <= e1) {
        hostRemove(c1[i].el);
        i++;
      }
    } else {
      // 无规律的情况 diff 算法
      // ab [cde] fg   // s1=2  e1=4
      // ab [edch] fg  //  s2=2  e2=5;  => [5,4,3,0]; 无视他
      const s1 = i;
      const s2 = i;
      // 新的索引 和 key 做成一个映射表
      const keyToNewIndexMap = new Map();
      for (let i = s2; i <= e2; i++) {
        const nextChild = c2[i];
        keyToNewIndexMap.set(nextChild.key, i); //{e:2,d:3,c:4,h:5}
      }
      const toBePatched = e2 - s2 + 1;
      const newIndexToOldMapIndex = new Array(toBePatched).fill(0);

      // 只是做相同属性的diff 但是位置可能还不对
      for (let i = s1; i <= e1; i++) {
        const prevChild = c1[i];
        // 老的去 新的里面找
        let newIndex = keyToNewIndexMap.get(prevChild.key); // 获取新的索引
        // 新的里面没找到
        if (newIndex == undefined) {
          hostRemove(prevChild.el); // 老的有 新的没有直接删除
        } else {
          // 新索引 对应老的 索引    为0 代表是新增加的元素
          // 数组,是以 新的索引,为key,老的索引为 value的
          newIndexToOldMapIndex[newIndex - s2] = i + 1; // [5,4,3,0]
          patch(prevChild, c2[newIndex], el); // 会复用元素,更新属性,比较儿子
        }
      }
      //  最长递增子序列 [0,1]  [0,1,2,3]
      let increasingIndexSequence = getSequence(newIndexToOldMapIndex);
      let j = increasingIndexSequence.length - 1;
      // 倒序
      for (let i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i; // [edch]   找到h的索引
        const nextChild = c2[nextIndex]; // 找到 h
        let anchor = nextIndex + 1 < c2.length ? c2[nextIndex + 1].el : null; // 找到当前元素的下一个元素
        if (newIndexToOldMapIndex[i] == 0) {
          // 这是一个新元素 直接创建插入到 当前元素的下一个即可
          patch(null, nextChild, el, anchor);
        } else {
          // 根据参照物 将节点直接移动过去  所有节点都要移动 (但是有些节点可以不动)
          if (j < 0 || i != increasingIndexSequence[j]) {
            // 此时没有考虑不动的情况
            hostInsert(nextChild.el, el, anchor);
          } else {
            j--;
          }
        }
      }
    }
  };
 // O(n logn) 性能好于 O(n^2)
      // [2, 3, 1, 5, 6, 8, 7, 9, 4]  2 3 5 6 7 9
      //    对应索引 result
      // 2   对应索引 0
      // 2 3  对应索引 0 1
      // 1 3   对应索引 2 1
      // 1 3 5   对应索引 2 1 3
      // 1 3 5 6   对应索引 2 1 3 4
      // 1 3 5 6 8   对应索引 2 1 3 4 5
      // 1 3 5 6 7   对应索引 2 1 3 4 6
      // 1 3 5 6 7 9   对应索引 2 1 3 4 5 7
      // 1 3 4 6 7 9    对应索引 2 1 8 4 6 7
      function getSequence(arr) {
        const len = arr.length;
        const result = [0];
        const p = arr.slice(0); // 里面内容不重要,和原本的数组相同 用来存放索引
        let start;
        let end;
        for (let i = 0; i < len; i++) {
          const arrI = arr[i];
          console.log('result:i ', i, result);
          if (arrI !== 0) {
            let resultLastIndex = result[result.length - 1];
            // 如果当前项 大于里面的直接 放入数组中
            if (arr[resultLastIndex] < arrI) {
              p[i] = resultLastIndex; // 并且记录位置
              result.push(i);
              continue;
              // continue终止本次循环,本次后面的代码不用执行了
              //break用于完全结束一个循环,跳出循环体执行循环后面的语句
            }
            // 二分查找 找到比当前值大的哪一个
            start = 0;
            end = result.length - 1;
            while (start < end) {
              middle = ((start + end) / 2) | 0;
              if (arr[result[middle]] < arrI) {
                start = middle + 1;
              } else {
                end = middle;
              }
            }
            // start /end 就是找的位置
            // 如果相同 或者 比当前的还大 就不换了
            if (arrI < arr[result[start]]) {
              // 才需要替换
              if (start > 0) {
                p[i] = result[start - 1]; // 记录当前的前一个 是谁
              }
              result[start] = i; // 如果找的的人比他大 需要做替换
            }
          }

          console.log('p: ', p);
        }

        let len1 = result.length; // 总个数
        let last = result[len1 - 1]; // 最后一项
        // 根据前驱节点 一个个向前查找
        while (len1-- > 0) {
          result[len1] = last;
          last = p[last];
        }
        return result;
      }
      // console.log('result: ', getSequence([1, 8, 5, 3, 4, 9, 7, 5, 0]));
      console.log('result:all:  ', getSequence([2, 3, 1, 5, 6, 8, 7, 9, 4]));

3、Axios 中的取消请求源码解析

function source(){
  let cancel;
  const promise = new Promise((resolve)=>{
    cancel = resolve
  })
  return {
    cancel:cancel,
    token:promise
  }
}
// 发请求
function axios_get(config){
  if(config.cancelToken){
    config.cancelToken.then(()=>{
      console.log("xhr.abort")
      // 取消请求的真正方法
      // xhr&&xhr.abort();
    })
  }
}
// 使用方法
const source1 = source()
axios_get({cancelToken:source1.token})
setTimeout(() => {
  source1.cancel()
}, 2000);