react源码学习(3-1)实现

状态更新

  • 我们知道render阶段是用于更新的时候根据更新的内容进行fiber树的创建。开始于performSyncWorkOnRoot(同步)或者performConcurrentWorkOnRoot(异步。),而一般的流程是状态更新->render->commit
  • 那从状态更新到render阶段,做了什么呢?

创建upDate对象

upDate

  • 在react中,可以通过ReactDOM.render,this.setState,this.forceUpdate, useState,useReducer等等来触发状态更新。这些方法触发的场景各不同,但他们使用的是同一套状态更新机制,就是因为upDate对象,他用来保存更新状态相关的内容,在render阶段beginwork的时候会根据该对象计算新的state。

rootFiber

  • 现在我们有某个fiber有这个upDate对象,那么怎么从该fiber找到rootFiber(不同的组件树拥有不同的rootFiber)去更新呢?react会调用markUpdateLaneFromFiberToRoot方法从该fiber向上遍历,找到rootFiber。

调度更新 Scheduler

  • 现在我们已经找到了rootFiber了,他的某个fiber节点有upDate对象,需要更新,接着就是通知Scheduler,根据任务优先级,采用同步或者异步的方式调度本次更新。调用的方法是ensureRootIsScheduled
if (newCallbackPriority === SyncLanePriority) {
    
    
  // 任务已经过期,需要同步执行render阶段
  newCallbackNode = scheduleSyncCallback(
    performSyncWorkOnRoot.bind(null, root)
  );
} else {
    
    
  // 根据任务优先级异步执行render阶段
  var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
    newCallbackPriority
  );
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );
}

可以看到会根据调度的优先级分别调用不同的方法,启动render阶段。至此,状态更新跟render阶段已经连接上了。
整体流程就是
触发状态更新 =》 创建update对象 =〉根据fiber找到rootFiber =》调度 =》render阶段 =〉commit阶段.

update

update的分类

不同的触发更新方式所隶属的分类组件不同,比如this.setState是类组件,useState是函数组件,ReactDom.render是原生dom HostRoot。
类组件与原生dom共用一种update结构,函数组件自己有一套update结构。
先看类组件的update结构

const update: Update<*> = {
    
    
  eventTime,
  lane,
  suspenseConfig,
  tag: UpdateState,
  payload: null,
  callback: null,

  next: null,
};
  • next是用来指向下一个update形成链表。我们不禁想到fiberd的fierstEffect和nextEffect形成的链表。两者有关联吗?答案是有,update被fiber节点上的updateQueue包裹着,可以同时存在多个update。这是正常的,比如多个地方调用了同一个this.setState更新同一个状态。就会存在多个update。多个update通过next形成单链表。
  • fber节点最多同时存在两个updateQueue,分别对应currentFiber树的fiber节点的updateQueue和WorkInProgress fiber树的fiber节点的updateQueue。

updateQueue

updateQueue有三种类型,HostComponent,剩余两种跟update的两种类型对应。类组件和原生dom使用的updateQueue结构如:

const queue: UpdateQueue<State> = {
    
    
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
    
    
      pending: null,
    },
    effects: null,
  };

  • baseState:更新前对应的state,Update是基于该state计算更新后的state。
  • firstBaseUpdate和lastBaseUpdate:链表的头跟尾,保存着update,可能会疑惑为啥本次还没更新就有update了,因为有可能有些任务优先级低的任务在上一次render阶段的时候通过Update生成最新的state这个阶段的时候,优先级低的update没有被计算。
  • shared.pending 触发更新时,产生的Update会保存在shared.pending中形成单向环状链表。当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面。
  • effects:数组。保存update.callback !== null的Update。

updateQueue的例子

假设有两个update由于优先级过低。被保留在了baseUpdate中。我们称之为n1,n2,n1.next = n2

fiber.updateQueue.firstBaseUpdate === n1;
fiber.updateQueue.lastBaseUpdate === n2;
n1.next === n2;

此时updateQueue里的update链表指向应该是

fiber.updateQueue.baseUpdate: n1 --> n2

此时我们在此fiber节点上触发两次更新,称之为n3,n4。每个update都会通过enqueueUpdate方法插入到updateQueue中,最近进来的是n3。
此时n3保存在shared.pending,而是环状链表

fiber.updateQueue.shared.pending === n3;
n3.next === n3;

用图表示就是

fiber.updateQueue.shared.pending:   n3 ─────┐ 
                                     ^      |                                    
                                     └──────┘

接着轮到n4进来了,shared.pending保证每次指向的都是最后插入的update。所以如图

fiber.updateQueue.shared.pending:   n4 ──> n3
                                     ^      |                                    
                                     └──────┘

也就是n3.next = n4, n4.next = n3。
此时已经调度完毕,准备进行render阶段。这时候shared.pending会剪断,拼接到updateQueue.lastBaseUpdate上。n3->n4的被剪断。所以updateQueue的update链表应该是

