react-react17源码分析

一、历史版本 react 的简单分析

1.v15.x.pre:

  1. 拥有2个重要的功能 reconciler、render,对后续版本react影响巨大,基本奠定此后react框架大方向。
  2. 有优化批量更新。
  3. 缺点是业务大时,数据更新还是会卡顿,用户体验差。

2.v16(fiber架构):

  1. 解决(优化)15版本前的卡顿问题,每隔一段时间响应一次用户操作
    1. 处理方案:给每个不同的更新设置优先级:
      1. 生命周期更新、用户操作——同步更新,优先级最高
      2. 交互的事件——优先级高
      3. 数据请求等——优先级低
    2. 对于优先级低的,但是很早就已经执行了的任务,会因为时间的流失而逐渐提高优先级,优先级具体的优先程度涉及到 scheduler 调度器
    3. 某个执行过程是:一个优先级低的任务 taskA 被先执行,然后一个优先级高的任务 taskB 被后执行,最后执行一个优先级高的任务 taskC;那么由于 taskB 是优先级高的任务,所以他会被最先调度,然后判断 taskA 的优先级是否因为时间的流逝而超过或相等于 taskC 的优先级,如果如此那么先执行 taskA ,否则先执行 taskC。
  2. 在v16.8引入了hooks的概念,react风格向FC模式转变,高阶组件等使用都很方便。
  3. 遗留问题:高优先级 cpu 任务会打断低优先级 io 任务。

3.v17:

  1. 解决v16遗留问题,将优先级定义从指定一个优先级修改为指定一个优先级区间,区间的表示为32位二进制数,在某个区间内的任务将被任务优先级相同

二、react 内部功能及概念

  • fiber节点:运行时,react中每一个组件已经render函数的节点都是一个fiber节点
  • fiber tree:fiber节点之间通过 child、return、sibling 构成了 fiber tree
  • lanes:v17优先级通道,react 设置优先级为一个通道(区间),每一个通道都是一个32位二进制数,判断优先级时按照位运算计算。
  • fiber:
    • 需要完成或已完成的在组件上的任务,一个组件可以有多个 fiber 。
    • 重要的节点:
      • return:指向父节点:Fiber
      • child:指向子节点:Fiber
      • sibling:指向兄弟节点:Fiber
      • tag:表示当前 fiber 节点处于哪种类型的组件,比如 class、function
      • stateNode:初始化为null,渲染后指向真实dom节点信息
      • key:fiber 节点实例的唯一标识,用于 dom diff
      • flag:标记更新的类型,表示此节点应该执行的副作用,比如对应的 dom 节点应该执行增删改
      • lanes:优先级车道(区间)
      • childrenLanes:子节点车道(区间),为了方便调用
      • alternate:指向前一次构件的 fiber tree 所对应的节点(workingProgress fiber node)
  • scheduler:调度器。
  • reconciler:调节器。

三、scheduler调度

一、调度流程:

  1. 同步任务 -> 优先级最高,立刻执行
  2. 异步任务 -> 判断当前是否已经有一个 fiber 在运行:
    1. 有 fiber 在运行,判断优先级:
      1. 优先级相同 -> 进入批处理逻辑
      2. 优先级不同:
        1. 根据当前任务的优先级,将立即执行的任务放到及时任务列表 taskQueue
        2. 延时任务放到延时任务列表 timeQueue
    2. 没 fiber 在运行,进入 2.1.2 的判断
  3. 最终所有任务都会被放到  performSyncWorkOnRoot ,它可以看做调度器的出口。它会将所有任务通过宏任务 MessageChannel 中执行。

二、taskQueue 、timeQueue

  1. 在 taskQueue 中的任务,会被放在宏任务中执行,立即执行(虽然宏任务本身是异步的)。
  2. 档执行完 taskQueue 中的任务时,会清空 taskQueue 任务列表,然后判断在 timeQueue 中的 delay 任务,如果某些任务的优先级已经达到需要立即执行的时候,将这些任务添加到 taskQueue。

三、scheduleUpdateOnFiber 判断任务是否立刻执行

  1. 立即执行:
    1. v16:过期时间 与 当前时间的比较
    2. v17:是非批处理任务
  2. 非立即执行:
    1. 批处理任务,首次进入判断的任务节点会被添加一个 callback 方法,再次有任务进入判断时,会判断当前任务和前一个任务的优先级关系:
      1. 如果相同,则使用同一批次更新
      2. 如果不同,取消前一个 callback 任务。v17判断优先级前会进行合并,所以理论上优先级总是最高的,除非遇到一个非常高的完全不同等级的任务,才会取消

四、设定过期时间 expirationTime

  1. 获取当前时间 currentTime
  2. 计算任务开始时间:
    1. 如果有设置延迟时间,则开始时间 = 当前时间 + 延迟时间
    2. 如果没设置有延迟时间,则开始时间 = 当前时间
  3. 根据优先级,添加时间间隔。优先级也是时间概念,越大的数字表示越晚的过期时间,表示越低的优先级,-1为最小。
  4. 所以,过期时间 = 当前时间 + 配置的延迟时间 + 优先级时间

比较 开始时间 和 过期时间:

  1. 如果 开始时间 晚,那么此任务将被添加到 timerQueue,然后延迟到 开始时间 后运行。
  2. 如果 开始时间 早,那么次任务将被添加到 taskQueue,然后通过创建 messageChannel 调度即使任务。

五、MessageChannel

messageChannel 是一个宏任务,一共只有2个通道: port1 、port2,port2.postMesssage() 将会调用 port1 的回调。

