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节点下的对应节点,如果当前节点在上一个节点的左侧,那么就右移,否则不变。

相关推荐
Boilermaker19925 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子16 分钟前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上102431 分钟前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry1 小时前
Fetch 笔记
前端·javascript
拾光拾趣录1 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟1 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan1 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson1 小时前
青苔漫染待客迟
前端·设计模式·架构