setState原理

  • setState是异步更新的
    setState调用时,会执行enqueueSetState方法对当前要设置的state和_pendingStateQueue更新队列进行合并,最终通过enqueueUpdate执行state更新。首先,我们看个例子,如下所示,如果你知道答案那么你对这个机制是了解的。
 componentDidMount() {
    console.log('App dit')
    this.setState({value: this.state.value + 1})
    console.log('value:', this.state.value)
    this.setState({value: this.state.value + 1})
    console.log('value:', this.state.value)
    setTimeout(() => {
      this.setState({value: this.state.value + 1})
      console.log('value:', this.state.value)
      this.setState({value: this.state.value + 1})
      console.log('value:', this.state.value)
    }, 0);
    
  }

下面是张简单的机制流程图


1-1.png

那到此为止,我们可以知道前两次的state输出都是0了。那解释后面两state之前,我们来介绍一个概念,transaction(事务),react更新机制依赖事务,被事务所包裹。所谓事务就是一个高阶函数,用wrapper封装起来,然后通过事务提供的perform方法执行method,最后通过事务close掉。那前面两次setState的时候,我们通过setState的调用栈发现,前两次已经处于batchUpdate中了,因为React组件渲染到Dom中的过程就处于一个大事务中,然后isBatchUpdates标识为true,所以无法立即更新,而settimerout则没有这么操作,所以每次setState都会更新了,所以上面答案就是 0 0 2 3

React16最新版本setState执行过程

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');
};

这里我们发现,还是调用enqueueSetState,接下来我们看下这个方法里面做了啥:

  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);
  },

这里,先获取fiber实例,然后获取当前时间,这个当前时间,这个当前时间是这样算的:如果现在是处于react内,直接根据当前时间Now生成,如果是在react外(比如在浏览器原生事件内,那么这个期间发生的所有update都会被重置为同一个更新,当然时间也是一样的),然后是获取suspense配置,这个就不多说了,expirationTime根据currentTime生成,我们看下图源码,到期时间会根据当前fiber的mode或者是在渲染期间直接使用当前的渲染超时时间,或者如果配置了suspenseConfig,则根据suspenseConfig.timeoutMs生成,或者根据priorityLevel(任务优先级)生成超时。


image.png

接下来,执行createUpdate,这个主要是生成一个update对象,这个对象含有以下属性:

let update: Update<*> = {
    expirationTime,
    suspenseConfig,

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

    next: null,
    nextEffect: null,
  };

然后执行enqueueUpdate,我们看下面源码,这里我们先获取alternate,这个参数是判断当前是否是一个fiber,那这个方法主要是初始化queue1和queue2,然后加入到appendUpdateToQueue

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // Update queues are created lazily.
  const alternate = fiber.alternate;
  let queue1;
  let queue2;
  if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      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;
    }
  }
}

下面是appendUpdateToQueue,如果队列是空的,则queue队列的队头和队尾指针指向当前update,否则队尾指针指向当前的update。

function appendUpdateToQueue<State>(
  queue: UpdateQueue<State>,
  update: Update<State>,
) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}

最后我们回到主函数,执行scheduleWork

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  expirationTime: ExpirationTime,
) {
//检测是否嵌套超过最大深度,如果超过就报"超过最大更新深度。
// 当组件在componentWillUpdate或componentdiddupdate内重复调用setState时,可能会发生这种情况。
// React限制嵌套更新的数量以防止无限循环。超过最大更新深度。当组件在useffect内部调用setState时,
// 可能会发生这种情况,但useffect要么没有依赖数组,要么在每个呈现上都有一个依赖项更改"
  checkForNestedUpdates();
  warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);
// 查找要更新的fiber所依赖的Root,如果没有找到,则说明是处于卸载的组件里面。
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }
// 检测是否接受本次更新,如果更新时间小于渲染时间则终止,或者当前正在执行的root为空也终止
  checkForInterruption(fiber, expirationTime);
  recordScheduleUpdate();

  // 获取当前的任务优先级别
  const priorityLevel = getCurrentPriorityLevel();
// 如果超时时间是sync
  if (expirationTime === Sync) {
    if (
      // Check if we're inside unbatchedUpdates 检查我们是否在未锁定的更新中
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // Check if we're not already rendering  检查我们是否还没有渲染
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // 如果上述条件都符合,则在更新前要把当前挂起的交互数据进行保存
      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) {
    
        flushSyncCallbackQueue();
      }
    }
  } else {
// 使用此函数可为root安排任务。每个root只有一个任务;如果任务已被调度,
// 我们将检查以确保现有任务的到期时间与root所工作的下一级的到期时间相同。
// 这个函数在每次更新之前调用,并且在退出任务之前就被调用。
    ensureRootIsScheduled(root);
// 在更新前要把当前挂起的交互数据进行保存
    schedulePendingInteractions(root, expirationTime);
  }

  if (
    (executionContext & DiscreteEventContext) !== NoContext &&
    // 仅考虑用户阻塞优先级或更高级别的更新
    // discrete, even inside a discrete event.
    (priorityLevel === UserBlockingPriority ||
      priorityLevel === ImmediatePriority)
  ) {
    // This is the result of a discrete event. Track the lowest priority
    // discrete update per root so we can flush them early, if needed.
    if (rootsWithPendingDiscreteUpdates === null) {
      rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
    } else {
      const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
      if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
        rootsWithPendingDiscreteUpdates.set(root, expirationTime);
      }
    }
  }
}

最后我们总结一下


react16 setState机制.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容