react中diff算法

diff 比较

diff 比较只会同级比较,如果父节点标记为新建/删除,子节点也不会进行比较;

如果代码看的比较烦,可以直接跳到底部图形描述

单节点

判断单节点是更新之后的节点计算,即使更新之前的多节点,更新之后是单节点。

遍历更新之前的节点,如果key和type都相同,这个节点会复用,其他节点将标记为删除状态,如果没有符合的话,就将之前所有的节点都标记成删除状态。

如果更新后是文本,更新前第一个节点是文本节点,更新内容,将其他fiber标记为删除,否则都删除,创建一个新文本节点

多节点

多节点是diff算法中的重点

  1. 首先第一次遍历
js 复制代码
let resultingFirstChild = null;
let previousNewFiber = null;
// current树中的第一个子节点
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 遍历更新后的fiber节点
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  if (oldFiber.index > newIdx) {
    nextOldFiber = oldFiber;
    oldFiber = null;
  } else {
    nextOldFiber = oldFiber.sibling;
  }
  const newFiber = updateSlot(
    returnFiber,
    oldFiber,
    newChildren[newIdx],
    lanes,
  );
  if (newFiber === null) {
    if (oldFiber === null) {
      oldFiber = nextOldFiber;
    }
    break;
  }
  // 更新阶段 shouldTrackSideEffects = true
  if (shouldTrackSideEffects) {
    if (oldFiber && newFiber.alternate === null) {
      deleteChild(returnFiber, oldFiber);
    }
  }
  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
  if (previousNewFiber === null) {
    resultingFirstChild = newFiber;
  } else {
    previousNewFiber.sibling = newFiber;
  }
  previousNewFiber = newFiber;
  oldFiber = nextOldFiber;
}

首先遍历更新后的fiber节点,调用updateSlot方法进行处理,

js 复制代码
const key = oldFiber !== null ? oldFiber.key : null;

if (
  (typeof newChild === 'string' && newChild !== '') ||
  typeof newChild === 'number'
) {
  if (key !== null) {
    return null;
  }
  return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
}

if (typeof newChild === 'object' && newChild !== null) {
  switch (newChild.$$typeof) {
    case REACT_ELEMENT_TYPE: {
      if (newChild.key === key) {
        return updateElement(returnFiber, oldFiber, newChild, lanes);
      } else {
        return null;
      }
    }
    case REACT_PORTAL_TYPE: {
      if (newChild.key === key) {
        return updatePortal(returnFiber, oldFiber, newChild, lanes);
      } else {
        return null;
      }
    }
    case REACT_LAZY_TYPE: {
      const payload = newChild._payload;
      const init = newChild._init;
      return updateSlot(returnFiber, oldFiber, init(payload), lanes);
    }
  }

  if (isArray(newChild) || getIteratorFn(newChild)) {
    if (key !== null) {
      return null;
    }

    return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
  }

  throwOnInvalidObjectType(returnFiber, newChild);
}


return null;
   

如果当前节点是文本/数字,current中当前节点有存在key,返回null, newFiber == null; break; 那么当前遍历结束;如果不存在key,调用updateTextNode方法,

js 复制代码
if (current === null || current.tag !== HostText) {
  // Insert
  const created = createFiberFromText(textContent, returnFiber.mode, lanes);
  created.return = returnFiber;
  return created;
} else {
  // Update
  const existing = useFiber(current, textContent);
  existing.return = returnFiber;
  return existing;
}

如果不是文本节点就创建一个文本节点返回,如果存在oldFiber节点,将这个节点标记删除deleteChild(returnFiber, oldFiber); ,否则就用当前的节点更新一个新的节点;

如果是非文本节点,新老节点的key进行比较,如果不同就退出当前循环;如果相同调用updateElement方法,如果是elementType相同,更新当前的fiber,否则创建一个新的fiber节点;

第一次遍历为了找到从开始符合的节点,当第一个节点不符合的时候就结束;

  1. 判断新节点是否完成
js 复制代码
if (newIdx === newChildren.length) {
  deleteRemainingChildren(returnFiber, oldFiber);
  // hydrate 情况下, 默认渲染都是false
  if (getIsHydrating()) {
    const numberOfForks = newIdx;
    pushTreeFork(returnFiber, numberOfForks);
  }
  return resultingFirstChild;
}

deleteRemainingChildren方法

js 复制代码
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
  deleteChild(returnFiber, childToDelete);
  childToDelete = childToDelete.sibling;
}
return null;

如果新节点遍历完成,将旧节点剩余的都标记为删除态,并且返回新节点。

  1. 如果新节点未完成,旧节点没有完成,
