setState是如何知道該怎麼作的?

當你在組件裏調用 setState時,你以爲發生了什麼?html

import React from 'react';
import ReactDOM from 'react-dom';

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { clicked: false };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState({ clicked: true });
  }
  render() {
    if (this.state.clicked) {
      return <h1>Thanks</h1>;
    }
    return (
      <button onClick={this.handleClick}>
        Click me!
      </button>
    );
  }
}

ReactDOM.render(<Button />, document.getElementById('container'));
複製代碼

很明顯,React會隨着新的{ clicked: true} 狀態重渲染組件(component),更新DOM,匹配返回 <h1>Thanks</h1> 元素(element)。react

彷佛很簡單。不過問題來了,是 React 乾的仍是 React DOM 乾的?git

更新DOM聽起來像 React DOM 負責的,但咱們調用 this.setState(),和 React DOM 彷佛沒有關聯,React.Component 這個基類是在React中聲明的。github

那麼React.Component中的setState()是如何更新DOM的?編程

免責聲明:與 多數 其餘 文章 同樣,這篇文章,對React實際使用來講不是必須的,它適合喜歡追尋萬物原理的朋友們,謹慎選擇react-native


咱們可能認爲 React.Component 包含了更新DOM的邏輯。dom

可是若是是這樣的話,this.setState()如何在其餘環境奏效?例如,React Native 的組件也擴展了React.Component,它們就像前面那樣調用this.setState(),且 React Native 使用在Android和iOS原生視圖而不是DOM。ide

你可能也會對 React的 Test Renderer 或 Shallow Renderer 有些印象,這兩種測試方案均可以渲染普通組件並在其中調用this.setState(),但它們和DOM都不要緊。post

若是你用過像React ART這樣的渲染器(renderer),你可能也知道頁面有可能使用多個渲染器(例如,ART組件運行於React DOM樹中),這使得全局標誌或變量再也不可靠。性能

因此,針對不一樣平臺代碼,React.Component以某種委託方式處理state更新。在咱們弄清楚怎麼回事前,先深刻探討下如何及爲何要分離包(packages)。


有一種常見的誤解,即React的「引擎」在 react 依賴包中,這不是真的。

實際上,自從React 0.14拆分依賴包以來,react依賴包特地地只暴露 定義 組件(components)的APIs,React絕大多數 實現 都放在 「渲染器」,

react-domreact-dom/serverreact-nativereact-test-rendererreact-art都是渲染器樣例(你能夠搭建本身的)。

這也是爲何react依賴包無論面向哪一個平臺均可行,它全部的導出,例如React.ComponentReact.createElementReact.Children和最近的Hooks,都獨立於目標平臺,不管你運行 React DOM、React DOM Server或者React Native,你均可以用同一種方式導入使用組件。

相比之下,渲染器依賴包暴露特定平臺的APIs,如ReactDOM.render(),能夠將React組件插入DOM節點中。每一個渲染器都會提供一個相似的API,理想狀況下,大多數 組件 不須要從渲染器導入任何內容,這使它們更靈活。

大多數人認爲React的「引擎」在每一個渲染器中。不過許多渲染器確實包含了同一份副本代碼 —— 咱們稱爲"reconciler"。有個構建步驟將 reconciler 代碼與渲染器代碼融合成一份高度優化過的代碼,以得到更好的性能。(複製代碼一般不利於依賴包大小,但絕大多數用戶一次只須要一個渲染器,例如react-dom)

這裏要說的是,react依賴包只讓你知道React有哪些功能,但不知道功能是如何實現的。渲染器依賴包(react-domreact-native等)提供了React功能的實現和平臺特性的邏輯。其中一些代碼是共享的("reconciler"),但更多的是各個渲染器的具體實現。


如今咱們知道爲何有功能時,reactreact-dom依賴包須要同時更新了,好比說,在React 16.3添加 Context API 時,React依賴包會暴露React.createContext()

React.createContext()實際上並無 實現 context功能,React DOM 與 React DOM Server 的實現是不一樣的。例如,createContext返回一些 plain objects:

// A bit simplified
function createContext(defaultValue) {
  let context = {
    _currentValue: defaultValue,
    Provider: null,
    Consumer: null
  };
  context.Provider = {
    $$typeof: Symbol.for('react.provider'),
    _context: context
  };
  context.Consumer = {
    $$typeof: Symbol.for('react.context'),
    _context: context,
  };
  return context;
}
複製代碼

當你在代碼裏使用<MyContext.Provider>或者<MyContext.Consumer>時,渲染器 決定如何處理它們。React DOM可能以一種方式跟蹤context,而React DOM Server可能會採用另外一種方式。

