React23-状态更新

613 阅读7分钟

1.流程

image-20210521161843724

同步模式

异步并发模式(concurrent):我们所有的更新都有优先级,异步调度的方式执行

函数组件创建的update,新的state值放到action值,这个update放到hook对象的updateQueue中

const update: Update<S, A> = {
    lane,
    action,//新的state值
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

然后进入schduleUpdateOnfiber 进入调度 然后再进入render 再进去commit

流程:

我们知道更新的是app这个functioncmp 但是我们要执行performconcurrentWorkonroot,调度的是根节点,所以我们需要遍历找到app的根节点,这一路上所有孩子节点的优先级改为和app一样的优先级,然后根节点去调度,获取root下优先级最高的lane,转换成schedule的优先级,传入schedulecallback进行调度,调度的就是performconcurrentWorkonroot这个方法,这个方法会和schdule中所有的回调函数一起调度,找到优先级最高的回调函数来执行,当时间到了这里performconcurrentWorkonroot就会被调用,就进入render阶段,去diff,然后commit.

ps:进入调度器的函数是performconcurrentWorkonroot即render阶段的开始函数,不是一个单单的update,之前以为每个update都是个函数可以被调度,其实所有dom创建的update都是触发performconcurrentWorkonroot函数这一个函数进入调度,里面的update还是按照深度优先遍历来diff

image-20210521164105631

主要函数为ensureRootisScheduled

if (newCallbackPriority === SyncLanePriority) {
  // 任务已经过期,需要同步执行render阶段
  newCallbackNode = scheduleSyncCallback(
    performSyncWorkOnRoot.bind(null, root)
  );
} else {
  // 根据任务优先级异步执行render阶段
  var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
    newCallbackPriority
  );
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );

判断任务优先级是同步还是异步 同步调用performSyncWorkOnRoot,异步调用performConcurrentWorkOnRoot,并传入这个函数的优先级去调度看什么时候执行

2.优先级和update

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

// TODO: Use symbols?
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

6种优先级

ImmediatePriority就是同步优先级(sync),是最高的优先级

UserBlockingPriority 是用户触发的优先级 比如点击事件的回调 this.state()产生的update就将有UserBlockingPriority这个优先级

NormalPriority:一般的优先级(最常用),比如请求服务端数据后更新状态,这个update就是NormalPriority

LowPriority:suspense采用的

update是在fiber中的

一个组件的状态可由以下计算出,useState也会创建一个小fiber,也有内部的状态,即state得值,组件fiber的状态就是this.state的值

image-20210521165218992

update1是NormalPriority,update2是UserBlockingPriority,

如果本次更新是UserBlockingPriority,那么basestate只会执行update2的更新,因为update1的优先级低于本次更新,所以不执行

如果本次更新是NormalPriority,那么两个update都会执行

重要:优先级是个全局概念,update只是存在某个fiber中的

3.优先级和update举例

image-20210521171419553

此时f组件在mount生命周期里触发了一次setState,那么创建update的优先级为normal,这个优先级会一直往上传直到root节点,root节点以normal优先级进入调度,执行的函数为performConcurrentWorkOnRoot,到render阶段,假如diff过程很长,这个时候f点击事件创建啦一个update

image-20210521171720778

这个update为UserBlockingPriority,又会一直上传到root节点,此时又去以UserBlockingPriority的优先级调度performConcurrentWorkOnRoot,但是这个优先级更高,所以正在render阶段的第一个performConcurrentWorkOnRoot函数就中断,第二个UserBlockingPriority的performConcurrentWorkOnRoot执行,执行到f的时候,只有UserBlockingPriority的update能够执行改变state.

4.update的数据结构和计算

理念参照啦git版本控制的理念

1.数据结构

classcmp的update数据结构

const update: Update<*> = {
  eventTime,//!update发生的时间,采用performance.now()获取
  lane,//update的优先级
  suspenseConfig,
  tag: UpdateState,//updatestate/forceupdate/replaceudpate三种
  payload: null,//state的新值 或者elemnt元素
  callback: null,//setstate的第二个参数

  next: null,//指针形成链表
};

classcmp的updateQueue的数据结构

const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
    },
    effects: null,//保存update.callback!=null的update,因为要执行callback属于副作用
  };

