我的编程空间,编程开发者的网络收藏夹
学习永远不晚

ReactDOM-diff节点源码解析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

ReactDOM-diff节点源码解析

前言

这篇文章帮助大家梳理一下React中的dom-diff。在React中,根据新的虚拟DOM的不同,分为单节点(指的是同层级只有一个子节点),和多节点(指的是同层级有多个子节点),分别是在reconcileSingleElementreconcileChildrenArray中进行的。下面结合源码和原理图进行详解。

单节点

单节点的dom-diff是在reconcileSingleElement中进行的,而能否复用的判断依据就是将要更新的虚拟DOM的key和HTML元素的类型(即divp的区别)是否和当前(页面上正在渲染的)真实DOM的fiber一致。

如图所示,对于单节点的diff我们按照图中的流程,结合源码进行一一解读


  function reconcileSingleElement(returnFiber, currentFirstChild, element) {
    //新的虚拟DOM的key,也就是唯一标准
    const key = element.key;        // null
    let child = currentFirstChild; //老的FunctionComponent对应的fiber
    while (child !== null) {
      //判断此老fiber对应的key和新的虚拟DOM对象的key是否一样 null===null
      if (child.key === key) {
        //判断老fiber对应的类型和新虚拟DOM元素对应的类型是否相同
        if (child.type === element.type) {// p div
          deleteRemainingChildren(returnFiber, child.sibling);
          //如果key一样,类型也一样,则认为此节点可以复用
          const existing = useFiber(child, element.props);
          existing.ref = element.ref;
          existing.return = returnFiber;
          return existing;
        } else {
          //如果找到一key一样老fiber,但是类型不一样,不能此老fiber,把剩下的全部删除
          deleteRemainingChildren(returnFiber, child);
        }
      } else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }
    //因为我们现实的初次挂载,老节点currentFirstChild肯定是没有的,所以可以直接根据虚拟DOM创建新的Fiber节点
    const created = createFiberFromElement(element);
    created.ref = element.ref;
    created.return = returnFiber;
    return created;
  }

key相同,类型相同

<div>
  <div key='A'>A</div> 
  <div key='B'>B</div>
</div>
<!-- 变化到 -->
<div>
  <div key='A'>C</div>
</div>

对于上面列举到的情况,新的虚拟DOM匹配到第一个即为相同key和type,我们首先通过deleteRemainingChildren方法删除掉其它的多余的子节点(上面的 <div key='B'>B</div>),然后通过useFiber方法来复用老fiber产生新的fiber,这样就完成我们的复用。

key不同,类型相同

<div>
  <div key='A'>A</div> 
  <div key='B'>B</div>
</div>
<!-- 变化到 -->
<div>
  <div key='C'>C</div>
</div>

对于上面列举到的情况,新的虚拟DOM匹配到第一个即为不同key即使type相同也不会往下进行,通过deleteChild方法删掉第一个子节点,即<div key='A'>A</div>对应的fiber,然后再对第二个子节点<div key='B'>B</div>进行对比,发现key依然不同,继续删除,删除完成之后child === null成立,跳出while循环,通过createFiberFromElement方法根据新的虚拟DOM创建新的fiber。

key相同,类型不同

<div>
  <div key='A'>A</div> 
  <div key='B'>B</div>
</div>
<!-- 变化到 -->
<div>
  <p key='A'>C</p>
</div>

对于上面列举的情况,第一次匹配到了相同的key但是type不同,依旧是不符合复用的条件,而且此时会通过deleteRemainingChildren方法删除掉所有子节点,即不会再进行第二次比较,直接就跳出循环,通过createFiberFromElement方法根据新的虚拟DOM创建新的fiber。

多节点