若是你更新react到16.3+而沒更新react-dom,你將使用的渲染器便不知道什麼是ProviderConsumer這也是舊的react-dom會引起類型無效錯誤的緣由

React Native一樣有這警告。不過不一樣於 React DOM,一次React更新發布不會「迫使」React Native也當即發佈新版本,它有本身一套發行時間表。更新的渲染器代碼將單獨同步到React Native代碼庫中。因此React Native和React DOM同一個功能,能夠用上的時間是不一樣的。


好了,咱們如今知道react依賴包不包含任何有趣的內容,由於具體實現放到react-domreact-native等渲染器中了。可是這沒能解決咱們的問題,React.Component中的setState()是如何與對應的渲染器「交流的」。

答案是每一個渲染器在建立的class上設置一個特殊字段。這個字段叫作updater。這不是由你設置的,而是React DOM、React DOM Server、React Native在你實例class後給你加上的:

// Inside React DOM
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;

// Inside React DOM Server
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;

// Inside React Native
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;
複製代碼

查看React.Component中的setState實現,它所作的就是將任務所有委託給實例此組件的渲染器:

// 簡化後的代碼
setState(partialState, callback) {
  // 用`updater` 反饋給渲染器
  this.updater.enqueueSetState(this, partialState, callback);
}
複製代碼

React DOM Server 也許打算 忽略state更新並警告你,而React DOM和React Native會用複製來的"reconciler"去處理它

這也是爲何即便this.setState()定義在React依賴包中,依然能夠更新DOM。它會獲取由React DOM設置的this.updater,並讓React DOM調度和處理更新。


咱們如今知道class了,那Hooks是怎麼作的?

當你們第一次看到Hooks API,極可能會想:useState怎麼「知道該怎麼作」?猜測是它的this.setState()比基於React.Component的更「神奇」。

但正如咱們今天看到的,基於class的setState()實現一直是一種錯覺,除了調用指向當前的渲染器以外,它不參與任何操做。useState Hook也一樣如此

Hooks使用dispatcher對象而不是updater字段。在你調用 React.useState()React.useEffect()、或者其餘內置Hook時,這些都會轉發給當前的dispatcher。

// In React (簡化)
const React = {
  // 真正的屬性隱藏得有點深,你能夠嘗試去找找看!
  __currentDispatcher: null,

  useState(initialState) {
    return React.__currentDispatcher.useState(initialState);
  },

  useEffect(initialState) {
    return React.__currentDispatcher.useEffect(initialState);
  },
  // ...
};
複製代碼

而每種渲染器在組件渲染以前會設置dispatcher:

// In React DOM
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;
let result;
try {
  result = YourComponent(props);
} finally {
  // Restore it back
  React.__currentDispatcher = prevDispatcher;
}
複製代碼

例如,React DOM Server的實如今這兒,React DOM和React Native共享的 reconciler 實如今這兒

這就是像react-dom這樣的渲染器須要獲取同一個react依賴包的緣由,不然,你的組件不會「看到」這個dispatcher!若是在同一棵組件樹中存在多個React副本,就有可能發生問題。不過這樣容易出現隱蔽bug,因此Hooks會強迫你在發生前就解決依賴包重複問題。

雖然咱們不鼓勵這樣作,但爲了更適用於某些情景,你能夠在技術上自行覆蓋dispatcher(__currentDispatcher是我編造的,不過你能夠在代碼庫中找到真實的名稱),例如,React DevTools會用一個專門定製的dispatcher經過捕獲JavaScript堆棧軌跡來描繪反饋Hooks樹。不要在家重複這樣作了

這也意味着Hooks自己並不依賴於React。若是未來有更多的類庫想複用React裏的Hooks理念,理論上dispatcher能夠挪過去用而且做爲一個更少「可怕」名稱的一流API展示出來。在開發過程當中,咱們應該避免過早抽象概念,直到咱們不得不這麼作了。

updater字段和__currentDispatcher對象都造成於一個叫 依賴注入 的通用編程原理。這兩種狀況裏,渲染器將諸如setState之類的功能實現「注入」到通用的React依賴包中,組件所以以聲明爲主。

在使用React時,你不須要思考這些是怎麼跑起來的。咱們但願React開發者花更多的時間在應用程序代碼上,而不是像依賴注入這些抽象概念上。但若是你想知道this.setState()或者useState是如何知道怎麼作的,我但願這會有所幫助。


翻譯原文How Does setState Know What to Do?(2018-12-09)

相關文章
相關標籤/搜索