[譯]react的setState如何知道該做什麼 --Dan Abramov

xiaohesong發表於2018-12-09

原文: How Does setState Know What to Do?

原譯文: react的setState如何知道他要做什麼

譯:可能看到標題的時候會想,怎麼去做還不是看程式碼嗎?react中的setState不就是負責更新狀態碼?於是就抱著好奇心看下去了。

當你在元件中呼叫setState的時候,你認為讓發生了什麼?

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}狀態的時候re-render元件並且更新DOM去返回<h1>Thanks</h1>元素。

看起來很簡單,對吧?等一下,請考慮下,這個是react在處理?或者是ReactDOM在處理?

更新DOM聽起來似乎是React DOM在負責處理。但是我們呼叫的是this.setState,這個api是來自react,並非是React DOM。並且我們的React.Component是定義在React裡的。

所以React.Component.prototype.setState()是如何去更新DOM的。

事先宣告: 就像本部落格大多數其他文章一樣,你其實可以完全不用知道這些內容,一樣可以很好的使用react。這系列的文章是針對於那些好奇react內部原理的一些人。所以讀不讀,完全取決於你。


我們可能會認為React.Component裡包含了DOM的一些更新邏輯。

但是如果是我們猜想的這樣,那麼this.setState()如何在其他環境中正常工作?比如在React Native中的元件也是繼承於React.Component, React Native應用像上面一樣呼叫this.setState(),但React Native可以使用Android和iOS原生的檢視而不是DOM。

再比如,你可能也熟悉React Test Renderer或Shallow Renderer。這些都可以讓你渲染普通元件並在其中呼叫this.setState()。但是他們都不適用於DOM

如果你使用過像React ART這樣的渲染器,你便會知道可以在頁面上使用多個渲染器。(例如,ART 元件在React DOM中工作)。這使得全域性標誌或變數無法工作。

所以**React.Component以某種方式委託處理狀態更新到特定的平臺。** 在我們理解這是如何發生之前,讓我們深入瞭解包的分離方式和原因。


有一種常見的誤區就是React“引擎(engine)”存在React包中。這其實是不對的。

事實上,自從React 0.14拆分包以來,react包只是暴露了用於定義元件的API。React的大多數實現都在“渲染器(renderers)”中。

react-domreact-dom / serverreact-nativereact-test-rendererreact-art是在renderers中的一些例子(你也可以建立屬於你自己的)。

所以,無論在什麼平臺,react包都可以正常工作。他對外暴露的所有的內容,例如: React.ComponentReact.createElementReact.Children的作用和Hook,都獨立於目標平臺。無論執行React DOMReact DOM Server還是React Native,元件都將以相同的方式匯入和使用它們。

相比之下,renderer包對外暴露了特定於平臺的api,像ReactDOM.render就可以讓你把React的層次結構掛載到DOM節點。每個renderer都提供了像這樣的API。理想情況下,大多陣列件不需要從renderer匯入任何內容。這使它們更輕便易用。

大多數人都認為react的"引擎(engine)"在每個renderer中。 許多renderer都包含相同程式碼的副本 - 我們將其稱為“reconciler(和解)”。構建步驟將reconciler(和解)的程式碼與renderer(渲染器)程式碼一起成為一個高度優化的捆綁包,以獲得更好的效能。(複製程式碼對於包大小通常不是很好,但絕大多數React使用者一次只需要一個渲染器,例如react-dom。)

這裡要說的是,react包只允許你使用React功能,但不知道它們是如何實現的。renderer包(react-domreact-native等)提供了React功能和特定於平臺的邏輯的實現。其中一些程式碼是共享的(“reconciler”),但這是各個渲染器的實現細節。


現在我們知道了為什麼每次有新的功能都會同時更新reactreact-dom包。例如,當React 16.3新增了Context API時,React.createContext()在React包上對外暴露。

但是React.createContext實際上並沒有實現上下文功能。例如,React DOMReact DOM Server之間的實現需要有所不同。所以createContext()返回一些普通物件:

// 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>時,這就由renderer去決定如何處理他們。React DOM可能以一種方式跟蹤上下文值,但React DOM Server可能會採用不同的方式。

所以如果你更新react到16.3+,但是沒有更新react dom, 那麼你使用的這個renderer將是一個無法解析ProviderConsumer型別的renderer 這就是為什麼舊的react-dom失敗報錯這些型別無效

同樣的警告也適用於React Native。但是,與React DOM不同,React的版本更新不會迫使React Native的版本去立即更新。他有一個自己的釋出週期。幾周後,更新過的renderer會單獨同步到React Native庫中。這就是為什麼React Native和React DOM可用功能的時間不一致的區別


好吧,現在我們知道了React包中不包含我們感興趣的內容,而且這些實現是存在於像react-dom, react-native這樣的renderer中。但是這些並不能回答我們的問題 -- React.Component中的setState是如何知道他要幹什麼的(與對應的renderer協同工作)。

答案是在每個建立renderer的類上設定一個特殊的欄位。 這個欄位就叫做updater。這不是你想要設定啥就設定啥,你不可以設定他,而是要在類的例項被建立後再去設定React DOMReact DOM ServerReact Native:

// 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 DOM Server 可能想要忽略狀態的更新並且給你一個警告,然而React DOM和React Native會拷貝一份reconciler(和解)程式碼去處理他

這就是為什麼this.setState定義在React的包中仍然可以更新DOM的原因。他通過讀取this.updater去獲取,如果是React DOM, 就讓React DOM排程並處理更新。


我們現在知道了類的操作方式,那麼hooks呢?

當大多數的人看到Hooks提案的API時,他們常常想知道: useState是怎麼'知道該去做什麼’?假設他會比this.setState更加神奇。

但是正如我們現在所看到的這個樣子,對於理解setState的實現一直是一種錯覺。他除了會將呼叫作用到對應的renderer之外不會再做其他任何的操作。實際上useState這個Hook做了同樣的事情

相對於setStateupdater欄位而言,Hooks使用dispatcher物件。 當呼叫React.useStateReact.useEffect或其他內建的Hook時,這些呼叫將轉發到當前排程程式(dispatcher)。

// In React (simplified a bit)
const React = {
  // Real property is hidden a bit deeper, see if you can find it!
  __currentDispatcher: null,

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

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

並且在渲染你的元件之前,各個renderer會設定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 DOMReact Native共享的reconciler實現就在這裡

這就是為什麼像react-dom這樣的renderer需要訪問你呼叫Hooks的同一個React包的原因。否則,你的元件將不會知道dispatcher!當在同一元件樹中有多個React副本時,這可能不會如期工作。但是,這會導致一些那難以理解的錯誤,所以Hook會迫使解決包重複問題。

雖然我們不鼓勵你這樣做,但是對於高階工具用例,你可以在此技術上重寫dispatcher。(我對__currentDispatcher這名稱撒了謊,這個不是真正的名字,但你可以在React庫中找到真正的名字。)例如,React DevTools將使用特殊的專用dispatcher程式通過捕獲JavaScript堆疊跟蹤來反思Hooks樹。不要自己在家裡重複這個。

這也意味著Hooks本身並不依賴於React。如果將來有更多的庫想要重用這些原始的Hook,理論上dispatcher可以移動到一個單獨的包中,並作為一個普通名稱的API對外暴露。在實踐中,我們寧願避免過早抽象,直到需要它的時候再說。

updater欄位和__currentDispatcher物件都是一種稱為依賴注入的程式設計原則的形式。在這些情況下,renderer注入一些比如setState這樣的功能到React的包中,這樣來保持元件更具有宣告性。

在使用react的時候,你不需要去考慮他的工作原理。我們希望React使用者花更多時間考慮他們的程式碼而不是像依賴注入這樣的抽象概念。但是如果你想知道this.setStateuseState如何知道該怎麼做,我希望本文會對你有所幫助。

相關文章