拆解setState[三][一源看世界][之React]

上一章节《拆解setState[二][一源看世界][之React]》讲到了更新过程最核心的方法flushBatchedUpdates,那我们接着聊

flushBatchedUpdates方法的源码中可以看出,它在ReactUpdatesFlushTransaction这个事务中执行了runBatchedUpdates方法,源码如下:

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  ...

  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.
  dirtyComponents.sort(mountOrderComparator);

  // Any updates enqueued while reconciling must be performed after this entire
  // batch. Otherwise, if dirtyComponents is [A, B] where A has children B and
  // C, B could update twice in a single batch if C's render enqueues an update
  // to B (since B would have already updated, we should skip it, and the only
  // way we can know to do so is by checking the batch counter).
  updateBatchNumber++;

  for (var i = 0; i < len; i++) {
    // If a component is unmounted before pending changes apply, it will still
    // be here, but we assume that it has cleared its _pendingCallbacks and
    // that performUpdateIfNecessary is a noop.
    var component = dirtyComponents[i];

    // If performUpdateIfNecessary happens to enqueue any new updates, we
    // shouldn't execute the callbacks until the next render happens, so
    // stash the callbacks first
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;

    var markerName;
    if (ReactFeatureFlags.logTopLevelRenders) {
      var namedComponent = component;
      // Duck type TopLevelWrapper. This is probably always true.
      if (
        component._currentElement.props ===
        component._renderedComponent._currentElement
      ) {
        namedComponent = component._renderedComponent;
      }
      markerName = 'React update: ' + namedComponent.getName();
      console.time(markerName);
    }

    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction,
      updateBatchNumber
    );

    if (markerName) {
      console.timeEnd(markerName);
    }

    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(
          callbacks[j],
          component.getPublicInstance()
        );
      }
    }
  }
}

这个方法遍历所有的dirty components,通过mount order进行排序(因为更新是从父级到子级),将所有setState的callback方法加入事务的队列,运行ReactReconcilerperformUpdateIfNecessary方法。所以我们得去看看ReactReconciler


ReactReconciler & performUpdateIfNeeded - 最后的步骤了

直接看源码实现吧

  performUpdateIfNecessary: function(
    internalInstance,
    transaction,
    updateBatchNumber
  ) {
    ...
    internalInstance.performUpdateIfNecessary(transaction);
    ...
  },

啊,原来是调用了internalInstance的方法,在上一章节中我们说过internalInstanceReactCompositeComponentWrapper实例,它的prototype继承了ReactCompositeComponent.Mixin,所以我们很容易就在ReactCompositeComponent找到了performUpdateIfNecessary这个方法,看下实现吧

  performUpdateIfNecessary: function(transaction) {
    if (this._pendingElement != null) {
      ReactReconciler.receiveComponent(
        this,
        this._pendingElement,
        transaction,
        this._context
      );
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(
        transaction,
        this._currentElement,
        this._currentElement,
        this._context,
        this._context
      );
    } else {
      this._updateBatchNumber = null;
    }
  },

这个方法分为两部分:

  • ReactReconciler.receiveComponent - 在element级别去比较components。所以element实例比较后,如果它们不一样或者context改变了,就会触发internal instance的receiveComponent
  • this.updateComponent将被调用,当有pending state的情况

你可能在想有必要检查pending state或者force updates吗?state必须处于pending状态那是因为你调用了setState,对吗?不是滴,updateComponent是递归的所有你可以有更新的组件,但pending state是空的。同时对_pendingElement的检查是用于处理children被更新的场景。

updateComponent: function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext
  ) {
    var inst = this._instance;
    ...

    var willReceive = false;
    var nextContext;
    var nextProps;

    // Determine if the context has changed or not
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }

    nextProps = nextParentElement.props;

    // Not a simple state update but a props update
    if (prevParentElement !== nextParentElement) {
      willReceive = true;
    }

    // An update here will schedule an update but immediately set
    // _pendingStateQueue which will ensure that any state updates gets
    // immediately reconciled instead of waiting for the next batch.
    if (willReceive && inst.componentWillReceiveProps) {
      ...
      inst.componentWillReceiveProps(nextProps, nextContext);
      ...
    }

    var nextState = this._processPendingState(nextProps, nextContext);
    var shouldUpdate = true;

    if (!this._pendingForceUpdate && inst.shouldComponentUpdate) {
      ...
      shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
      ...
    }

    ...

    this._updateBatchNumber = null;
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Will set `this.props`, `this.state` and `this.context`.
      this._performComponentUpdate(
        nextParentElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        nextUnmaskedContext
      );
    } else {
      // If it's determined that a component should not update, we still want
      // to set props and state but we shortcut the rest of the update.
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  },

