原文連接:https://overreacted.io/how-does-setstate-know-what-to-do/ by Dan Abramov
複製代碼
在組件中調用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 }
而且會返回<h1>Thanks</h1>
元素以更新DOM
。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的更新邏輯。api
但若是是那種狀況,this.setState()
是如何能在其餘環境下工做的呢?例如React Native 應用的組件也是繼承自React.Component
。跟上面同樣,他們也是那樣調用this.setState()
,然而React Native使用的是Android和iOS原生視圖,而不是DOM。數組
你可能還熟悉React Test Renderer 或 Shallow Renderer。這兩種測試策略都容許你渲染普通組件而且調用其中的this.setState()
。但它們都不能和DOM一塊兒工做。dom
若是你使用React ART同樣的渲染器,你可能也知道在頁面中使用多個渲染器是可能的。(例如,ART組件在React DOM樹的內部工做。)這使得全局標誌或者變量站不住腳。ide
所以在某種程度上React.Component
將狀態更新的處理委託給特定平臺的程序處理。在咱們瞭解這是怎樣發生的以前,讓咱們深刻到包是如何分離的以及其中的緣由去。
有一個常見的誤解,認爲React「引擎」駐留在React包中。這不是真的。
事實上,自從包在React 0.14中分裂後,react
包試圖只暴露用於定義組件的APIs。大多數React的實現都存在於「renderers」中。
react-dom
,react-dom/server
,react-native
,react-test-renderer
,react-art
是一些渲染器的例子(你也能夠建立你本身的)
這就是react
包無論針對哪一個平臺都有用的緣由。全部導出,例如React.Component
,React.createElement
,React.Children
實例與(最終的)Hooks,都獨立於目標平臺。不管你運行React DOM
、React DOM Server
仍是React Native
,組件都將以相同的方式導入和使用它們。
造成鮮明比較的是,renderer
包公開了特定於平臺的api
,好比ReactDOM.render()
,它容許你將一個React層次結構掛載到DOM節點中。每個renderer
都提供一個相似的API。理想狀態下,大多數組件不必從renderer
中導入任何東西。這使它們更輕便。
大多數人想象每一個renderer
中都存在React「engine」。許多renderers
都包含相同代碼的副本——咱們稱其爲「reconciler」。構建步驟將協調(reconciler)程序代碼與渲染器(renderer)程序代碼合併到一個高度優化的包中,以得到更好的性能。(複製代碼一般對於包尺寸來講不是很好,可是大多數React用戶一次只須要一個渲染器,例如react-dom
.)
這裏的要點是react包只容許你使用react特性,可是不知道它們是如何實現的。renderer
程序包(react-dom
, react-native
等等)提供了React特性和特定平臺的邏輯的實現。其中一些代碼是共享的(「reconciler」),但這是單個renderers
程序的實現細節。
如今咱們知道了爲何須要爲新特性更新react
和react-dom
包。例如,當React 16.3中添加了Context API
後,React.createContext()
就在React包中被暴露出來。
可是React.createContext()
沒有真正實現context特性。例如,在React DOM
和React DOM Server
之間,實現的需求有所不一樣。因此createContext()
僅返回一些普通對象:
// 簡化版
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.Comsumer>
時,是renderer
決定如何處理它們。React DOM
可能以一種方式跟蹤context
的值,但React DOM Server
可能採用不一樣的方式。
所以,若是你更新react
到16.3+,但不更新react-dom
,那麼你將使用一個沒有意識到特定的Provider
和Consumer
類型的renderer
。這就是爲何舊的react-dom
沒法說這些類型是失效的。
一樣的警告能夠運用到React Native
中。然而,不像React DOM
,React
的版本發佈不會當即「強制」React Native
的版本發佈。它們有獨立的版本發佈表。更新後的renderer
程序代碼每隔幾周就會被單獨同步到React
本地存儲庫中一次。這就是爲何React Native
中的特性與React DOM
中的特性在時間表上有所不一樣的緣由。
好了,如今咱們知道了react
包沒有包含任何有趣的東西,而且它的實現都存在與renderers
中像react-dom
,react-native
等等。可是這並無回答咱們的問題。React.Component
中的setState()
是如何和正確的renderer
「交談」的?
**答案是每一個renderer
在建立的類上設置一個特殊的字段。**這個特殊字段被稱做updater
。這不是你要設置的——更確切的說,它是在建立類的實例以後由React DOM
,React DOM Server
或者React Native
設置的:
// React DOM 內部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;
// React DOM Server 內部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;
// React Native 內部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;
複製代碼
看一下setState
在React.Component
中的實現,它所作的只是將工做委託給建立這個組件實例的renderer
:
// 簡化版
setState(partialState, callback) {
// 使用`updater`與渲染器對話
this.updater.enqueueSetState(this, partialState, callback);
}
複製代碼
React DOM Server可能想要忽略狀態更新而且警告你,而React DOM和React Native會讓它們的協調器副原本處理它。
這就是this.setstate()如何更新DOM的,即便它是在React包中定義的。但它讀取的React DOM
設置的this.updater
,而且讓React DOM
進行調度和處理更新。
咱們如今知道類(Class)了,可是鉤子(Hooks)呢?
當人們第一次看到Hooks API的提議時,他們常常會想:useState
是如何知道要作什麼的?覺得它比基於基礎React.Component
類的this.setState()
更加神奇。
可是就像咱們今天所看到的,基礎類的setState()
實現一直都是幻覺。它沒有作任何事情除了將調用轉到當前的renderer
中。useState
也作了一樣的事情。
取代了updater
字段,Hooks使用了「dispatcher」對象。當你調用React.useState()
, React.useEffect()
,或者其它被建立的鉤子時,這些調用會轉向到當前的dispatcher
中。
// 在React中(簡化版)
const React = {
// 真正的屬性被藏得有點深,若是你能找到它的話就看一下
_currentDispatcher: null,
useState(initialState) {
return React.__currentDispatcher.useState(initialState);
},
useEffect(initialState) {
return React.__currentDispatcher.useEffect(initialState);
},
// ...
}
複製代碼
特定的renderer
在你的組件渲染以前就設置了dispatcher:
// React DOM 中
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;
let result;
try {
result = YourComponent(props);
} finally {
// 恢復過來
React.__currentDispatcher = prevDispatcher;
}
複製代碼
例如,React DOM Server這裏的實現,和這裏的被React DOM和React Native共享的調節器的實現。
這就是爲何像react-dom
這樣的renderer
程序須要訪問調用鉤子的同一個react包。不然,你的組件不會「看到」dispatcher!當你在同一個組件樹中有多個React副本時,這可能沒法工做。然而,這老是會致使一些模糊的bug,所以Hooks
迫使你在付出代價以前解決包複製問題。
雖然咱們不鼓勵這樣作,可是對於高級工具用例,你能夠重寫dispatcher
。(我在__currentDispatcher
名稱上撒謊了,可是你能夠在React repo
中找到真正的名稱。)例如,React DevTools
將使用一個特殊的專門構建的dispatcher經過捕獲JavaScript
堆棧跟蹤來反映Hooks樹。別在家裏重複作這件事。
這也意味着鉤子自己並不與react
綁定。若是未來有更多的庫但願重用相同的原語鉤子,理論上,dispatcher
能夠轉移到一個單獨的包中,並做爲一個一流的沒有那麼可怕的名稱的API公開。在實踐中,咱們但願在須要抽象以前避免過早地抽象。
updater
字段和__currentDispatcher
對象都是依賴注入的通用編程原則的形式。在這兩種狀況下,renderer
程序都將setState
等特性的實現「注入」到通用的React包中,以使組件更具聲明性。
當你使用React時你不須要考慮這是怎樣工做的。咱們但願用戶花更多的時間思考他們的應用程序代碼,而不是像依賴注入
這樣的抽象概念。可是若是你曾經想了解 this.setstate()
或useState()
如何知道該作什麼的,我但願這能有所幫助。