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位内容

相关推荐
anyup_前端梦工厂2 小时前
了解几个 HTML 标签属性,实现优化页面加载性能
前端·html
前端御书房2 小时前
前端PDF转图片技术调研实战指南:从踩坑到高可用方案的深度解析
前端·javascript
2301_789169542 小时前
angular中使用animation.css实现翻转展示卡片正反两面效果
前端·css·angular.js
风口上的猪20153 小时前
thingboard告警信息格式美化
java·服务器·前端
程序员黄同学3 小时前
请谈谈 Vue 中的响应式原理,如何实现?
前端·javascript·vue.js
爱编程的小庄4 小时前
web网络安全:SQL 注入攻击
前端·sql·web安全
宁波阿成5 小时前
vue3里组件的v-model:value与v-model的区别
前端·javascript·vue.js
柯腾啊5 小时前
VSCode 中使用 Snippets 设置常用代码块
开发语言·前端·javascript·ide·vscode·编辑器·代码片段
Jay丶萧邦5 小时前
el-select:有关多选,options选项值不包含绑定值的回显问题
javascript·vue.js·elementui
weixin_535854225 小时前
oppo,汤臣倍健,康冠科技,高途教育25届春招内推
c语言·前端·嵌入式硬件·硬件工程·求职招聘