多节点的diff相对于单节点的diff来说更加复杂一些。这里主要是在方法reconcileChildrenArray中进行,这个过程最多会经历三次遍历,每次完成相应的功能,下面我们结合源码来具体探究一下。

  function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
    let resultingFirstChild = null; //返回的第一个新儿子
    let previousNewFiber = null; //上一个的一个新的儿fiber
    let newIdx = 0;//用来遍历新的虚拟DOM的索引
    let oldFiber = currentFirstChild;//第一个老fiber
    let nextOldFiber = null;//下一个第fiber
    let lastPlacedIndex = 0;//上一个不需要移动的老节点的索引
    // 开始第一轮循环 如果老fiber有值,新的虚拟DOM也有值
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      //先暂下一个老fiber
      nextOldFiber = oldFiber.sibling;
      //试图更新或者试图复用老的fiber
      const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
      if (newFiber === null) {
        break;
      }
      //如果有老fiber,但是新的fiber并没有成功复用老fiber和老的真实DOM,那就删除老fiber,在提交阶段会删除真实DOM
      if (oldFiber && newFiber.alternate === null) {
        deleteChild(returnFiber, oldFiber);
      }
      //指定新fiber的位置
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;//li(A).sibling=p(B).sibling=>li(C)
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber
    }
    //新的虚拟DOM已经循环完毕
    if (newIdx === newChildren.length) {
      //删除剩下的老fiber
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
    if (oldFiber === null) {
      //如果老的 fiber已经没有了, 新的虚拟DOM还有,进入插入新节点的逻辑
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx]);
        if (newFiber === null) continue;
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        //如果previousNewFiber为null,说明这是第一个fiber
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber; //这个newFiber就是大儿子
        } else {
          //否则说明不是大儿子,就把这个newFiber添加上一个子节点后面
          previousNewFiber.sibling = newFiber;
        }
        //让newFiber成为最后一个或者说上一个子fiber
        previousNewFiber = newFiber;
      }
    }
    // 开始处理移动的情况
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    //开始遍历剩下的虚拟DOM子节点
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
      if (newFiber !== null) {
      //如果要跟踪副作用,并且有老fiber
       if (newFiber.alternate !== null) {
         existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
       }
        //指定新的fiber存放位置 ,并且给lastPlacedIndex赋值
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber; //这个newFiber就是大儿子
        } else {
          //否则说明不是大儿子,就把这个newFiber添加上一个子节点后面
          previousNewFiber.sibling = newFiber;
        }
        //让newFiber成为最后一个或者说上一个子fiber
        previousNewFiber = newFiber;
      }
    }
    //等全部处理完后,删除map中所有剩下的老fiber
    existingChildren.forEach(child => deleteChild(returnFiber, child));
    return resultingFirstChild;
  }

这段代码是比较长的,这里全部贴出来就是体现其完整性。下面帮助大家逐步的分析。

<ul key="container">
  <li key="A">A</li>
  <li key="B">B</li>
  <li key="C">C</li>
  <li key="D">D</li>
  <li key="E">E</li>
  <li key="F">F</li>
</ul>
<!-- 变化到 -->
<ul key="container">
  <li key="A">A2</li>
  <li key="C">C2</li>
  <li key="E">E2</li>
  <li key="B">B2</li>
  <li key="G">G</li>
  <li key="D">D2</li>
</ul>

第一次遍历

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      //先暂下一个老fiber
      nextOldFiber = oldFiber.sibling;
      //试图更新或者试图复用老的fiber
      const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
      if (newFiber === null) {
        break;
      }
      if (shouldTrackSideEffects) {
        //如果有老fiber,但是新的fiber并没有成功复用老fiber和老的真实DOM,那就删除老fiber,在提交阶段会删除真实DOM
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }
      //指定新fiber的位置
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;//li(A).sibling=p(B).sibling=>li(C)
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber
    }

我们所有的对比都是基于新节点的虚拟DOM和老节点的fiber,当我们对比A1和A2时,会根据updateSlot方法进行条件判断,发现他们的key和type相同,符合复用条件返回创建好的fiber,我们的操作指针都指向下一个操作节点,开始对下一个节点进行第一次遍历。

当我们对比C2和B时,因为C2和B的key并不相同,updateSlot返回null,第一次遍历break开始进入第二次遍历。