fiber.updateQueue.baseUpdate: n1 --> n2 --> n3 --> n4

在对update计算最新的state这个阶段,会遍历该链表,以fiber.updateQueue.baseState为初始state,依次遍历每个update计算新的state。优先级低的update会被忽略,当遍历更新后的state,就是此次更新计算的最新state。state的变化在render阶段产生与上次更新不同的jsx对象,通过diff算法产生出effecttag,在commit阶段被渲染。

优先级

react对状态更新赋予了优先级的概念,比如

  1. 生命周期 同步
  2. 用户输入框输入,同步
  3. 交互,渲染动画,优先
  4. 请求数据,低优先级

如何调度优先级呢

我们知道Schedule是用来调度优先级的,通过Schdeult提供的runWithPriority,该方法接受一个优先级常量和一个回调函数,回调函数一般是render阶段的入口函数,performSyncWorkOnRoot函数等。

例子

有个切换背景色的任务为n1,优先级为2(数值越低优先级越高)。用户点了切换背景色,此时产生一个update对象,lang属性(update.lang是用来保存优先级的)假设是2。然后进入render阶段,现在的updateQueue对象假设为

fiber.updateQueue = {
    
    
  baseState: {
    
    
    blackTheme: true,
    text: 'H'
  },
  firstBaseUpdate: null,
  lastBaseUpdate: null
  shared: {
    
    
    pending: u1
  },
  effects: null
};

然后用户又快速在输入框输入了一个HI,这时候又产生了一个update对象,为n2,优先级为n1。因为n2的优先级较高,所以中断了n1的render阶段,此时updateQueue中的shared的pending为

fiber.updateQueue.shared.pending === n2 ----> n1
                                     ^        |
                                     |________|
// 即
n2.next === n1;
n1.next === n2;

指向最新的n2。此时n2的优先级是高于n1的,所以n2进入render阶段,而pending的环状链表会被切掉,又因为pending是指向最后一个update的所以正确的顺序应该是
n1—n2
进入render阶段后,在beginwork函数中遍历updateQueue计算新的state,因为n2的优先级高于n1,所以n1会跳过,n2先执行,重点来了

!!!!!
  • 因为update之间可能有依赖关系,比如下一个update 可能依赖于上一个Update对象的state等。
!!!!!
  • 所以被跳过的update及其后面所有update会成为下次更新的baseUpdate。(即n1 – n2)。
    这是什么概念呢?
    比如,此时n2完成了commit阶段后,updateQueue的对象为
fiber.updateQueue = {
    
    
  baseState: {
    
    
    blackTheme: true,
    text: 'HI'
  },
  firstBaseUpdate: n1,
  lastBaseUpdate: n2
  shared: {
    
    
    pending: null
  },
  effects: null
};

可以看到updateQueue中的update链表为n1->n2。在n2 commit阶段完成后,会继续进行调度,render剩余未完成的update对象,此次render是基于n1开始的,除了计算n1的更新,还会多计算一次n2的更新。

相应的n2的render阶段的一些生命周期函数,比如componentWillXX就会执行两次,所以才变得不安全。现在一些声明周期都会加上unsafe_标记。
当n1,n2的变化计算完毕后才会执行commit阶段进行渲染。所以n2的render阶段会运行两次。

componentWillXX为什么会不安全。

因为react16增加了优先级的概念,如果先触发优先级低的任务再触发优先级高的任务,优先级高的任务会先render,对应的一些生周就会执行。此时还没commit。当优先级高的任务render完,优先级低的任务会继续render,而因为每个update对象是有关联的,所以跳过的update以及后面所有的update会作为下一次render的链表。也就是说当低优先级的任务render之后,还会继续优先级高的render,此时优先级高的render被调用两次,导致可能触发两次生周函数,导致其变得不安全。

如何保证状态正确?

我们知道了updateQueue的大概工作流程了,那么还有问题?

  1. render阶段可以被中断,那么如何保证updateQueue保存的update对象 不丢失?
  2. 如果当前状态依赖上一个状态,如何在支持跳过优先级低的任务的同时保证状态依赖的连续性。
1 保存update对象

我们知道updateQueue对象的pending指向的环会被切断并拼接在updateQueue.lastBaseUpdate后面。

  • 实际上,shared.pending会被同时连接在workInProgress updateQueue.lastBaseUpdate与current updateQueue.lastBaseUpdate后面。
  • 那么当,在render阶段中断的时候,workInProgress的updateQueue会改变,所以基于currentFiber的updateQueue克隆出个updateQueue,这样就不会丢失了update。
  • 当commit阶段完成渲染,由于workInProgress updateQueue.lastBaseUpdate中保存了上一次的Update,所以 workInProgress Fiber树变成current Fiber树后也不会造成Update丢失。