之所以在更新产生前该Fiber节点内就存在Update,是由于某些Update优先级较低所以在上次render阶段Update计算state时被跳过。这个update会记录在firstbaseupdate到lastbaseupdate的链表上,下次更新的时候会在最前面加上这些update

2.update执行顺序

如何保证Update不丢失

实际上shared.pending会被同时连接在workInProgress updateQueue.lastBaseUpdatecurrent updateQueue.lastBaseUpdate后面。

render阶段被中断后重新开始时,会基于current updateQueue克隆出workInProgress updateQueue。由于current updateQueue.lastBaseUpdate已经保存了上一次的Update,所以不会丢失。上次的update会加到这次update前面

a2->b1(其中a2是上次的update被打断了 b1新update进来了 会先去current里面看上次的update并加到最前面),总之上次被打断的或者没更新的都会放到最前面。

commit阶段完成渲染,由于workInProgress updateQueue.lastBaseUpdate中保存了上一次的Update,所以 workInProgress Fiber树变成current Fiber树后也不会造成Update丢失。

如何保证状态依赖的连续性

当某个Update由于优先级低而被跳过时,保存在baseUpdate中的不仅是该Update,还包括链表中该Update之后的所有Update

考虑如下例子:

baseState: ''
shared.pending: A1 --> B2 --> C1 --> D2

其中字母代表该Update要在页面插入的字母,数字代表优先级,值越低优先级越高。

第一次render优先级为1。

baseState: ''
baseUpdate: null
render阶段使用的Update: [A1, C1]
memoizedState: 'AC'

其中B2由于优先级为2,低于当前优先级,所以他及其后面的所有Update会被保存在baseUpdate中作为下次更新的Update(即B2 C1 D2)。

这么做是为了保持状态的前后依赖顺序。

第二次render优先级为2。

baseState: 'A'
baseUpdate: B2 --> C1 --> D2
render阶段使用的Update: [B2, C1, D2]
memoizedState: 'ABCD'

注意这里baseState并不是上一次更新的memoizedState。这是由于B2被跳过了。

即当有Update被跳过时,下次更新的baseState !== 上次更新的memoizedState

跳过B2的逻辑见这里(opens new window)

通过以上例子我们可以发现,React保证最终的状态一定和用户触发的交互一致,但是中间过程状态可能由于设备不同而不同。

总结:被打断或者被跳过的update都作为下次低优先级的render阶段的开始的update,只有执行完一轮processQueue后update才会没了否则创建啦update就是在链上,下一轮才没有,被跳过的下次baseState不是meriziedState 而是被跳过的update之前的state,而且这次跳过的update之后的链都会保存到下一次Updatequeue前面,保证最终结果正确。

import React,{useState,useEffect,useRef} from 'react'

export default function App(){
  const [number, setNumber] = useState(0)
  const button = useRef(null)
  useEffect(()=>{
    setTimeout(() => {
      setNumber(1)//!主要是这个hook对象内部的state已经发生改变啦
    }, 1000);
    setTimeout(() => {
      setNumber(2)
      button.current.click()
    }, 1004);
  },[])
  return (
    <div> 
      <button onClick={()=>{setNumber(number=>number+2)}} ref={button}>加2</button>
        {
          new Array(4500).fill(0).map((_,index)=><span>{number}</span>)
        }
    </div>
  )
}
/* 1->2->number->number+2 */

首先创建一个1的update 正在render 然后又有一个2的update加入到queue中,然后又有number=>number+2加入queue中 此时是useblocking下,只有第三个update执行,number为2,commit后会有2

然后进入normal的环节,跳过的是第一个update,所以这一轮也是1->2->number=>number+2这个queue,baseState是第一个update前的状态是1,所以最后执行完是4,

所以有一瞬间为2马上到4.

setNumber(number+2)

setNumber(number+2)//这种传递依赖的是baseState
setNumber(number=>number+2)/这种传递依赖的是上次update后的state

3.ReactDom.render执行流程

image-20210521203125267

这个update对象的payload就是第一个element子元素,然后触发performSynconRoot

updatehostcmp会把state赋值给nextchildren 开始recocilechildren

来自卡颂react电子书