第二次遍历

if (oldFiber === null) {
  //如果老的 fiber已经没有了, 新的虚拟DOM还有,进入插入新节点的逻辑
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = createChild(returnFiber, newChildren[newIdx]);
    if (newFiber === null) continue;
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    //如果previousNewFiber为null,说明这是第一个fiber
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber; //这个newFiber就是大儿子
    } else {
      //否则说明不是大儿子,就把这个newFiber添加上一个子节点后面
      previousNewFiber.sibling = newFiber;
    }
    //让newFiber成为最后一个或者说上一个子fiber
    previousNewFiber = newFiber;
  }
}

然而oldFiber依旧是存在的,会直接进入到第三次遍历,但是我们这里带大家梳理一下,看看是如何操作的。这里的遍历主要是针对新节点还存在,但是老fiber已经没有了,即新更新的节点要多余老节点的情况,我们这里需要做的就是将剩下的新节点的fiber通过createChild创造出来。

第三次遍历

// 开始处理移动的情况
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
//开始遍历剩下的虚拟DOM子节点
for (; newIdx < newChildren.length; newIdx++) {
  const newFiber = updateFromMap(
    existingChildren,
    returnFiber,
    newIdx,
    newChildren[newIdx],
  );
  if (newFiber !== null) {
  //如果要跟踪副作用,并且有老fiber
  if (newFiber.alternate !== null) {
    existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
  }
    //指定新的fiber存放位置 ,并且给lastPlacedIndex赋值
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber; //这个newFiber就是大儿子
    } else {
      //否则说明不是大儿子,就把这个newFiber添加上一个子节点后面
      previousNewFiber.sibling = newFiber;
    }
    //让newFiber成为最后一个或者说上一个子fiber
    previousNewFiber = newFiber;
  }
}
function mapRemainingChildren(returnFiber, currentFirstChild) {
  const existingChildren = new Map();
  let existingChild = currentFirstChild;
  while (existingChild != null) {
    //如果有key用key,如果没有key使用索引
    if (existingChild.key !== null) {
      existingChildren.set(existingChild.key, existingChild);
    } else {
      existingChildren.set(existingChild.index, existingChild);
    }
    existingChild = existingChild.sibling;
  }
  return existingChildren;
}

接下来我们进行第三次遍历,也就是我们节点移动的情况,这里的复用是比较复杂了。