2 保证状态的依赖性
  • 在上面我们已经说过了update会有依赖性,所以跳过的update会连同后面的update一起被保存在updateQueue对象中。
  • 当某个Update由于优先级低而被跳过时,保存在baseUpdate中的不仅是该Update,还包括链表中该Update之后的所有Update。
    比如 A1 A2 A3 A4, A1,A3的优先级较高,跳过A2, A4,下次更新的时候update是A2 A3 A4,A3还会再render一次。

ReactDom.render

创建rootFiber和fiberRootNode并且关联他们

ReactDOM.render会创建rootFiber(组件根节点),fiberRootNode(整个应用的根节点)这一步由legacyCreateRootFromDOMContainer方法实现。
legacyCreateRootFromDOMContainer方法内部会调用createFiberRoot方法完成fiberRootNode和rootFiber的创建以及关联。并初始化updateQueue。

export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
    
    
  // 创建fiberRootNode
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  
  // 创建rootFiber
  const uninitializedFiber = createHostRootFiber(tag);

  // 连接rootFiber与fiberRootNode
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  // 初始化updateQueue
  initializeUpdateQueue(uninitializedFiber);

  return root;
}

rootFiber和fiberRootNode通过current和stateNode指针相关联。
rootFiber.stateNode = fiberRootNode
fiberRootNode.current = rootFiber

创建update

调用updateContainer方法:

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
    
    
  // ...省略与逻辑不相关代码

  // 创建update
  const update = createUpdate(eventTime, lane, suspenseConfig);
  
  // update.payload为需要挂载在根节点的组件
  update.payload = {
    
    element};

  // callback为ReactDOM.render的第三个参数 —— 回调函数
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    
    
    update.callback = callback;
  }

  // 将生成的update加入updateQueue
  enqueueUpdate(current, update);
  // 调度更新
  scheduleUpdateOnFiber(current, lane, eventTime);

  // ...省略与逻辑不相关代码
}

这样整体流程就连接起来了,
从ReactDOM.render

创建fiberRootNode、rootFiber、updateQueue(`legacyCreateRootFromDOMContainer`|
    |
    v

创建Update对象(`updateContainer`|
    |
    v

从fiber到root(`markUpdateLaneFromFiberToRoot`|
    |
    v

调度更新(`ensureRootIsScheduled`|
    |
    v

render阶段(`performSyncWorkOnRoot``performConcurrentWorkOnRoot`|
    |
    v

commit阶段(`commitRoot`

react还有其他入口函数,

legacy -- ReactDOM.render(<App />, rootNode)
blocking -- ReactDOM.createBlockingRoot(rootNode).render(<App />)
concurrent -- ReactDOM.createRoot(rootNode).render(<App />)

不同的入口函数对应的模式不同

this.setState

setState内部会调用this.updater.enqueueSetState,
在这里插入图片描述
该方法会创建创建update并且调度update

enqueueSetState(inst, payload, callback) {
    
    
  // 通过组件实例获取对应fiber
  const fiber = getInstance(inst);

  const eventTime = requestEventTime();
  const suspenseConfig = requestCurrentSuspenseConfig();

  // 获取优先级
  const lane = requestUpdateLane(fiber, suspenseConfig);

  // 创建update
  const update = createUpdate(eventTime, lane, suspenseConfig);

  update.payload = payload;

  // 赋值回调函数
  if (callback !== undefined && callback !== null) {
    
    
    update.callback = callback;
  }

  // 将update插入updateQueue
  enqueueUpdate(fiber, update);
  // 调度update
  scheduleUpdateOnFiber(fiber, lane, eventTime);
}

this.forceUpdate

this.updater上不仅有enqueueSetState,还有enqueueForceUpdate,调用this.forceUpdate的时候会调用他。
在这里插入图片描述

enqueueForceUpdate(inst, callback) {
    
    
    const fiber = getInstance(inst);
    const eventTime = requestEventTime();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const lane = requestUpdateLane(fiber, suspenseConfig);

    const update = createUpdate(eventTime, lane, suspenseConfig);

    // 赋值tag为ForceUpdate
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
    
    
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  },
};

forceupdate多了个update.tag赋值还有少了payload,其他的跟setState一样。赋值tag的用处?
类组件更新需要满足两个条件,

这里是引用
const shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext, );

checkHasForceUpdateAfterProcessing:内部会判断本次更新的Update是否为ForceUpdate。即如果本次更新的Update中存在tag为ForceUpdate,则返回true。(判断是否是forceUpdate)
checkShouldComponentUpdate:内部会调用shouldComponentUpdate方法。以及当该ClassComponent为PureComponent时会浅比较state与props。(性能优化手段)

所以当存在tag的时候就是调用forceUpdate强制更新,那么当前的组件不会受如PureComponent性能优化影响,一定会更新。

学习文章来自:https://react.iamkasong.com/hooks/prepare.html#%E4%BB%8Elogo%E8%81%8A%E8%B5%B7

猜你喜欢

转载自blog.csdn.net/lin_fightin/article/details/120512967
3-1