翻译 :深入React代码:处理状态变化

最近学习React setState的机制一直不甚理解 部门大佬推荐的文章
尝试翻译一下 权当学习 有翻译的不足的地方欢迎指出

原文链接点这里

深入React代码:处理状态变化

这里写图片描述
状态.React命名中最复杂的概念之一。然而我们中的一些人在我们的项目中已经通过外化状态摆脱了它(Redux了解一下?),但它任旧是在react.js中被广泛使用的特性。

在带来方便的同时,它可能会导致一些问题。Robert PankoweckiRails meets React.js的作者之一曾经在他开始使用React的时候在输入验证上遇到了问题。

故事是这样的:验证输入似乎是很简单的。但是这里有一个vanilla form 的问题-即当用户第一次看到input输入框的时候,这个输入框的值是不应该被验证的,即使这个输入框的值是空的而且这是不符合规则的。这绝对是一个有状态的行为,所以Robert 认为将他的验证信息放在state中是合理的。

所以基本上他实现了这样的逻辑

changeTitle: function changeTitle (event) {
  this.setState({ title: event.target.value });
  this.validateTitle();
},
validateTitle: function validateTitle () {
  if(this.title.length === 0) {
    this.setState({ titleError: "Title can't be blank" });
  }
},

爆炸,这段代码没有按照预期运行起来。为什么?这是因为setState是一个异步的方法。这意味着当你调用setState,在this.state中的变量不会立即被改变

setState() does not immediately mutate this.state but creates 
a pending state transition. 
Accessing this.state after calling this method can potentially 
return the existing value.

所以 ,与其说这是一个相当容易误解的问题不如说是常识的缺乏。Robert可以通过阅读文档来回避这个问题。但是对于初学者来说,这是一个很容易犯的错误。

这种情况在团队内部引发了一些相当有趣的讨论。你想怎么样setState。你预期的执行结果是什么?你如何在你改变输入并且提交的时候保证状态会被更新。为了了解其内部的机制以及到底发生了什么在React内部。我决定去跟踪一下当你setState的时候引擎内部发什么什么。但是首先,我们首先,让我们来用适当的方式来解决Robert的问题。

解决验证问题

让我们来看看在React 0.14.7中你有什么方法来解决以上的问题

  • 你可以通过给setState增加一个回调方法来解决这个问题。setState接受两个参数,第一个是state的变化,第二个是一个在state被更新的时候调用的callback函数。
  changeTitle: function changeTitle (event) {
    this.setState({ title: event.target.value }, function afterTitleChange () {
      this.validateTitle();
    });
  },

在afterTitleChange中,state已经被正确的更新而且你可以读取到预期的值

  • 你也可以合并状态的变化。这样做你必须将validateTitle参数化。以此将两个state变化合并为一个。
  changeTitle: function changeTitle (event) {
    let nextState = Object.assign({},
                                  this.state,
                                  { title: event.target.value });
    this.validateTitle(nextState);
    this.setState(nextState);
  },
  validateTitle: function validateTitle (state) {
    if(state.title.length === 0) {
      state.titleError = "Title can't be blank";
    }
  }

所以问题到这里就消失了,因为再也没有两个状态的改变了,但是当你的state比较大而且嵌套层级较多的时候这就有点复杂了(这绝不是一个很好的实践)。但对于改造validateTitle使其成为一个纯函数来说这又是一个很好的实践。这个函数的返回值只依赖于参数而且你可以得到预期的结果(在运行中不会有突变)。

  changeTitle: function changeTitle (event) {
    let nextState = Object.assign({},
                                  this.state,
                                  { title: event.target.value });
    this.setState(this.withTitleValidated(nextState));
  },
  withTitleValidated: function validateTitle (state) {
    let validation = {};
    if(state.title.length === 0) {
      validation.titleError = "Title can't be blank";
    }
    return Object.assign({}, state, validation);
  }