首先我们会创造一个Map来承接所有的剩余的老节点,接下来我们会根据key,或者index,来挑选老节点以供复用。找到一个能复用的节点,就会在Map中删除对应的节点,如果有对应的点就复用,没有就新创建节点。

  • 多个节点数量不同、key 不同;
  • 第一轮比较 A 和 A,相同可以复用,更新,然后比较 B 和 C,key 不同直接跳出第一个循环;
  • 把剩下 oldFiber 的放入 existingChildren 这个 map 中;
  • 然后声明一个lastPlacedIndex变量,表示不需要移动的老节点的索引;
  • 继续循环剩下的虚拟 DOM 节点;
  • 如果能在 map 中找到相同 key 相同 type 的节点则可以复用老 fiber,并把此老 fiber 从 map 中删除;
  • 如果能在 map 中找不到相同 key 相同 type 的节点则创建新的 fiber;
  • 如果是复用老的 fiber,则判断老 fiber 的索引是否小于 lastPlacedIndex,如果是要移动老 fiber,不变;
  • 如果是复用老的 fiber,则判断老 fiber 的索引是否小于 lastPlacedIndex,如果否则更新 lastPlacedIndex 为老 fiber 的 index;
  • 把所有的 map 中剩下的 fiber 全部标记为删除;
  • (删除#li#F)=>(添加#li#B)=>(添加#li#G)=>(添加#li#D)=>null;

总结

DOM DIFF 的三个规则

  • 只对同级元素进行比较,不同层级不对比
  • 不同的类型对应不同的元素
  • 可以通过 key 来标识同一个节点

第 1 轮遍历

  • 如果 key 不同则直接结束本轮循环
  • newChildren 或 oldFiber 遍历完,结束本轮循环
  • key 相同而 type 不同,标记老的 oldFiber 为删除,继续循环
  • key 相同而 type 也相同,则可以复用老节 oldFiber 节点,继续循环

第 2 轮遍历

  • newChildren 遍历完而 oldFiber 还有,遍历剩下所有的 oldFiber 标记为删除,DIFF 结束
  • oldFiber 遍历完了,而 newChildren 还有,将剩下的 newChildren 标记为插入,DIFF 结束
  • newChildren 和 oldFiber 都同时遍历完成,diff 结束
  • newChildren 和 oldFiber 都没有完成,则进行节点移动的逻辑
  • 第 3 轮遍历

处理节点移动的情况

以上就是React DOM-diff 节点源码解析的详细内容,更多关于React DOM-diff 的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

ReactDOM-diff节点源码解析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

ReactDOM-diff节点源码解析

这篇文章主要为大家介绍了ReactDOM-diff节点源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-02-27

React DOM-diff节点源码分析

本篇内容介绍了“React DOM-diff节点源码分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!单节点单节点的dom-diff是在re
2023-07-05

Vue3源码通过render patch 了解diff

这篇文章主要为大家介绍了Vue3源码系列通过render及patch了解diff原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-11-13

Redis源码解析:集群手动故障转移、从节点迁移详解

一:手动故障转移Redis集群支持手动故障转移。也就是向从节点发送”CLUSTER FAILOVER”命令,使其在主节点未下线的情况下,发起故障转移流程,升级为新的主节点,而原来的主节点降级为从节点。为了不丢失数据,向从节点发送”CLUS
2022-06-04

Vue3源码分析组件挂载创建虚拟节点

这篇文章主要为大家介绍了Vue3源码分析组件挂载创建虚拟节点,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-11-13

nacos注册中心单节点ap架构源码解析(最新推荐)

这篇文章主要介绍了nacos注册中心单节点ap架构源码解析,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
2023-01-03

vue原理Compile之optimize标记静态节点源码分析

这篇文章主要介绍“vue原理Compile之optimize标记静态节点源码分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“vue原理Compile之optimize标记静态节点源码分析”文章能帮
2023-07-02

OKhttp拦截器实现实践环节源码解析

这篇文章主要为大家介绍了OKhttp拦截器实现实践环节源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-05

ElasticSearch节点、分片、CRUD、倒排索引和分词源码分析

这篇文章主要介绍了ElasticSearch节点、分片、CRUD、倒排索引和分词源码分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇ElasticSearch节点、分片、CRUD、倒排索引和分词源码分析文章都
2023-07-05

SpringApplicationListener源码解析

这篇文章主要为大家介绍了SpringApplicationListener源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-15

Java中的InputStreamReader和OutputStreamWriter源码分析_动力节点Java学院整理

InputStreamReader和OutputStreamWriter源码分析1. InputStreamReader 源码(基于jdk1.7.40)package java.io; import java.nio.charset.Cha
2023-05-31

next-redux-wrapper使用细节及源码分析

这篇文章主要为大家介绍了next-redux-wrapper使用细节及源码分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-02-12

JavaAQS(AbstractQueuedSynchronizer)源码解析

AbstractQueuedSynchronizer被称为队列同步器,简称为大家熟知的AQS,这个类可以称作concurrent包的基础。本文将通过剖析源码来看看AQS是如何工作的,感兴趣的可以了解一下
2023-02-09

Spring源码解析CommonAnnotationBeanPostProcessor

这篇文章主要为大家介绍了Spring源码解析CommonAnnotationBeanPostProcessor示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-11-13

dom4j怎么解析指定节点

要解析指定节点,你可以使用XPath表达式来定位节点并提取数据。以下是一个使用dom4j解析XML文档并提取指定节点数据的示例代码:import org.dom4j.Document;import org.dom4j.DocumentE
dom4j怎么解析指定节点
2024-03-14

编程热搜

目录