基于React 源码深入浅出setState:深度刨析updater的结构和原理

2,568 阅读6分钟

作者 : 墨成   React 版本 :16.4.1

阅读本文之前,建议阅读:

1.基于React 源码深入浅出setState:官方文档的启示录

2.基于React 源码深入浅出setState:setState异步实现

上一篇 详细了解了setState的一些机制和原理,同时对 updater 也作了简单的解释,这篇文章我们将详细了解这个updater的数据结构和调用堆栈.

代码如下:

react\packages\react-reconciler\src\ReactFiberClassComponent.js

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.tag = ReplaceState;
    update.payload = payload;

    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'replaceState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueForceUpdate(inst, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'forceUpdate');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
};

代码并不长,但是包含的信息还是比较多,首先我们知道这个 updater 是个常量对象,而且在整个React它是单例的,即所有组件都是用同一个updater(注意与后面提及的update区别),updater中包含了3个非常重要的方法 enqueueSetState,enqueueForceUpdate,enqueueReplaceState 准确的说是包含了一个重要的方法 enqueueSetState

我们首先看下 在setState中调用的 enqueueSetState(inst, payload, callback)

this.updater.enqueueSetState(this, partialState, callback, 'setState');   

实际的 enqueueSetState(inst, payload, callback)只有三个参数,第四个参数是为默认的ReactNoopUpdateQueue 准备的,可以忽略。

/**
** @param {?react component} 组件事例 
* @param {object|function} payload 对应的就是 partialState
* @param {?function} callback 回调函数 
 
*/
enqueueSetState(inst, payload, callback) {
  const fiber = ReactInstanceMap.get(inst);
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, fiber);

  const update = createUpdate(expirationTime);
  update.payload = payload;
  if (callback !== undefined && callback !== null) {
    if (__DEV__) {
      warnOnInvalidCallback(callback, 'setState');
    }
    update.callback = callback;
  }

  enqueueUpdate(fiber, update);
  scheduleWork(fiber, expirationTime);
}
  1. 前三个变量 fiber,currentTime,expirationTime

const fiber = ReactInstanceMap.get(inst);

ReactInstanceMap拿到Component的Fiber实例 ,它其实等于

const fiber = inst._reactInternalFiber; 


FB的工程师们通过 ReactInstanceMap这样的实现应该是考虑到后续的拓展和对私有变量取值的封装,后续 ReactInstanceMap 应该会使一个类似<key,value>的键值对数据类型 .

  const currentTime = requestCurrentTime();

React通过Performance.now()(如果浏览器不支持这个API ,则是用Date.now())获取时间值(说明:返回当前日期和时间的 Date 对象与'1970/01/01 00:00:00'之间的毫秒值(北京时间的时区为东8区,起点时间实际为:'1970/01/01 08:00:00'),所以这个currentTime是个number类型,即typeof currentTime === 'number' )

  const expirationTime = computeExpirationForFiber(currentTime, fiber);

expirationTime根据当前时间和 fiber计算它的 有效时间.expirationTime 涉及的体系比较复杂,后续会结合实际的代码讲解.

