同一事件中屡次setState时react setState的源码

问题

又一次的bug,很少说了,都是泪,这里直接贴一下“有问题的代码”吧javascript

import React from "react";

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: 1,
      flag: false,
    };
  }

  onValidateSuccessSubmit = (data) => {
    this.setState({ flag: true });
    // do something
    this.setState({ data });
  };

  asyncFunc = () => {
    return true;
  };

  onClick = async () => {
    const validate = await this.asyncFunc();
    if (validate) {
      this.onValidateSuccessSubmit(2);
    } else {
      // do something
    }
  };

  onSubmit = () => {
    console.log(this.state.data);
  };

  render() {
    return (
      <div onClick={this.onClick}> <div>click it</div> <ChildComponent flag={this.state.flag} onSubmit={this.onSubmit} /> </div>
    );
  }
}

export default Parent;

class ChildComponent extends React.Component {
  componentWillReceiveProps(nextProps) {
    if (nextProps.flag) {
      nextProps.onSubmit();
    }
  }
  render() {
    return <div>child component</div>;
  }
}
复制代码

简单说一下这段代码的逻辑:html

  1. 点击parent组件执行onClick事件,此事件会经过async/await拿到一个变量。
  2. 执行onValidateSuccessSubmit事件,这个事件触发两次setState
  3. setState把flag置为true,触发子组件的componentWillReceiveProps钩子函数,执行父组件的onSubmit函数。
  4. 父组件的onSubmit函数输出state中的数据。

这时的console会输出两次,结果分别是1和2,因此当咱们在使用onSubmit函数处理业务逻辑时,拿到的也是更新以前的state,而后就没有而后了😭java

你们能够先思考一下为何是这样?是什么缘由致使的呢?node

初步猜测

由于输出了两次,而且咱们也都知道setState有同步和异步,因此会不会是setState的这种同步异步状态致使的呢。为了验证咱们的猜测,咱们把其中的关键代码改为这样:react

onClick = () => {
    const validate = this.asyncFunc();
    if (validate) {
      setTimeout(() => {
        this.onValidateSuccessSubmit(2);
      }, 0);
    } else {
      // do something
    }
};
复制代码

果不其然,输出的结果和使用async/await的一致,再看一下源码,验证一下运行的流程是否彻底一致。segmentfault

合成事件setState源码

下面咱们就看一下当咱们setState时,react具体是怎么作的(react版本16.12.0)数组

首先看一下正常的合成事件中setState,此时关键代码以下:markdown

onClick = () => {
    const validate = this.asyncFunc();
    if (validate) {
      this.onValidateSuccessSubmit(2);
    } else {
      // do something
    }
};
复制代码

当咱们执行this.setState({ flag: true })时,react处理流程以下:架构

免喷声明:因为这是本人经过debugger的同时再基于本人对于react很是浅薄的理解写出来的文章,对于react里很是多的细节处理没有介绍到,还但愿你们多多理解,对于其中的错误地方多多指正。app

执行setState

// packages/react/src/ReactBaseClasses.js
/** * Sets a subset of the state. Always use this to mutate * state. You should treat `this.state` as immutable. * * There is no guarantee that `this.state` will be immediately updated, so * accessing `this.state` after calling this method may return the old value. * * There is no guarantee that calls to `setState` will run synchronously, * as they may eventually be batched together. You can provide an optional * callback that will be executed when the call to setState is actually * completed. * * When a function is provided to setState, it will be called at some point in * the future (not synchronously). It will be called with the up to date * component arguments (state, props, context). These values can be different * from this.* because your function may be called after receiveProps but before * shouldComponentUpdate, and this new state, props, and context will not yet be * assigned to this. * * @param {object|function} partialState Next partial state or function to * produce next partial state to be merged with current state. * @param {?function} callback Called after state is updated. * @final * @protected */
Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码

感兴趣的同窗能够自行看一下注释,更有助于对react的理解。

setState函数会执行this.updater.enqueueSetState(this, partialState, callback, 'setState');,其中this就是当前组件了,partialState就是咱们将要修改的state,callback就是修改state后的回调,其实也是咱们常见的确保state更新以后触发事件的函数。

enqueueSetState

enqueueSetState是挂载在classComponentUpdater上的一个方法,以下所示

// packages/react-reconciler/src/ReactFiberClassComponent.js
const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

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

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  ...
}
复制代码

咱们挑重点看一下属性赋值部分

const expirationTime = computeExpirationForFiber(
  currentTime,
  fiber,
  suspenseConfig,
);
复制代码

