React进阶之React核心源码解析(二)

React核心源码解析

diff

一共两个阶段

  • render:内存中的更新,主要是通过递归的过程,来将react变化的部分,在内存中找到哪些元素是需要发生变化的,针对需要发生变化的内容,会打上标,也就是Update,针对这些需要Update的组件,会拿着上一次与当前这一次的节点,通过Fiber(专门的数据结构存储当前的组件/模块)进行此次与上一次元素更新之间对比。
    diff 的过程,其实就是Update Fiber的过程,diff的结果就是生成一个经过Update更新之后的新的Fiber的节点。
  • commit:同步的过程,同步渲染到浏览器端/客户端的过程
  1. 针对不同类型的元素

html 复制代码
<div>
   <Component />	
</div>

html 复制代码
<span>
   <Component />	
</span>

这是不同类型元素的展示,在Fiber中,定义当前组件类型不一致的时候,是需要将当前树全部销毁重建的

因此,在开发过程中,需要尽可能减少元素的变化。

  1. 针对相同类型元素

html 复制代码
<ul className="hello">
  <li>1</li>	
  <li>2</li>	
</ul>

html 复制代码
<ul className="world">
  <li>3</li>	
  <li>4</li>
  <li>5</li>		
</ul>

就是在render的阶段中,相同类型的节点进行diff判断,并不会立马更新。生成mutation要变化的内容,针对不同diff进行汇总,然后在commit过程中,对dom的节点直接进行操作。

但是 上述例子中,如果是针对第一个元素<li>之前进行添加的话,会将所有的子列表全部销毁掉,重新创建,

就比如,从

html 复制代码
<ul className="hello">
  <li>1</li>	
  <li>2</li>	
</ul>

html 复制代码
<ul className="world">
  <li>3</li>	
  <li>1</li>
  <li>2</li>		
</ul>

这样的情况

这时候,通过key的方式去解决,并且尽可能保证每个key是唯一的,不要去使用index作为key来传输

跟一个真实DOM相关联的是:

  • current fiber:当前的fiber节点,表达当前DOM的 内存对象
  • workInProgress fiber:内存中变化的节点,表达接下来在内存中需要进行操作的对象
  • DOM:在commit过程中生成的真实的dom
  • JSX:真实代码

通过current fiberJSX比较,更新到workInProgress fiber,进行current fiber和workInProgress fiber的互换,拿着workInProgress fiber替换生成到真实DOM

  1. 同级元素的比较
  2. 不同类型元素 销毁当前节点&所有子节点
  3. key
javascript 复制代码
// 根据newChild类型选择不同diff函数处理
function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
): Fiber | null {

  //这里的newChild是fiber节点,不是dom元素了
  const isObject = typeof newChild === 'object' && newChild !== null;

  //如果是对象的话,那么就是单一的节点
  if (isObject) {
    // object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        // 调用 reconcileSingleElement 处理
      // // ...省略其他case
    }
  }

  if (typeof newChild === 'string' || typeof newChild === 'number') {
    // 调用 reconcileSingleTextNode 处理
    // ...省略
  }

  //如果是数组的话,则是多个节点
  if (isArray(newChild)) {
    // 调用 reconcileChildrenArray 处理
    // ...省略
  }

  // 一些其他情况调用处理函数
  // ...省略

  // 以上都没有命中,删除节点
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

单一节点比较diff

javascript 复制代码
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  
  // 首先判断是否存在对应DOM节点
  while (child !== null) {
    // 上一次更新存在DOM节点,接下来判断是否可复用

    // 首先比较key是否相同
    if (child.key === key) {

      // key相同,接下来比较type是否相同

      switch (child.tag) {
        // ...省略case
        
        default: {
          if (child.elementType === element.type) {
            // type相同则表示可以复用
            // 返回复用的fiber
            return existing;
          }
          
          // type不同则跳出switch
          break;
        }
      }
      // 代码执行到这里代表:key相同但是type不同
      // 将该fiber及其兄弟fiber标记为删除
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // key不同,将该fiber标记为删除
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }

  // 创建新Fiber,并返回 ...省略
}

针对单一的节点:

  • key是否一样
    • 一样
      • type 一样:同一个DOM
      • type不一样:当前节点和所有的兄弟节点sibling全都删除
    • 不一样
      • 直接删除

举例:

全部删除:

javascript 复制代码
// 更新前
<div>a</div>
// 更新后
<p>a</p>

// 更新前
<div key="xxx">a</div>
// 更新后
<div key="ooo">a</div>

只更新内容:

javascript 复制代码
// 更新前
<div key="xxx">a</div>
// 更新后
<div key="xxx">b</div>

多节点比较diff

isArray方法比较

  1. 节点更新
javascript 复制代码
// 更新前
<ul>
  <li key="0" className="before">0<li>
  <li key="1">1<li>
</ul>

// 更新后 情况1 ------ 节点属性变化
<ul>
  <li key="0" className="after">0<li>
  <li key="1">1<li>
</ul>

// 更新后 情况2 ------ 节点类型更新
<ul>
  <div key="0">0</div>
  <li key="1">1<li>
</ul>

节点属性变化,只需要更新即可

节点类型变化,则当前<li>和兄弟节点全部推倒重建

  1. 节点新增和删除
javascript 复制代码
// 更新前
<ul>
  <li key="0">0<li>
  <li key="1">1<li>
</ul>

// 更新后 情况1 ------ 新增节点
<ul>
  <li key="0">0<li>
  <li key="1">1<li>
  <li key="2">2<li>
