React浅谈setState

5,037 阅读10分钟

写到前面

为什么是setState,因为对于大家而言,大多数使用react的新手或者初学者,大多会直接接触到setState,而且这个方法也可能是接触最多的操作方法。那么要想详细了解setState究竟在React中做了什么事情,就需要深入了解一下。而在最新的React 16版本中,React的核心渲染框架时进行过一次升级的,由之前的React升级到了React Fiber。(PS:本文针对菜鸟、初级工程师而写,有错误不足之处,请各位大佬指出更正。感觉太low,请绕道,谢谢。)

  • 为什么会升级?
  • 为什么了解Fiber?

别着急,让我来慢慢给你们解答,在16版本之前,React使用的还是旧版的渲染核心,它的渲染过程是一口气完成,怎么理解呢?就是会一次性遍历你所有的Dom节点,这个过程取决于你的应用的复杂程度。当然,这个过程一般比较快,但是也不排除在大型复杂应用中出现比较长的等待时间,这个时间是基于ms级别的。而作为一个前端工程师,性能优化是比较重要的一方面之一,大家都知道,浏览器是的渲染引擎是单线程的,这就意味着一个时间段之内只能完成一件事。当你的应用过于复杂时,用户操作变多,弊端就显示出来了:卡顿,未响应,甚至是页面崩溃...这就是为什么React会升级到React Fiber,在未升级之前,渲染模式是这样的:

假设你的结构是这样的 A组件 => B组件 => C/D/E组件 D组件 => F组件 未使用Fiber架构的渲染方式

他的旧版渲染模式是这样的:

以render()函数为分界线。从顶层组件开始,一直往下,直至最底层子组件。然后再往上。组件update阶段同理。一直执行,直到完成,这个过程完全不理你。(我喜欢叫狗不理阶段)

在升级为Fiber之后,就如同游泳一样,每个一段时间,都需要上岸呼吸一口气,所以渲染模式就变成更了以下情况:

潜水员会每隔一段时间就上岸,看是否有更重要的事情要做。

加入fiber的react将组件更新分为两个阶段,Reconcile阶段和Commit阶段。

  • Reconcile阶段,在这个阶段内,React通过diff算法,判断哪些组价需要更新,经需要更新的组件打上tag(标记),再将所有需要更新的组件添加到一个数组中,等待或者执行更新任务。注意:这个阶段是可以被打断的,也就是说在这个阶段内,react检测到有用户操作行为,或者是其他的一些事情都会打断,在事件执行完毕之后在重新将进行此阶段,是重新进行。
  • Commit阶段,这个阶段是根据Reconcile阶段生成的更新的数组,遍历更新DOM,这个阶段是一次性执行完毕的,并且是不会被打断的。

通过这个俩个阶段,你就会明白,为什么之前会把componentWillMount、componentWillReviceProps和componentWillUpdate标记为不安全的生命周期函数了,因为在Reconcile阶段,被打断之后是重新进行的,就有可能造成对此的数据请求,对此渲染,造成不必要的资源、性能浪费(这里有一个比较有意思饥饿问题,聪明的同学应该已经猜出来了,react现在还没有公布解决方法哦)。

Fiber具体是什么样的?

Fiber其实是一个对象。在Fiber源码中,有这么一段描述

A Fiber is work on a Component that needs to be done or was done. There can be more than one per component.

Fiber就是通过对象记录组件上需要做或者已经完成的更新,一个组件可以对应多个Fiber。

接下来让我们看看Fiber具体是什么样子的?既然是一个对象,就肯定是{}模式。如下:

{
    tag,
    key,
    elementType,
    type,
    stateNode,
    return,
    child,
    sibling,
    index,
    ref,
    pendingProps,
    memoizedProps,
    updateQueue,
    memoizedState,
    firstContextDependency,
    mode,
    effectTag,
    nextEffect,
    firstEffect,
    lastEffect,
    expirationTime,
    childExpirationTime,
    alternate,
    actualDuration,
    actualStartTime,
    selfBaseDuration,
    treeBaseDuration
}

在render函数中创建的React Element树在第一次渲染的时候会创建一颗结构一模一样的Fiber节点树。不同的React Element类型对应不同的Fiber节点类型。一个React Element的工作就由它对应的Fiber节点来负责。

Fiber的优先级如下:

高优先级会打断正在执行的低优先级任务先执行。

一个React Element可以对应不止一个Fiber,因为Fiber在更新的时候,会从原来的Fiber(current)克隆出一个新的Fiber(alternate)。两个Fiber diff出的变化(side effect)记录在alternate上。所以一个组件在更新时最多会有两个Fiber与其对应,在更新结束后alternate会取代之前的current的成为新的current节点。

这是fiber在目前版本v16.6.3所维护的所有属性,具体想要了解阅读源码请看这里。ReactFiber.js

setState

在官方文档中,明确指出,要把state认作是不可变的,所以,现在更推崇的写法不是直接setState,而是通过setState的回调函数进行更改。

this.setState(() => {[key]: value});

好,不说题外话了,让我们进入今天的正题,setState。 大家写项目的时候,在index.js文件中,会引入两个文件,react,react-dom。setState在react文件是这样的:

熟不熟悉?Conmponent类,在这里面我们可以看看干了什么事情,接受props,context和updater,注意我拿红线标出来的部分,短路运算,再看看注释,这个updater是随后注入进去的。先不管是什么时候注入进去的,让我们接着往下看,setState肯定会触发更新,那我们就沿着this.updater往下走,去寻找ReactNoopUpdateQueue(react空操作更新队列),很多人会犯嘀咕,都空操作了还要更新什么?耐心点,这里的确是不进行任何更新操作,只是验证一个数据格式,和检验旧版V8引擎的一些错误,并抛出来。
这是什么?setState?干了什么?参数校验,如果通过就执行下面的方法,this指的当前实例。
在enqueueSetState方法中,也是实例验证。验证实例是否mounted。

在你的应用第一次渲染的时候,最主要的是关注react-dom的进行,前面说过updater是随后注入进去的,就是在react-dom加载的时候注入进去的。接下来,setState带大家去看看究竟是什么?

直接来看setState队列,这里需要3个参数可以看到分别是实例对象,载荷和回调函数。在这里我们先看在最开始生命4个变量分别是干什么用的,直接语义化就能猜出个大概来。

Q1:fiber通过get方法获取一些东西?

A1: 可以看到,源代码实现的方法,获再结合当前调用方法的上下文可以得知,当前的fiber获取到时当前实例上的一个_reactInternalFiber的值。这个值是什么,其实是通过相应的一个set方法,将当前实例和workInProgress传入,并给赋值给当前实例的_reactInternalFiber属性。

Q2:currentTime获取当前的时间?

A2:

  • 首先判断是否正在渲染中,是的话就返回最近一次的调度时间
  • 如果不在渲染中的话,会检查是否有上次遗留的待处理的工作。
  • 如果nextFlushedExpirationTime === NoWork || nextFlushedExpirationTime === Never,来判断优先级。
  • 重新计算当前的渲染时作为调度时间,并且return;
  • 如果上次有遗留,则直接返回当前调度时间。
  • rederingTime 可以随时更新,currentSechedulerTime只有在没有新任务的时候才更新

Q3:expirationTime获取到期时间?什么鬼?

A3:

  • 在此时,会进入第一个if条件判断,通过判断当前是否存在正在执行的上下文时间,是否正在进行渲染,还是其他情况。
  • 如果存在expirationContext,则到期时间就是修改为当前的上下文执行时间。
  • 如果正在调度时间的话,判断是否处于commit阶段,是的话就设置为同步优先级,否则的话就赋值为下次渲染到期时间。
  • 如果上述情况都不满足的情况下,就会计算当前实例fiber的优先级。
  • 这里分为异步和同步,分别调用不同的方法进行计算,获得优先级后则和同步更新一样, 创建update并放进队列, 然后调用sheuduleWork
  • 在这里还会有交互式刷新的判断,是追踪最短待处理的交互式到期时间。 这允许我们在需要时同步刷新所有交互式更新。
  • 最后返回当前所需要的到期时间。
  • 此步骤和2步骤可以合并为计算优先级

Q4:update创建update队列?

A4: 这个阶段就是通过createUpdate来创建一个更新对象。

在进行了一系列不可描述的过程之后,终于可以进行接下来的操作了。

首先调用flushPassiveEffects()来进行刷新,将被动影响的属性刷新一遍,接着是重头戏,调用enqueueUpdate()方法,将需要更新的fiber放入更新队列。

这里其实就是这么个原理:

第一部分

  1. 首先判断是不是只有一个fiber,只有一个fiber的话就让q1等于这个值,然后q2克隆q1
  2. 如果是有俩个fiber,则q1等于当前实例的fiber.updateQueue,q2就等于alternate.updateQueue;
  3. 如果两个fiber都没有更新队列。则q1,q2都创建新的。
  4. 只有一个fiber有更新队列。克隆以创建一个新的。
  5. 俩个fiber都有更新队列。总之就是,q1和q2都需要有一个fiber。

第二部分

  1. 当q1与q2是相等时,一位置实际上只有一个fiber,将此fiber插入到更新队列;
  2. 若q1和q2有一个是非空队列,则两个对列都需要更新;
  3. 当q1和q2两个队列都是非空,由于结构共享,两个列表中的最后一次更新是相同的。因此,只需q1添加到更新队列即可;
  4. 最后将q2的lastUpdate指针更新。

最后一步,就是掉用scheduleWork()方法,来进行最后的更新。在此方法中会根据优先级进行分片式更新。

  1. 首先调用scheduleWorkToRoot()方法,更新fiber的优先级,遍历到根组件的父级路径,并更新子组件的优先级。
  2. 为先前未计划的交互更新挂起的异步工作计数。
  3. 更新当前交互的挂起的异步工作计数。
  4. 监听更新列表的变化,返回root。

接下来,在commit阶段,一口气执行完毕。你的DOM就是最新的了。说了这么多,可能执行起来,就是短短的几十毫秒... 就比如下面

image

至此,setState整个过程算是完成了。

总结:这篇文章是鄙人第一次下手书写,有些地方可能表述不是很准确,可能有点啰嗦,但是我喜欢啊。俗话说万事开头难,但是过程也难啊,结果更难啊。对于代码也一样,要坚持下去,坚持下去你就得颈椎病了哦。本文有什么错误的地方,还烦请各路大神指出,鄙人是不会改滴,都会记在心里哒,上述是我对setState的理解,抛砖引玉,希望帮助大家有方向的去了解react原理机制。