2.Update对象

    const update = createUpdate(expirationTime);
    update.payload = payload;

    创建一个 update 的对象,数据结构在下面会有提及,这里的payLoad 还是partialState

    export function createUpdate(expirationTime: ExpirationTime): Update<*> {
      return {
        expirationTime: expirationTime,
    
        tag: UpdateState,
        payload: null,
        callback: null,
    
        next: null,
        nextEffect: null,
      };
    }

    3. enqueueUpdate&scheduleWork

    故事讲到这里,应该很快进入高潮,两个比较核心的看点

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);

    在了解这两个函数之前,我们首先看下两个数据结构

    位置:react\packages\react-reconciler\src\ReactUpdateQueue.js
    
    export type Update<State> = {
      expirationTime: ExpirationTime,//有效时间
    
      tag: 0 | 1 | 2 | 3,//tag  1 2 3 4 其中之一 
      payload: any,//update的partialState
      callback: (() => mixed) | null,//setState的回调函数
    
      next: Update<State> | null,//指向下一个 update的指针
      nextEffect: Update<State> | null,//指向下一个effect(变化) 
    };
    
    export type UpdateQueue<State> = {
      baseState: State,//就是Component初始化的state
    
      firstUpdate: Update<State> | null,
      lastUpdate: Update<State> | null,
    
      firstCapturedUpdate: Update<State> | null,
      lastCapturedUpdate: Update<State> | null,
    
      firstEffect: Update<State> | null,
      lastEffect: Update<State> | null,
    
      firstCapturedEffect: Update<State> | null,
      lastCapturedEffect: Update<State> | null,
    };

    每个update有个指向下一个update的"指针" next,  UpdateQueue<State> 是个单向链表,firstxxx  lastxxx分别指向链表头部和尾部. enqueueUpdate(fiber, update),所以很多人错误的认为updateQuenue 是一个(类)数组,  代码如下 :

    //以下注释仅对 ClassComponent有效(即你的组件是继承React.Component)
    export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
      // Update queues are created lazily. Update queues被延迟创建(即这个组件没有update 我们没有必要给它fiber来创建这样属性) 
      const alternate = fiber.alternate;
      let queue1;//设计它是为了指向current的updateQueue
      let queue2; //设计它是为了指向alternate 的updateQueue
      if (alternate === null) {//alternate为null,对于classcomponent第一次调用setState时alternate为null
        // There's only one fiber.
        queue1 = fiber.updateQueue;
        queue2 = null;
        if (queue1 === null) {//首次调用setState创建updateQueue,这时的memoizedState为初始值
          queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        }
      } else {
        // There are two owners.
        queue1 = fiber.updateQueue;//current Fiber updateQueue 
        queue2 = alternate.updateQueue;
        if (queue1 === null) {
          if (queue2 === null) {
            // Neither fiber has an update queue. Create new ones.
            //如果都为空,则分别创建,这个case我没有找到
            queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
            queue2 = alternate.updateQueue = createUpdateQueue(
              alternate.memoizedState,
            );
          } else {
            // Only one fiber has an update queue. Clone to create a new one.
            // 如果有一个有update Queue,则克隆一个 
            queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
          }
        } else {
          if (queue2 === null) {
            // Only one fiber has an update queue. Clone to create a new one.
            queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
          } else {
            // Both owners have an update queue.
          }
        }
      }
      if (queue2 === null || queue1 === queue2) { 
        // There's only a single queue.
        //一般发生在首次 setState
        appendUpdateToQueue(queue1, update);
      } else {
        // There are two queues. We need to append the update to both queues,
        // while accounting for the persistent structure of the list — we don't
        // want the same update to be added multiple times.
         // 翻译:如果存在两个queues,我们需要追加这个 update到这个两个 queues.
          //然而对于这种持久性结构的列表(updateQueue)需要保证一次update不能添加多次 
        if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
          // One of the queues is not empty. We must add the update to both queues.
          appendUpdateToQueue(queue1, update);
          appendUpdateToQueue(queue2, update);
        } else {
          // Both queues are non-empty. The last update is the same in both lists,
          // because of structural sharing. So, only append to one of the lists.
          appendUpdateToQueue(queue1, update);
          // But we still need to update the `lastUpdate` pointer of queue2.
          queue2.lastUpdate = update;
        }
      }
    
      if (__DEV__) {
        if (
          fiber.tag === ClassComponent &&
          (currentlyProcessingQueue === queue1 ||
            (queue2 !== null && currentlyProcessingQueue === queue2)) &&
          !didWarnUpdateInsideUpdate
        ) {
          warningWithoutStack(
            false,
            'An update (setState, replaceState, or forceUpdate) was scheduled ' +
              'from inside an update function. Update functions should be pure, ' +
              'with zero side-effects. Consider using componentDidUpdate or a ' +
              'callback.',
          );
          didWarnUpdateInsideUpdate = true;
        }
      }
    }

    关于alternate ( 参考React Fiber Architecture) 


    简单的概括下:在任何时候,一个组件的实例最多应对两个fiber:current,alternate(flushed fiber or work-in-progress fiber,不同阶段的叫法),alternate是延迟创建并从 cloneFiber这个方法中克隆出来的 ,也就是所谓的persitent structure,他们结构共享(Structural Sharing).

    Structural Sharing 和 延迟创建贯穿整个React  ,比如这里 

    FB 的工程师有段注释:

    // Update queues are created lazily.

    update queues是延迟创建,要注意的是,这里的延迟创建不仅仅是对current,对alternate的queue也是一样. 

    scheduleWork 涉及调度器的知识体系,后续学习完成补充着部分的欠缺 。

    参考资料 :

    1.React Fiber Architercture