react源码学习-实习篇-状态更新

Posted lin-fighting

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react源码学习-实习篇-状态更新相关的知识,希望对你有一定的参考价值。

实现篇-状态更新

状态更新的流程-几个关键节点

先了解几个关键函数的调用,看看常见触发状态更新的方法是如何工作的。

render阶段的开始

之前就了解归,render阶段开始于performSyncWorkOnRoot/performConcurrentWorkOnRoot,他们会执行对应的performUnitOfWork方法。

commit阶段的开始

commit阶段开始于commitRoot方法的调用,rootFiber作为传参。

render阶段完成后会进入commit阶段。那么从触发状态更新,到render阶段的路径又经过了什么呢?

触发状态更新(this.setState) ------>. ? ------> render阶段 ----> commit阶段
1 创建update对象

在react中,有以下方法触发状态更新:

ReactDOM.render
this.setState
this.forceUpdate
useState
useReducer

这些方法调用的场景各不相同,如何接入同一套状态更新机制呢?

那就是每次状态更新都会创建一个保存更新状态内容的对象,Update,然后在render阶段的beginWork中根据Update对象计算最新的state。

每次状态更新都会创建对应的update对象。(后面详细讲,现在先知道会创建一个update对象),然后存放在fiber上。

2 从有update对象的fiber到root

render阶段是重rootFiber开始向下遍历的,如何根据当前有update对象的fiber找到rootFiber呢?

触发状态更新的fiber一直向上遍历到rootFiber,并返回rootFiber。由于不同更新优先级不尽相同,所以过程中还会更新遍历到的fiber的优先级,现在暂不了解。

3 调度更新

现在已经有了一个rootFiber了,该rooFiber所在的fiber树上有某一个fiber节点包含一个update对象。那就需要通知Scheduler根据更新的优先级,决定以同步或者异步调度本次更新。

就是ensureRootIsScheduled方法,看在他的核心代码:

根据任务的优先级,决定同步还是异步调度,shceduleCallback和s cheduleSyncCallback会调用Schedule提供的带哦度方法根据优先级调度其回调函数执行

可以看到这调度的回调函数是

performSyncWorkOnRoot.bind(null, root)
performConcurrentWorkOnRoot.bind(null, root)

这两个就是render阶段的入口,至此,状态更新与render阶段连接上了。

触发状态更新(this.setState) ------>创建update对象 ---> 从fiber到root(markUpdateLaneFromFiberToRoot) ---> 调度更新(ensureRootIsScheduled)
	------> render阶段 ----> commit阶段

以this.setState为例子:

调用this.setState会调用enqueueSetState方法,创建update对象(这里是calssComponent,对应的update的payload就是第一个参数),赋值callback,然后将update插入fiber.updateQueue上,接着执行scheduleUpdateOnFiber,该方法刚才也看见了

找到rootFiber,并且调度更新,执行performSyncWorkOnRoot/performConcurrentWorkOnRoot,开始render阶段。

创建Update

update对象是构成 react concurrent mode的关键。

每次状态更新,都会创建对应的Update,react中一共有三种组件,HostRoot(rootFiber), ClassComponent,FunctionComponent可以出发更新,不同组件工作方式不同,所以Update分为两种结构,其中,HostRoot和ClassComponent共用一种,FunctionComponent单独使用一种Update结构,虽然结构不同,但是他们的作用机制与工作流程大体相同。

Update的结构

类组件的update


字段的意义:

  • eventTime: 任务时间

  • lane 优先级相关字段,不同update的优先级不同。

  • tag 更新的类型,包括 UpdateState|ReplaceState|ForceUpdate|CaptureUpdate

  • payload 更新挂在的数据,不同类型组件挂在的数据不同,对于类组件,payload就是this.setState的第一个传参。对于HostRoot,payload为reactDom.render的第一个传惨。

  • Callback: 更新的回调函数 //setState的第二个参数,在commit阶段的layout阶段执行, 对于HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用

  • next 与其他update连接形成链表,放在fiber.updateQueue.shared.pending上。

类似于fiber树,update以next指针组成一个链表,一个fiber节点上的多个update以链表的形式存放在fiber.updateQueue.shared.pending上。

每个fiber节点最多同时存在两个updateQueue --Fiber.updateQueue,和fiber.alternamte.updateQueue。

即current fiber保存的updateQueue和workInProgress fiber上保存的updateQueue。

commit阶段完成页面渲染后,workInProgress Fiber树变为current Fiber树workInProgress Fiber树Fiber节点updateQueue就变成current updateQueue

updateQueue

三种类型,对于HostComponent,在completeWork的时候我们知道他会处理props存放在fiber.updateQueue上。而剩余的两种类型,刚好与两种update类型对应。

对于类组件使用的updateQueue:

  • baseState 保存本次更新前fiber的state。update会基于该state计算更新后的state。

  • firstBaseUpdate, lastBaeUpdate: 本次更新前如果该fiber有已保存的update,那么他们会以链表的形式存在,表头为firstBaseUpdate,表尾为lastBaeUpdate。

  • Shared.pending,存放着update组成的环状链表。比如ABCD,那么顺序将会是: shared.pending = D => A =>B => C => D。计算的时候,该环状链表会剪开,然后连接在lastBaeUpdate后面,如 lastBaseUpdate => A => B => C => D

  • Effets:保存update.callback不为null的update。即有回调函数的update。