这是另一种性能稍微得到优化的解决方案。得益于shouldcomponentupdate的配置。如果一个shouldComponentUpdate是被定制的,一些state更新将不会触发componentDidUpdate,因此 validateTitle也不会被调用。

  • 你可以外化状态,将验证放在应用的其他部分(比如Redux 的reducer)。这样可以彻底的避免这种问题。

    在一本书中,Robert用了第二种解决方案。这段经历让他确信保持state在React组件外来避免这些麻烦。但是这真的是正确的对待state的方法么,让我想想、、、、

    如果你觉得接下来的想跳过,你可以直接去扼要重述区域(最底下)

跳进React的兔子洞

让我们从setState的定义开始,他被定义在react组件的原型上,而这个原型是基于React.Component 类继承自 ES2015-class definition of React components
(这块还有点不明白)setState在被React.createClass创建的组件中也是可用的并且代码也是一致的

var ReactClassComponent = function() {};
assign(
  ReactClassComponent.prototype,
  ReactComponent.prototype,
  ReactClassMixin
);

如你所见,ReactComponent的原型在这里,这意味着setState从那里就开始被使用了(除非ReactClassMixin不做一下奇怪的事情,他确实不) 让我们看一下实现

ReactComponent.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.'
  );
  if (__DEV__) {
    warning(
      partialState != null,
      'setState(...): You passed an undefined or null state object; ' +
      'instead, use forceUpdate().'
    );
  }
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback);
  }
};

在完成检查不变性以及抛出警告后 这里做了两件事

  • steState被入队到updater中
  • callback 如果存在就被入队到回调updater中

    但什么是updater,顾名思义他将会更新组件,让我们看下它在ReactClass 和 ReactComponent中的定义

  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;

react.js代码严重依赖依赖注入原理This allows to substitute parts of React.js based on the environment (server-side vs. client-side, different platforms) in which you’re rendering.
reactcomponent是同构命名空间的一部分,而且将一直存在,不论是在哪一个端(本地或者服务器端),他只包含原始的每一台设备都能运行的ES5标准的js.

所以真正的updater在哪里被注入?In ReactCompositeComponent part of the renderer (mountComponent method):

    // These should be set up in the constructor, but as a convenience for
    // simpler class abstractions, we set them up after the fact.
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = ReactUpdateQueue;

ReactCompositeComponent 类被应在许多类型的reacr库中(react-dom, react-native, react-art)去建立一个独立的每一个组件都有的依赖基础。
It is used as a precondition of a Transaction, for example in ReactMount of the react-dom client - so the platform-dependent code goes there and is wrapped with a transaction which sets platform-independent internals correctly.

Since you know what is an updater, let’s see how enqueueSetState and enqueueCallback are implemented.(这前面一段都看的很迷)

入队的状态变化和回调 - React更新队列

在setState 中 enqueueSetState与 enqueueCallback均被调用了,ReactUpdateQueue实例被用于实现这些方法。
enqueueSetState:

  enqueueSetState: function(publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'setState'
    );

    if (!internalInstance) {
      return;
    }

    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  },

enqueueCallback:

  enqueueCallback: function(publicInstance, callback) {
    invariant(
      typeof callback === 'function',
      'enqueueCallback(...): You called `setProps`, `replaceProps`, ' +
      '`setState`, `replaceState`, or `forceUpdate` with a callback that ' +
      'isn\'t callable.'
    );
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);

    // Previously we would throw an error if we didn't have an internal
    // instance. Since we want to make it a no-op instead, we mirror the same
    // behavior we have in other enqueue* methods.
    // We also need to ignore callbacks in componentWillMount. See
    // enqueueUpdates.
    if (!internalInstance) {
      return null;
    }

    if (internalInstance._pendingCallbacks) {
      internalInstance._pendingCallbacks.push(callback);
    } else {
      internalInstance._pendingCallbacks = [callback];
    }
    // TODO: The callback here is ignored when setState is called from
    // componentWillMount. Either fix it or disallow doing so completely in
    // favor of getInitialState. Alternatively, we can disallow
    // componentWillMount during server-side rendering.
    enqueueUpdate(internalInstance);
  },