又是一个大方法!我们一步一步来解析它吧:

  • 首先,先对context进行检查。如果改变了,context会被存进nextContext变量中。这里就不进行展开了。
  • updateComponent检查props更新或者只是state更新。如果props更新,则触发componentWillReceiveProps生命周期方法的执行
  • 接下来是处理当前最新的state。_processPendingState方法就是用来搞定这事的
  • 最后是判断component是否应该更新虚拟DOM,如果是,则通过_performComponentUpdate进行更新。如果不是,则仅仅是更新变量的值

瞧一瞧_processPendingState

  _processPendingState: function(props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    var replace = this._pendingReplaceState;
    this._pendingReplaceState = false;
    this._pendingStateQueue = null;

    if (!queue) {
      return inst.state;
    }

    if (replace && queue.length === 1) {
      return queue[0];
    }

    var nextState = assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      var partial = queue[i];
      assign(
        nextState,
        typeof partial === 'function' ?
          partial.call(inst, nextState, props, context) :
          partial
      );
    }

    return nextState;
  },

可以看出设置和替换state共享同个队列_pendingStateQueue,有一个属性_pendingReplaceState用于判断是否替换。如果是,pending state将合并replaced state;如果不是则合并当前的state。

从源码中也可以看出setState的第一个参数可以是个对象,也可以是一个函数,通过这个函数的入参可以拿到实例,当前最新的state, props和context,返回的是一个对象


ReactUpdates.asap

这是ReactUpdates的一个重要特性,在asap方法中实现

function asap(callback, context) {
  ...
  asapCallbackQueue.enqueue(callback, context);
  asapEnqueued = true;
}

它用在ReactUpdatesflushBatchedUpdates方法上,如:

if (asapEnqueued) {
  asapEnqueued = false;
  var queue = asapCallbackQueue;
  asapCallbackQueue = CallbackQueue.getPooled();
  queue.notifyAll();
  CallbackQueue.release(queue);
}

这个主要用在input elements上。一般情况调用callback的策略如下:所有的更新(包括嵌套的更新)完成后,Callbacks才被调用。Asap使callback可以在当前的更新完成后立即调用 - 所以如果有嵌套的更新,必须等待asap callbacks完成后才能继续


Wow,设置state真是一个好长好长的流程啊,有没昏昏欲睡的感觉,来个总结吧

  • 调用setState,它把pending state change和callbacks都丢给ReactUpdateQueue处理
  • ReactUpdateQueue更新component的internal instance,把所有更新存放到internal instance的队列变量上,然后交给ReactUpdates
  • ReactUpdates利用batchingStrategy来保证所有state更新在一个事务中执行和刷新
  • flushBatchedUpdates负责同步地原子性地进行更新
  • ReactUpdatesFlushTransaction保证了嵌套的更新被正确地处理
  • runBatchedUpdates的职责是保证更新的顺序,即从父级到子级的顺序进行更新,然后调用ReactReconciler来更新components
  • performUpdateIfNecessary的功能是判断是prop还是state更新,然后调用updateComponent来处理更新
  • updateComponent区分更新的类型,检查shouldComponentUpdate的逻辑以判断是否要阻止虚拟DOM的更新。同时触发了一系列的生命周期方法(shouldComponentUpdatecomponentWillReceivePropscomponentWillUpdatecomponentDidUpdate
  • _processPendingState用于处理pending state并返回当前最新的state对象值。它可以区分是部分设置还是整个替换,并且处理了第一个参数的不同类型入参逻辑(object vs function)
  • 最后介绍了asap callbacks,用于解决输入类型组件更新state的问题 - 它可以让callback在组件更新后立即执行,而不用等到所有嵌套组件完成更新才执行

最后,期待吐槽,期待指教!!!

--EOF--

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

推荐阅读更多精彩内容