更新的时候,会遍历该update链表,依此执行每个update计算产出新的state(类似于Array.prototype.reduce),获取到的新的state,就是本次更新的state,源码中叫做memoizedState。

在render阶段beginWork的时候,update的操作由processUpdateQueue完成,

function beginWork(...)
...
// mount时:根据tag不同,创建不同的子Fiber节点 update时,处理对应fiber上的updateQueue的update对象
switch (workInProgress.tag) 
...
 case ClassComponent: 
     ...
      return updateClassComponent( //处理类组件 调用updateClassInstance,updateClassInstance调用processUpdateQueue处理类组件的updateQueue
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    
...
 return updateHostRoot(current, workInProgress, renderLanes); // 调用processUpdateQueue
...
case CacheComponent: 
      if (enableCache) 
        return updateCacheComponent(current, workInProgress, renderLanes); // 调用processUpdateQueue
      
      break;
    
....

如图,他会将环状链表拆开,然后接入到lastBaseUpdate后面。然后

do while遍历执行update,优先级较低的,会放到新的neFirstBaseUpdate上去。

优先级较高的,执行该update,获取最新的state

最后一些收尾工作,更新queue上的fi rstBaseUpdate和baseState,更新当前fiber的memoizedState。

processUpdateQueue负责执行当前fiber的updaeQueue,计算出新的state保存。

state的变化在render阶段产生于上次不同的jsx对象,通过diff算法产生effectTag,赋值到新的fiber上,在commit阶段渲染到页面上。

总结:
  • 状态更新会产生update,update分为两种,函数组件一种,类组件和HostComponent一种。多个update以next相连,形成环状链表,存放在fiber.updateQueue.shared.pending上。

  • 而updateQueue又有三种类型,其中一种为HostComponent处理props生成的payload。第二种对应类组件的update,它上面存放着对应的update链表,还有当前的update使用的state。

  • 他们的关系就是: fiber上有updateQueue,updateQueue.shared.pending上存放着update组成的链表。

  • 创建完update之后,就会根据fiber找到rootFiber,然后开始调度,到达render阶段,render阶段会根据tag的不同,处理对应的fiber上的updateQueue,比如类组件上的updateQueue和hostRoot的updateQueue,他会计算出最新的state,赋值给fiber的memoizedState上。然后会产生不同的jsx对象,根据diff算法产生effecTag,挂到fiber上,在commie阶段渲染到页面。

  • 更新流程: 以this.setState为例子,调用this.setState会调用enqueueSetState方法,创建update对象,赋值callback,然后将update插入fiber.updateQueue上,接着执行scheduleUpdateOnFiber, 找到rootFiber,并且调度更新,执行performSyncWorkOnRoot/performConcurrentWorkOnRoot,开始render阶段(处理update产生新的fiber),开始comit阶段,结束。

    触发状态更新(this.setState) ------>创建update对象 ---> 从fiber到root(markUpdateLaneFromFiberToRoot) ---> 调度更新(ensureRootIsScheduled)
    	------> render阶段(处理updateQueue) ----> commit阶段
    
首次渲染流程:以ReactDOM.render为例子

首次执行ReactDOM.render会创建fiberRootNode和rootFiber

调用legacyRenderSubtreeIntoContainer,然后

legacyCreateRootFromDomContainer创建rootFiber和FiberRootNode,最终会调用

可以看到rootFiber 和fiberRootNode的联系就是 fiberRootNode.current = rootFiber; rootFiber.stateNode = fiberRootNode。

然后初始化rootfiber的updateQueue

现在已经做好了组件的初始化工作,只需要等待来一个update开启第一次更新。

接着执行updateContainer,这个函数很重要,是hostComponent创建update的方法。

可以看到,熟悉的流程,创建updat对象,hostComponent的update的pa yload对应的是render的第一个传惨,然后直接执行scheduleUpdateOnFiber方法,找到root fiber,调度执行开启render阶段。

所以第一次mount的时候,执行ReactDOM.render也可以看作是一次更新。

执行ReactDOM.render  
---->  创建rootFiber和fiberRootNode,初始化rootFiber的updateQueue  (legacyCreateRootFromDOMContainer)
---->  创建第一次渲染的update (UpdateContainer)
---->  开启调度(scheduleUpdateOnFiber) =》根据fiber找到rootFiber(markUpdateLaneFromFiberToRoot) => 调度更新 (ensureRootIsScheduled)
---->  判断同步异步,执行performSyncUnitOfWork/performConcurrentWorkOnRoot 开启render阶段
---->  render阶段处理HostComponent的updateQueue(beginWork中根据各组件调用processUpdateQueue处理updateQueue产生effectTag(mount只有rootFier会				产生effectTag))
---->  commit阶段渲染到页面 (before-mutation -> mutation -> layout)

渲染流程完成。

以上是关于react源码学习-实习篇-状态更新的主要内容,如果未能解决你的问题,请参考以下文章

自顶而下学习react源码 理念篇

React学习笔记1

React 系列导航

从源码的角度再看 React JS 中的 setState

react源码学习-架构篇-render阶段

react源码解析18事件系统