这个函数会根据当前react的模式返回不一样的expirationTime,这里返回的是Sync常量,关于react的legacy、blocking、concurrent三种模式你们能够自行查阅 使用 Concurrent 模式(实验性)- 特性对比

// packages/react-reconciler/src/ReactFiberWorkLoop.js
export function computeExpirationForFiber( currentTime: ExpirationTime, fiber: Fiber, suspenseConfig: null | SuspenseConfig, ): ExpirationTime {
  const mode = fiber.mode;
  if ((mode & BlockingMode) === NoMode) {
    return Sync;
  }
  ...
  return expirationTime;
}
复制代码

咱们再看一下函数执行部分,enqueueUpdate(fiber, update)

这个函数传入两个参数,fiber便是当前实例对应的fiber,update咱们能够看到是经过createUpdate函数建立并返回的一个update对象

// packages/react-reconciler/src/ReactUpdateQueue.js
export function createUpdate( expirationTime: ExpirationTime, suspenseConfig: null | SuspenseConfig, ): Update<*> {
  let update: Update<*> = {
    expirationTime,
    suspenseConfig,

    tag: UpdateState,
    payload: null,
    callback: null,

    next: null,
    nextEffect: null,
  };
  if (__DEV__) {
    update.priority = getCurrentPriorityLevel();
  }
  return update;
}
复制代码

enqueueUpdate

这一步的操做主要是给当前的fiber添加updateQueue

// packages/react-reconciler/src/ReactUpdateQueue.js
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // Update queues are created lazily.
  const alternate = fiber.alternate;
  // queue1和queue2是fiber成对出现的队列
  // queue1是current queue
  // queue2是work-in-progress queue
  // 感兴趣的能够看一下此文件上方的注释信息及参考连接中的Fiber架构的工做原理
  let queue1;
  let queue2;
  if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      // 首次执行setState时,fiber的任务队列都为null,执行下面的代码
      // createUpdateQueue从函数名咱们不难看出此函数用于建立更新队列,参数fiber.memoizedState为constructor中this.state的初始值。
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // There are two owners.
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // Neither fiber has an update queue. Create new ones.
        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.
        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.
    // 随后运行下面代码,将须要更新的对象添加至第一个队列中
    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.
    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;
    }
  }
}
复制代码

重点是下面两段代码:

...
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
...
appendUpdateToQueue(queue1, update);
...
复制代码

此时updateQueue的firstUpdate和lastUpdate均为createUpdate建立的update对象,此时fiber的updateQueue结构为:

updateQueue: {
    baseState: { a: 1, flag: false },
    firstCapturedEffect: null,
    firstCapturedUpdate: null,
    firstEffect: null,
    firstUpdate: {
      callback: null,
      expirationTime: 1073741823,
      next: null,
      nextEffect: null,
      payload: { flag: true },
      priority: 98,
      suspenseConfig: null,
      tag: 0,
    },
    lastCapturedEffect: null,
    lastCapturedUpdate: null,
    lastEffect: null,
    lastUpdate: {
      callback: null,
      expirationTime: 1073741823,
      next: null,
      nextEffect: null,
      payload: { flag: true },
      priority: 98,
      suspenseConfig: null,
      tag: 0,
    },
  },
复制代码

scheduleWork(重点

这里开始进入调度阶段

// packages/react-reconciler/src/ReactFiberWorkLoop.js
export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTime, ) {
  // 检查是否掉入死循环
  checkForNestedUpdates();
  // dev环境下的warn,跳过
  warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);

  // 从名字来看是标记fiber到root的更新时间,函数内部主要作了两件事
  // fiber.expirationTime置为更大的expirationTime,expirationTime越大优先级越高
  // 递归fiber的父节点,并将其childExpirationTime也置为expirationTime
  // 不太能理解这个函数,莫非是和react的事件机制有关?
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }

  checkForInterruption(fiber, expirationTime);
  recordScheduleUpdate();

  // TODO: computeExpirationForFiber also reads the priority. Pass the
  // priority as an argument to that function and this one.
  const priorityLevel = getCurrentPriorityLevel();

  if (expirationTime === Sync) {
    if (
      // Check if we're inside unbatchedUpdates
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // Check if we're not already rendering
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      schedulePendingInteractions(root, expirationTime);

      // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
      // root inside of batchedUpdates should be synchronous, but layout updates
      // should be deferred until the end of the batch.
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root);
      schedulePendingInteractions(root, expirationTime);
      if (executionContext === NoContext) {
        // Flush the synchronous work now, unless we're already working or inside
        // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
        // scheduleCallbackForFiber to preserve the ability to schedule a callback
        // without immediately flushing it. We only do this for user-initiated
        // updates, to preserve historical behavior of legacy mode.
        flushSyncCallbackQueue();
      }
    }
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }
  ...
}
export const scheduleWork = scheduleUpdateOnFiber;
复制代码