As you can see, both methods reference the enqueueUpdate function which will get inspected soon. The pattern goes like this:

The internal instance is retrieved. What you see as a React Component in your code has a backing instance inside React which has fields which are a part of the private interface. Those internal instances are obtained by a piece of code called ReactInstanceMap to which getInternalInstanceReadyForUpdate is delegating this task.
A change to the internal instance is made. In case of enqueuing callback, callback is added to a pending callbacks queue. In case of enqueuing a state change, pending state change is added to a pending state queue.
enqueueUpdate is called to flush changes made by those methods. Let’s see how it is done.
enqueueUpdate:

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

Oh, so yet another piece of this puzzle! It’s interesting why on this level ReactUpdates is referenced directly and not injected, though. It is because ReactUpdates is quite generic and it’s dependencies are injected instead. Let’s see how ReactUpdates works, then!

执行更新

Let’s see how enqueueUpdate is implemented on the ReactUpdates side:

function enqueueUpdate(component) {
  ensureInjected();

  // Various parts of our code (such as ReactCompositeComponent's
  // _renderValidatedComponent) assume that calls to render aren't nested;
  // verify that that's the case. (This is called by each top-level update
  // function, like setProps, setState, forceUpdate, etc.; creation and
  // destruction of top-level components is guarded in ReactMount.)

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
}

There are two moving parts (injections) on the ReactUpdates level which are important to mention. ensureInjected sheds some light on them:

function ensureInjected() {
  invariant(
    ReactUpdates.ReactReconcileTransaction && batchingStrategy,
    'ReactUpdates: must inject a reconcile transaction class and batching ' +
    'strategy'
  );
}

batchingStrategy is a strategy of how React will batch your updates. For now there is only one, called ReactDefaultBatchingStrategy which is used in the codebase. ReactReconcileTransaction is environment-dependent piece of code which is responsible for “fixing” transient state after updates - for DOM it is fixing selected pieces of text which can be lost after update, suppressing events during reconciliation and queueing lifecycle methods. More about it here.
Code of enqueueUpdate is a little hard to read. On the first look it seems that there is nothing special happening here. batchingStrategy which is a Transaction has a field which tells you whether a transaction is in progress. If it’s not, enqueueUpdate stops and registers itself to be performed in transaction. Then, a component is added to the list of dirty compon
FLUSH_BATCHED_UPDATES - which is calling flushBatchedUpdates from ReactUpdates after performing a function in a transaction. This is the heart of the state updating code. IMO it’s confusing that important piece of code is hidden within transaction’s implementation - I believe it is made for code reuse.
RESET_BATCHED_UPDATES - responsible for clearing the isBatchingUpdates flag after function is performed within transaction.
While RESET_BATCHED_UPDATES is more of a detail, flushBatchedUpdates is extremely important - it is where the logic of updating state really happens. Let’s see the implementation:

var flushBatchedUpdates = function() {
  // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
  // array and perform any updates enqueued by mount-ready handlers (i.e.,
  // componentDidUpdate) but we need to check here too in order to catch
  // updates enqueued by setState callbacks and asap calls.
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

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

There is yet another transaction (ReactUpdatesFlushTransaction) which is responsible for “catching” any pending updates that appeared after running flushBatchedUpdates. It is a complication because componentDidUpdate or callbacks to setState can enqueue next updates which needs to be processed. This transaction is additionaly pooled (there are instances prepared instead of creating them on the fly - React uses this trick to avoid unnecessary garbage collecting) which is a neat trick which came from video games development. There is also a concept of asap updates which will be described a little bit later.

As you can see, there is a method called runBatchedUpdates called. Whew! This is a lot of methods called from setState to an end. And it’s not over. Let’s see:
后面就有点难懂了 暂时推荐看原文

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值