當你在組件裏調用 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-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,你均可以用同一種方式導入使用組件。
相比之下,渲染器依賴包暴露特定平臺的APIs,如ReactDOM.render()
,能夠將React組件插入DOM節點中。每一個渲染器都會提供一個相似的API,理想狀況下,大多數 組件 不須要從渲染器導入任何內容,這使它們更靈活。
大多數人認爲React的「引擎」在每一個渲染器中。不過許多渲染器確實包含了同一份副本代碼 —— 咱們稱爲"reconciler"。有個構建步驟將 reconciler 代碼與渲染器代碼融合成一份高度優化過的代碼,以得到更好的性能。(複製代碼一般不利於依賴包大小,但絕大多數用戶一次只須要一個渲染器,例如react-dom
)
這裏要說的是,react
依賴包只讓你知道React有哪些功能,但不知道功能是如何實現的。渲染器依賴包(react-dom
、react-native
等)提供了React功能的實現和平臺特性的邏輯。其中一些代碼是共享的("reconciler"),但更多的是各個渲染器的具體實現。
如今咱們知道爲何有功能時,react
和react-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
,你將使用的渲染器便不知道什麼是Provider
和Consumer
。這也是舊的react-dom
會引起類型無效錯誤的緣由。
React Native一樣有這警告。不過不一樣於 React DOM,一次React更新發布不會「迫使」React Native也當即發佈新版本,它有本身一套發行時間表。更新的渲染器代碼將單獨同步到React Native代碼庫中。因此React Native和React DOM同一個功能,能夠用上的時間是不一樣的。
好了,咱們如今知道react
依賴包不包含任何有趣的內容,由於具體實現放到react-dom
、react-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)