</ul>

// 更新后 情况2 ------ 删除节点
<ul>
  <li key="1">1<li>
</ul>
  1. 节点位置变化,顺序调整
javascript 复制代码
// 更新前
<ul>
  <li key="0">0<li>
  <li key="1">1<li>
</ul>

// 更新后
<ul>
  <li key="1">1<li>
  <li key="0">0<li>
</ul>

这里key不一样,全部删除重建

两轮遍历比较

第一轮比较
  1. let i = 0,遍历newChildren,将newChildren[i]与oldFiber比较,判断DOM节点是否可复用;
    JSX:newChildren[i]
    currentFiber:oldFiber
  2. 如果可复用,i++,继续比较newChildren[i]与oldFiber.sibling,可以复用则继续遍历;
  3. 如果不可复用,分两种情况:
    a. key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束;
    b. key相同type不同导致不可复用,会将oldFiber标记mutation为DELETION,并继续遍历;
  4. 如果newChildren遍历完(即 i === newChildren.length - 1 )或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束;
javascript 复制代码
// 更新前
<li key="0">0</li>
<li key="1">1</li>
<li key="2">2</li>
            
// 更新后
<li key="0">0</li>
<li key="2">1</li>
<li key="1">2</li>

// 第一个节点可复用,遍历到key === 2的节点发现key改变,不可复用
// 跳出遍历,等待第二轮遍历处理

// oldFiber: key === 1、key === 2未遍历
// newChildren剩下key === 2、key === 1未遍历
javascript 复制代码
// 更新前
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>
            
// 更新后 情况1 ------ newChildren与oldFiber都遍历完
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
            
// 更新后 情况2 ------ newChildren没遍历完,oldFiber遍历完
// newChildren剩下 key==="2" 未遍历
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
<li key="2" className="cc">2</li>
            
// 更新后 情况3 ------ newChildren遍历完,oldFiber没遍历完
// oldFiber剩下 key==="1" 未遍历
<li key="0" className="aa">0</li>
第二轮比较
  • newChildren和oldFiber都遍历完了,针对单一节点标记mutation,需要发生变化的元素记录在render阶段中,加update更新

  • newChildren剩下了,oldFiber没剩下,意味着新增了

    剩余的reset newChildren直接添加到workInProgress Fiber

    新增元素打标,将mutation记为 Placement

  • newChildren 没剩下 oldFiber 剩下了,意味着删除了

    workInProgress Fiber中将旧的循环遍历掉,标记mutationDELETION

  • newChildren和oldFiber都剩下了

    第二轮遍历:

    const exisitingChildren = map(),通过map.get获取oldFiber剩下的所有存在existingChildren中

    • key:oldFiber中的key
    • value:oldFiber中的value

    遍历 newChildren 剩下的

    exisitingChildren.has(newChildren[i].key),有的话获取这个元素,然后删除掉

    lastPlacedIndex:最后一个可复用的节点,再当前oldFiber的索引位置



Update 状态更新

触发状态更新的方式

  • 初始化的render
  • setState
  • useState
  • forceUpdate

进行状态更新时候会创建一个Update 对象存储变更相关联的内容

包含到对应的fiber,在beginWork的过程中,找到这个fiber,记录到当前更新的数组队列updateQueue

Update是在render beginWork找到要更新的元素,Update置入要更新的节点中,去触发它的state

typescript 复制代码
// 一次更新的内容
const update: Update<*> = {
  eventTime, //获取当前的执行时间,判断执行更新的耗时
  lane, //优先级任务
  suspenseConfig, 
  tag: UpdateState, //包裹住更新的状态,updateState | ReplaceState | forceUpdate
  payload: null, //不同的更新元素带有的参数是什么
  callback: null,

  next: null, // 也是一个链表,下一次更新的内容
};
//一次更新的 update1 next->update2 next->update3 
//div1 div2

fiber 节点 updateQueue指代的是div1,div2的变化

typescript 复制代码
//一个更新的fiber的节点
const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState, //fiber中本身的state
    firstBaseUpdate: null, //第一个更新的
    lastBaseUpdate: null, //最后一个更新的
    shared: {
      pending: null, //关联链表的方式,第一个更新,第二个更新,...最后一个更新,通过这个来关联的
    },
    effects: null,
  };


pending候车的上车

先上u3

这都是在render中做的,进入到commit阶段后,不管是谁,都不能被中断了,因为都已经在视图中了

因此,说的 异步可中断,说的都是在内存中能够做到的事情

u2优先级高于u1

但是链表:u1 --- u2 这个顺序始终不会变

也就意味着,由于优先级,先执行的是u2,然后再执行一次 u1 和 u2。由此,高优先级的任务可能会触发两次

Concurrent Mode

协调模式

  • Fiber 异步可中断
  • Scheduler 协调器,结合可分片,异步可中断
  • lane 优先级
  1. 优先级不同
  2. 优先级表达的方式 batch批次执行
  3. 方便计算

划分二进制31位内容

相关推荐
持续升级打怪中17 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路21 分钟前
GDAL 实现矢量合并
前端
hxjhnct23 分钟前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星30 分钟前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全
前端工作日常1 小时前
我学习到的AG-UI的概念
前端
韩师傅1 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端
XiaoYu20021 小时前
第12章 支付宝SDK
前端
双向332 小时前
RAG的下一站:检索增强生成如何重塑企业知识中枢?
前端