Q&A:

  1. 功能上,完全可以用 setTimeout 代替。但 setTimeout 有定时功能,用在创建一个宏任务有点浪费它的能力。
  2. setInterval 同理。
  3. 不用微任务是因为无法解决卡顿问题,微任务运行时,主线程将会被占用。
  4. 题外话:实际上任何宏任务产生的时间间隔都大于0ms,setTimeout(fn,0)也不例外;而微任务在这方面会好很多。

四、reconciler 调节器

负责找出变化的节点,并打上标记。为了方便打断,v16 起,节点按链表结构组织。

reconciler 会将渲染机制分为2个阶段:

  1. 会执行 dom diff,并生成 dom 元素,但不会立刻渲染,这时如果有更高优先级的任务可以打断渲染。
  2. 下一次 commit 阶段才会渲染,而 commit 阶段是不能被打断的。

一、fiber tree 的双缓存结构

react 中最多同时存在两棵 fiber tree,这么做的原因是方便比较哪些节点发了变化。

正在显示中的称为 current fiber tree ;第二次(本次)构建的称为 workInProgress fiber tree ,其中,current fiber 的 alternate 属性指向 workInProgress fiber tree 中相对应的fiber。

current fiber                workInProgress fiber

fiberNode1        ←→        fiberNode1`

        ↓                                ↓

fiberNode2        ←→        fiberNode2`

        ↓                                ↓

fiberNode2        ←→        fiberNode3`

当任意层级的节点在进行比较时,可以进行对比,如果判断结构想通过可以直接拿来复用;当比较完所有的节点时,将 workInProgress tree 设置为 current fiber,current fiber 就回收掉;相当于 canvas 离屏缓存。

二、构建 fiber tree

const Fn = () => {
    return (
        <div>
            <p>文本</p>
            <input type="text"/>
        </div>
    )
}

对于以上代码,将会构建成 div → p → 文本 → p → input → div 的链表结构

流程大致是一个深度优先遍历的过程(默认认为已经是第N次构建,对于 fiber tree 删的某一个节点):

  1. 执行 performUnitOfWork 函数
    1. 将当前指针指向某节点,调用 beginWork。
    2. 先判断当前节点是否有子节点
      1. 如果有子节点,将当前指针指向子节点,执行流程1。
      2. 如果没有子节点,执行流程2。
  2. 执行 completeUnitOfWork 函数
    1. 判断是否有兄弟节点
      1. 如果有兄弟节点,将当前节点标记为已经访问过,将当前指针指向兄弟节点,重新进入流程2。
      2. 此时既没有子节点也没有兄弟节点,返回到父节点,重新进入流程2。

beginWork(构建 fiber tree):

  1. 获取 current-tree,如果为 null 说明是第一次渲染;否则处理更新逻辑:
    1. 有属性变化则一定有更新;否则尝试复用,在复用中还会判断是否有更新。
    2. 对于 {this.xxx} 和 <XXX/>,前者可以很好的缓存,后者需要做额外处理。 
  2. 根据 current fiber 的不同的 tag ,创建并返回不同的 fiber 节点。在非初次创建 fiber 节点时,会做 dom diff:
    1. 新创建的节点与 current fiber 作比较,主要分为:
      1. 单节点 diff
        1. 老节点不存在,标记建新
        2. key、类型相同,标记复用
        3. 否则标记删除
      2. 多节点 diff,一般指数组
        1. 对比相同 index 下的 key 是否相同,是则标记复用
        2. 判断 key 值不同的节点是否移动,(能够找出新的位置,用一个哨兵变量++,对比完老数组),表示复用
        3. 老节点不存在,标记创建(新创建了节点)
        4. 剩余的是多余的,标记删除(老节点多余)

五、commit 阶段

负责渲染页面的过程,分为3个阶段:dom操作前、时、后

一、dom 操作前

  • 遍历 fiber tree,判断删除标记,提交删除操作。先判断删除是因为删除是与众不同的,删除后没有自己点,不用更深的遍历了
  • 做深度优先遍历,对子节点进行相同的判断
  • 调用 getSnapshotBeforeUpdate ,一个钩子函数
  • 注册一次异步调度的操作的回调 useEffect,它不在 dom 操作完成之后注册并调用的原因是,为了防止阻塞(react 作者)。另 useLayoutEffect 是在 dom 操作完后同步执行的,同步意味着阻塞。

二、dom 操作时

步骤与 dom 操作前的逻辑几乎类似,根据 fiber 的标记做真正的 dom 操作

三、dom 操作后

  • 深度优先级遍历各类 effect ,调用生命周期,执行钩子函数
  • 赋值 ref
  • 处理异步回调(如果有挂载)
  • 删除老的 fiber tree

六、生命周期、钩子函数的顺序

对于 A 组件内嵌套 B 组件,问两个组件中,各类生命周期及钩子函数执行顺序的问题

判断钩子函数位于哪一个大步骤,reconciler 阶段还是 commit 阶段,reconciler 阶段的钩子函数比 commit 阶段的钩子函数靠前。

然后判断这些钩子函数执行和遍历子节点的顺序,先执行钩子函数,那就先执行 A 组件的钩子;否则执行 B 组件的钩子。

七、setState、hooks

它们都会通往 scheduler 的逻辑。

hooks 不允许被函数内的额外的作用域包裹,因为:hooks 在 react 的内部是按照链表结构保存的,所以如果将 hooks 不放在函数内的最顶级,那么有可能造成更新错位。

useEffect 模拟 didmount、didupdate、willunmont 三个生命周期,利用第二个参数。

useCallback ( + React.meno ) 模拟 shouldComponentUpdate 。

context + useRedcuer 实现一个 redux。


设置不批处理的函数:unbatchedUpdates,直接跳过 scheduler 阶段

调用它的有:ReactDOM.render

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值