直接看重点代码逻辑判断部分,经过上面enqueueSetState的属性赋值咱们知道,expirationTime被赋值为Sync常量,因此这里进到

if (
  // Check if we're inside unbatchedUpdates
  (executionContext & LegacyUnbatchedContext) !== NoContext &&
  // Check if we're not already rendering
  (executionContext & (RenderContext | CommitContext)) === NoContext
)
复制代码

看来这里就是传说中react批处理state的逻辑了,一堆莫名其妙的二进制变量加上位运算符属实让人头大,不过还好这些变量都在该文件内,我们来慢慢捋一下

const NoContext = /* */ 0b000000;
const BatchedContext = /* */ 0b000001;
const EventContext = /* */ 0b000010;
const DiscreteEventContext = /* */ 0b000100;
const LegacyUnbatchedContext = /* */ 0b001000;
const RenderContext = /* */ 0b010000;
const CommitContext = /* */ 0b100000;
...
// Describes where we are in the React execution stack
let executionContext: ExecutionContext = NoContext;
复制代码

此时executionContext变量值为number6,LegacyUnbatchedContext值为number0,NoContext值为number0。executionContext这个变量你们先着重记一下,表示react执行栈的位置,至于为何是6,我们后面再讲。进入判断逻辑,条件(executionContext & LegacyUnbatchedContext) !== NoContext不符,进入else,

ensureRootIsScheduled

// packages/react-reconciler/src/ReactFiberWorkLoop.js
function ensureRootIsScheduled(root: FiberRoot) {
  ...
  const existingCallbackNode = root.callbackNode;
  ...
  // If there's an existing render task, confirm it has the correct priority and
  // expiration time. Otherwise, we'll cancel it and schedule a new one.
  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    const existingCallbackExpirationTime = root.callbackExpirationTime;
    if (
      // Callback must have the exact same expiration time.
      existingCallbackExpirationTime === expirationTime &&
      // Callback must have greater or equal priority.
      existingCallbackPriority >= priorityLevel
    ) {
      // Existing callback is sufficient.
      return;
    }
    // Need to schedule a new task.
    // TODO: Instead of scheduling a new task, we should be able to change the
    // priority of the existing one.
    cancelCallback(existingCallbackNode);
  }
  ...
  let callbackNode;
  if (expirationTime === Sync) {
    // Sync React callbacks are scheduled on a special internal queue
    callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  }
  ...
  root.callbackNode = callbackNode;
}
复制代码

因为是第一次setState,root中并无调度任务,进入expirationTime === Sync逻辑,performSyncWorkOnRoot.bind(null, root)当成参数传进了scheduleSyncCallback

scheduleSyncCallback

// packages/react-reconciler/src/SchedulerWithReactIntegration.js
const fakeCallbackNode = {};
...
let syncQueue: Array<SchedulerCallback> | null = null;
...
export function scheduleSyncCallback(callback: SchedulerCallback) {
  // Push this callback into an internal queue. We'll flush these either in
  // the next tick, or earlier if something calls `flushSyncCallbackQueue`.
  if (syncQueue === null) {
    syncQueue = [callback];
    // Flush the queue in the next tick, at the earliest.
    immediateQueueCallbackNode = Scheduler_scheduleCallback(
      Scheduler_ImmediatePriority,
      flushSyncCallbackQueueImpl,
    );
  } else {
    // Push onto existing queue. Don't need to schedule a callback because
    // we already scheduled one when we created the queue.
    syncQueue.push(callback);
  }
  return fakeCallbackNode;
}
复制代码

syncQueue是一个数组类型的全局变量,初始值为null,并把performSyncWorkOnRoot.bind(null, root)给赋值进去,immediateQueueCallbackNode不影响流程暂不讨论,最后return fakeCallbackNode,函数内部也没处理fakeCallbackNode,因此返回空对象。返回的这个空对象赋值给了root.callbackNode

schedulePendingInteractions

// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, expirationTime);
复制代码

经过注释咱们能够了解,这个函数的主要做用是trace,并不影响流程。此任务完成后,进入下个逻辑判断executionContext === NoContext,条件不符,结束scheduleWork任务。