js 复制代码
if (oldFiber === null) {
  // 遍历剩余新节点
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
    if (newFiber === null) {
      continue;
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
  // 忽略这个条件
  if (getIsHydrating()) {
    const numberOfForks = newIdx;
    pushTreeFork(returnFiber, numberOfForks);
  }
  return resultingFirstChild;
}

遍历剩余当前节点,调用createChild 方法,创建新的fiber节点。

  1. 调用mapRemainingChildren方法
js 复制代码
const existingChildren: Map<string | number, Fiber> = new Map();

let existingChild = currentFirstChild;
while (existingChild !== null) {
  if (existingChild.key !== null) {
    existingChildren.set(existingChild.key, existingChild);
  } else {
    existingChildren.set(existingChild.index, existingChild);
  }
  existingChild = existingChild.sibling;
}
return existingChildren;

遍历旧fiber节点,将fiber节点存到Map对象对象中,Map中以key || index 为键,fiber为值。

  1. 继续遍历剩余新节点
js 复制代码
for (; newIdx < newChildren.length; newIdx++) {
  const newFiber = updateFromMap(
    existingChildren,
    returnFiber,
    newIdx,
    newChildren[newIdx],
    lanes,
  );
  if (newFiber !== null) {
    if (shouldTrackSideEffects) {
      if (newFiber.alternate !== null) {
        existingChildren.delete(
          newFiber.key === null ? newIdx : newFiber.key,
        );
      }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
}

遍历剩余新节点,调用updateFromMap方法,

js 复制代码
if (
  (typeof newChild === 'string' && newChild !== '') ||
  typeof newChild === 'number'
) {
  const matchedFiber = existingChildren.get(newIdx) || null;
  return updateTextNode(returnFiber, matchedFiber, '' + newChild, lanes);
}

if (typeof newChild === 'object' && newChild !== null) {
  switch (newChild.$$typeof) {
    case REACT_ELEMENT_TYPE: {
      const matchedFiber =
        existingChildren.get(
          newChild.key === null ? newIdx : newChild.key,
        ) || null;
      return updateElement(returnFiber, matchedFiber, newChild, lanes);
    }
    case REACT_PORTAL_TYPE: {
      const matchedFiber =
        existingChildren.get(
          newChild.key === null ? newIdx : newChild.key,
        ) || null;
      return updatePortal(returnFiber, matchedFiber, newChild, lanes);
    }
    case REACT_LAZY_TYPE:
      const payload = newChild._payload;
      const init = newChild._init;
      return updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        init(payload),
        lanes,
      );
  }

  if (isArray(newChild) || getIteratorFn(newChild)) {
    const matchedFiber = existingChildren.get(newIdx) || null;
    return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
  }

  throwOnInvalidObjectType(returnFiber, newChild);
}

return null;

如果是字符/数字,通过index作为key查找Map的值,如果旧fiber也是一个文本就更新,否则创建一个文本节点。

如果是dom节点,以key || index 为键,查找Map中fiber,如果新老elementType相同,以旧节点更新,否则创建一个新节点。

如果新节点不为null,为更新节点,将Map中的旧节点删除,

处理lastPlacedIndex , 如果是新建节点,lastPlacedIndex 不变,否则找到current树下的index,返回lastPlacedIndexindex 中的最大值,并将lastPlacedIndex赋值;如果lastPlacedIndex > oldIndex,当前节点为移动,

遍历Map,将旧节点标记为删除。

图形对比

相同的形状代表相同的elementType,a,b,c... 代表不同的key;

  1. a,b,c 作为第一次遍历,只是更新属性,不会对节点移动删除等处理,d节点新增

2. a,b,c 作为第一次遍历,只是更新属性,不会对节点移动删除等处理,d节点删除

3. 删除旧节点,新建节点

4. 将a,b节点移动,c,d节点不动

5. d节点不动,a,b,c节点移动

  1. 第一次遍历,a节点不变,第二次遍历b,d移动,e删除,g新增;
  1. 第一次遍历,a,b遍历,但是新a节点新建,旧a节点删除,第二次遍历,创建e,h节点,删除d,e节点,f移动。

8. 第一次遍历,第一个节点新建,b不变,第二次遍历,e,h和最后一个节点新建,d,f,g节点删除,c和第五个节点不变。 (第一次遍历如果当前节点和对应index的旧节点都不存在,就认为也是相同的key,第二次遍历,当前节点不存在key,那么Map中的键是index,遍历的时候会找到旧节点对应的index的fiber)

9. 第一次遍历,第一个节点和b,第二次遍历,c节点移动,h和最后一个节点新建,d和第五个节点删除。

10. 第一次遍历,a,b节点不变,第二次遍历,第二个a节点由old节点中第二个a节点移动,第三个a新建,e节点新建,d,f节点删除。(控制台会报错,key重复)

11. 第一次遍历,a,b节点不变,第二次遍历,因为Map存储,键是key,如果重复,后面的节点把前面的节点替换掉,所以最后两个a是新建,第三个a和c移动,第二个a,b删除。

总结

react会双遍历,如果第一次遍历发现新节点和旧节点的key顺序相同(前后没有key也算相同),就不会会第二次遍历,第一次遍历之后发现没有新节点,旧节点都标记为删除,没有旧节点就将新节点标记为新增;如果上述情况不符,那么会遍历剩余的新节点,react采取的是右移策略(针对相同type和key),找到当前节点前一个非新增的节点,如果不存在,那么当前节点不变,遍历下一个节点;存在的情况下,找到old节点下的对应节点,如果当前节点在上一个节点的左侧,那么就右移,否则不变。

相关推荐
梦境之冢2 分钟前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun5 分钟前
vue VueResource & axios
前端·javascript·vue.js
m0_5485147722 分钟前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect23 分钟前
xss csrf怎么预防?
前端·xss·csrf
Calm55026 分钟前
Vue3:uv-upload图片上传
前端·vue.js
浮游本尊30 分钟前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
m0_7482398331 分钟前
前端bug调试
前端·bug
m0_7482329233 分钟前
[项目][boost搜索引擎#4] cpp-httplib使用 log.hpp 前端 测试及总结
前端·搜索引擎
新中地GIS开发老师38 分钟前
《Vue进阶教程》(12)ref的实现详细教程
前端·javascript·vue.js·arcgis·前端框架·地理信息科学·地信
m0_7482495441 分钟前
前端:base64的作用
前端