本文翻譯自:How Does setState Know What to Do?原作者:Dan Abramov
如果有任何版權問題,請聯繫[email protected]
當你在組件中調用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}
這條狀態重新渲染組件並且更新匹配到的DOM,然後返回<h1>Thanks</h1>
元素。
聽起來似乎簡潔明瞭。但別急,React(或者說React DOM)是怎麼做的?
更新DOM聽起來像是React DOM的事兒,但別忘了我們調用的可是this.setState()
,它是React的東西,可不是React DOM的。另外,我們的基類React.Component
是被定義在React內部。
所以問題來了:React.Component
內部的setState
怎麼能去更新DOM呢?
事先聲明:就像我的其他博客,你不需要熟練掌握React。這篇博客是爲那些想要看看面紗之後是什麼東西的人準備的。完全可選!
我們或許會認爲React.Component
類已經包含了DOM更新邏輯。
但如果這是事實,那this.setState
是如何工作在其他環境中呢?比如:在React Native App中的組件也能繼承React.Component
,他們也能像上面一樣調用this.setState()
,並且React Native工作在Android和iOS的原生視圖而不是DOM中。
你可能也對React Test Renderer 或 Shallow Renderer比較熟悉。這兩個測試渲染器讓你可以渲染一般的組件並且也能在他們中調用this.setState
,但他們可都不使用DOM。
如果你之前使用過一些渲染器比如說React ART,你可能知道在頁面中使用超過一個渲染器是沒什麼問題的。(比如:ART組件工作在React DOM 樹的內部。)這會產生一個不可維持的全局標誌或變量。
所以React.Component
以某種方式將state的更新委託爲具體的平臺(譯者注:比如Android, iOS),在我們理解這是如何發生之前,讓我們對包是如何被分離和其原因挖得更深一點吧!
這有一個常見的錯誤理解:React "引擎"在react
包的內部。這不是事實。
事實上,從 React 0.14開始對包進行分割時,React
包就有意地僅導出關於如何定義組件的API了。React的大部分實現其實在“渲染器”中。
渲染器的其中一些例子包括:react-dom
,react-dom/server
,react-native
,react-test-renderer
,react-art
(另外,你也可以構建自己的)。
這就是爲什麼react
包幫助很大而不管作用在什麼平臺上。所有它導出的模塊,比如React.Component
,React.createElement
,React.Children
和[Hooks](https://reactjs.org/docs/hooks-intro.html)
,都是平臺無關的。無論你的代碼運行在React DOM、React DOM Server、還是React Native,你的組件都可以以一種相同的方式導入並且使用它們。
與之相對的是,渲染器會暴露出平臺相關的接口,比如ReactDOM.render()
,它會讓你可以把React掛載在DOM節點中。每個渲染器都提供像這樣的接口,但理想情況是:大多數組件都不需要從渲染器中導入任何東西。這能使它們更精簡。
大多數人都認爲React“引擎”是位於每個獨立的渲染器中的。許多渲染器都包含一份相同的代碼—我們叫它“調節器”,爲了表現的更好,遵循這個步驟 可以讓調節器的代碼和渲染器的代碼在打包時歸到一處。(拷貝代碼通常不是優化“打包後文件”(bundle)體積的好辦法,但大多數React的使用者一次只需要一個渲染器,比如:react-dom
(譯者注:因此可以忽略調節器的存在))
The takeaway here 是react
包僅僅讓你知道如何使用React的特性而無需瞭解他們是如何被實現的。渲染器(react-dom,react-native
等等)會提供React特性的實現和平臺相關的邏輯;一些關於調節器的代碼被分享出來了,但那只是單獨渲染器的實現細節而已。
現在我們知道了爲什麼react
和react-dom
包需要爲新特定更新代碼了。比如:當React16.3新增了Context接口時,React.createContext()
方法會在React包中被暴露出來。
但是React.createContext()
實際上不會實現具體的邏輯(譯者注:只定義接口,由其他渲染器來實現邏輯)。並且,在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.Consumer
>,那裏就是決定着如何處理他們的渲染器。React DOM會用A方法追蹤context值,但React DOM Server或許會用另一個不同的方法實現。
所以如果你將react
升級到16.3+,但沒有升級react-dom,你將使用一個還不知道Provider
和Consumer
類型的渲染器,這也就舊版的react-dom
可能會報錯:fail saying these types are invalid的原因。
同樣的警告也會出現在React Native中,但是不同於React DOM,一個新的React版本不會立即產生一個對應的React Native版本。他們(React Native)有自己的發佈時間表。大概幾周後,渲染器代碼纔會單獨更新到React Native庫中。這就是爲什麼新特性在React Native生效的時間會和React DOM不同。
Okay,那麼現在我們知道了react
包不包含任何好玩的東西,並且具體的實現都在像react-dom
,react-native
這樣的渲染器中。但這並不能回答我們開頭提出的問題。React.Component
裏的setState()
是如何和對應的渲染器通信的呢?
答案是每個渲染器都會在創建的類中添加一個特殊的東西,這個東西叫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
的實現就可以看出,它做的所有的工作就是把任務委託給在這個組件實例中創建的渲染器:
// 簡單例子
setState(partialState, callback) {
// 使用`updater`去和渲染器通信
this.updater.enqueueSetState(this, partialState, callback);
}
React DOM Server 可能想忽略狀態更新並且警告你,然而React DOM和React Native將會讓調節器的拷貝部分去 處理它。
這就是儘管this.setState()
被定義在React包中也可以更新DOM的原因。它調用被React DOM添加的this.updater
並且讓React DOM來處理更新。
現在我們都比較瞭解“類”了,但“鉤子”(Hooks)呢?
當人們第一次看到 鉤子接口的提案時,他們常回想:useState
是怎麼知道該做什麼呢?這一假設簡直比對this.setState()
的疑問還要迷人。
但就像我們如今看到的那樣,setState()
的實現一直以來都是模糊不清的。它除了傳遞調用給當前的渲染器外什麼都不做。所以,useState
鉤子做的事也是如此。
這次不是updater
,鉤子(Hooks)使用一個叫做“分配器”(dispatcher)的對象,當你調用React.useState()
、React.useEffect()
或者其他自帶的鉤子時,這些調用會被推送給當前的分配器。
// 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);
},
// ...
};
單獨的渲染器會在渲染你的組件之前設置分配器(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共享的調節器實現在這裏。
這就是爲什麼像react-dom
這樣的渲染器需要訪問和你調用的鉤子所使用的react
一樣的包。否則你的組件將找不到分配器!如果你有多個React的拷貝在相同的組件樹中,代碼可能不會正常工作。然而,這總是造成複雜的Bug,因此鉤子會在它耗光你的精力前強制你去解決包的副本問題。
如果你不覺得這有什麼,你可以在工具使用它們前精巧地覆蓋掉原先的分配器(__currentDispatcher
的名字其實我自己編的但你可以在React倉庫中找到它真正的名字)。比如:React DevTools會使用一個特殊的內建分配器來通過捕獲JavaScript調用棧來反映(introspect)鉤子。不要在家裏重複這個(Don’t repeat this at home.)(譯者注:可能是“不要在家裏模仿某項實驗”的衍生體。可能是個笑話,但我get到)
這也意味着鉤子不是React固有的東西。如果在將來有很多類庫想要重用相同的基礎鉤子,理論上來說分配器可能會被移到分離的包中並且被塑造成優秀的接口—會有更少讓人望而生畏的名稱—暴露出來。在實際中,我們更偏向去避免過於倉促地將某物抽象,直到我們的確需要這麼做。
updater
和__currentDispatcher
都是泛型程序設計(依賴注入/dependency injection)的絕佳實例。渲染器“注入”特性的實現。就像setState
可以讓你的組件看起來簡單明瞭。
當你使用React時,你不需要考慮它是如何工作的。我們期望React用戶去花費更多的時間去考慮它們的應用代碼而不是一些抽象的概念比如:依賴注入。但如果你曾好奇this.setState()
或useState()
是怎麼知道它們該做什麼的,那我希望這篇文章將幫助到你。