到这里this.setState({ flag: true })执行完毕了,咱们能够看到,react只是把这个SchedulerCallback给push进了内部的队列中,并无diff的操做,也没有触发渲染的逻辑,也正所以setState并非每次都会触发组件的渲染。

接下来this.setState({ data })过程因为root已经存在了callbackNode,因此在ensureRootIsScheduled中直接return结束任务。

因为篇幅问题,后续的流程及渲染视图过程很少加讨论,感兴趣的同窗能够自行研究。

setTimeout时setState源码

关键代码以下:

onClick = () => {
    const validate = this.asyncFunc();
    if (validate) {
      setTimeout(() => {
        this.onValidateSuccessSubmit(2);
      }, 0);
    } else {
      // do something
    }
};
复制代码

setTimeout时setState过程和合成事件相似,不一样之处在于scheduleWork中executionContext的值变成了number 0,因此执行了flushSyncCallbackQueue,看来合成事件和setTimeout的执行不一样之处就在executionContextflushSyncCallbackQueue上面了,咱们先来看一下flushSyncCallbackQueue这个函数作了什么。

flushSyncCallbackQueue

export function flushSyncCallbackQueue() {
  if (immediateQueueCallbackNode !== null) {
    const node = immediateQueueCallbackNode;
    immediateQueueCallbackNode = null;
    Scheduler_cancelCallback(node);
  }
  flushSyncCallbackQueueImpl();
}
复制代码

flushSyncCallbackQueueImpl

function flushSyncCallbackQueueImpl() {
  if (!isFlushingSyncQueue && syncQueue !== null) {
    // Prevent re-entrancy.
    isFlushingSyncQueue = true;
    let i = 0;
    try {
      const isSync = true;
      const queue = syncQueue;
      runWithPriority(ImmediatePriority, () => {
        for (; i < queue.length; i++) {
          let callback = queue[i];
          do {
            callback = callback(isSync);
          } while (callback !== null);
        }
      });
      syncQueue = null;
    } catch (error) {
      // If something throws, leave the remaining callbacks on the queue.
      if (syncQueue !== null) {
        syncQueue = syncQueue.slice(i + 1);
      }
      // Resume flushing in the next tick
      Scheduler_scheduleCallback(
        Scheduler_ImmediatePriority,
        flushSyncCallbackQueue,
      );
      throw error;
    } finally {
      isFlushingSyncQueue = false;
    }
  }
}
复制代码

这个方法咱们能够清楚的看到try代码块里,拿出了以前的syncQueue任务队列,根据优先级开始执行这些任务。

executionContext

上面setState过程当中咱们并无发现该变量有变化,在查阅相关资料后发现是react在处理合成事件时改变了此变量,也就是setState以前对合成事件的处理,咱们看一下点击合成事件时的调用栈

image.png

从dispatchDiscreteEvent到callCallback都是react在处理合成事件了,通过一番调查,终于给搞清楚了。

discreteUpdates$1

function discreteUpdates$1(fn, a, b, c) {
  var prevExecutionContext = executionContext;
  executionContext |= DiscreteEventContext;
  try {
    // Should this
    return runWithPriority$2(UserBlockingPriority$2, fn.bind(null, a, b, c));
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      flushSyncCallbackQueue();
    }
  }
}
复制代码

executionContext |= DiscreteEventContext,关于位操做符不懂得能够自行查阅,这里再也不赘述,这里按位或以后赋值给executionContext,此时executionContext变量值是0b000100,也便是十进制中的4。

你们注意一下finally里的代码块,刚进来时prevExecutionContext为0b000000,try代码块中代码结束后,又把prevExecutionContext赋值给了executionContext

DiscreteEventContext是全局变量,默认值为0b000100。而合成事件中onClick就是DiscreteEvent,关于react的事件类型能够参考React 事件 | 1. React 中的事件委托

batchedEventUpdates$1

function batchedEventUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= EventContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      flushSyncCallbackQueue();
    }
  }
}
复制代码

executionContext |= EventContext,这里再次按位或以后赋值给executionContext,此时executionContext变量值是0b00110,也便是十进制中的6。

后记

这片文章只是简单叙述一下setState的逻辑,看源码的过程当中才发现本身对react知之甚少,知其然不知其因此然,react对合成事件的处理,fiber机制,concurrent模式,渲染视图。。。之后慢慢填坑吧

参考连接

相关文章
相关标签/搜索