「譯」setState如何知道它該做什麼?
本文翻譯自: 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’));
<p>當然,React會用<code>{ clicked: true}</code> 這條狀態重新渲染元件並且更新匹配到的DOM,然後返回<code><h1>Thanks</h1></code>元素。</p> <p>聽起來似乎簡潔明瞭。但別急,React(或者說React DOM)是怎麼做的?</p> <p>更新DOM聽起來像是React DOM的事兒,但別忘了我們呼叫的可是<code>this.setState()</code>,它是React的東西,可不是React DOM的。另外,我們的基類<code>React.Component</code>是被定義在React內部。</p> <p>所以問題來了:<code>React.Component</code>內部的<code>setState</code>怎麼能去更新DOM呢?</p> <p><strong>事先宣告:就像我的<a href="https://overreacted.io/how-does-react-tell-a-class-from-a-function/" rel="nofollow noreferrer">其他部落格</a>,你不需要熟練掌握React。這篇部落格是為那些想要看看面紗之後是什麼東西的人準備的。完全可選!</strong></p> <hr> <p>我們或許會認為<code>React.Component</code>類已經包含了DOM更新邏輯。</p> <p>但如果這是事實,那<code>this.setState</code>是如何工作在其他環境中呢?比如:在React Native App中的元件也能繼承<code>React.Component</code>,他們也能像上面一樣呼叫<code>this.setState()</code>,並且React Native工作在Android和iOS的原生檢視而不是DOM中。</p> <p>你可能也對React Test Renderer 或 Shallow Renderer比較熟悉。這兩個測試渲染器讓你可以渲染一般的元件並且也能在他們中呼叫<code>this.setState</code>,但他們可都不使用DOM。</p> <p>如果你之前使用過一些渲染器比如說<a href="https://github.com/facebook/react/tree/master/packages/react-art" rel="nofollow noreferrer">React ART</a>,你可能知道在頁面中使用超過一個渲染器是沒什麼問題的。(比如:ART元件工作在React DOM 樹的內部。)這會產生一個不可維持的全域性標誌或變數。</p> <p><strong>所以<code>React.Component</code>以某種方式將state的更新委託為具體的平臺(譯者注:比如Android, iOS)</strong>,在我們理解這是如何發生之前,讓我們對包是如何被分離和其原因挖得更深一點吧!</p> <hr> <p>這有一個常見的錯誤理解:React "引擎"在<code>react</code>包的內部。這不是事實。</p> <p>事實上,從 <a href="https://reactjs.org/blog/2015/07/03/react-v0.14-beta-1.html" rel="nofollow noreferrer">React 0.14</a>開始對包進行分割時,<code>React</code>包就有意地僅匯出關於如何定義元件的API了。React的大部分實現其實在“渲染器”中。</p> <p>渲染器的其中一些例子包括:<code>react-dom</code>,<code>react-dom/server</code>,<code>react-native</code>,<code>react-test-renderer</code>,<code>react-art</code>(另外,你也可以<a href="https://github.com/facebook/react/blob/master/packages/react-reconciler/README.md" rel="nofollow noreferrer">構建自己的</a>)。</p> <p>這就是為什麼<code>react</code>包幫助很大而不管作用在什麼平臺上。所有它匯出的模組,比如<code>React.Component</code>,<code>React.createElement</code>,<code>React.Children</code>和<code>[Hooks](https://reactjs.org/docs/hooks-intro.html)</code>,都是平臺無關的。無論你的程式碼執行在React DOM、React DOM Server、還是React Native,你的元件都可以以一種相同的方式匯入並且使用它們。</p> <p>與之相對的是,渲染器會暴露出平臺相關的介面,比如<code>ReactDOM.render()</code>,它會讓你可以把React掛載在DOM節點中。每個渲染器都提供像這樣的介面,但理想情況是:大多陣列件都不需要從渲染器中匯入任何東西。這能使它們更精簡。</p> <p><strong>大多數人都認為React“引擎”是位於每個獨立的渲染器中的</strong>。許多渲染器都包含一份相同的程式碼—我們叫它<a href="https://github.com/facebook/react/tree/master/packages/react-reconciler" rel="nofollow noreferrer">“調節器”</a>,為了表現的更好,遵循<a href="https://reactjs.org/blog/2017/12/15/improving-the-repository-infrastructure.html" rel="nofollow noreferrer">這個步驟</a> 可以讓調節器的程式碼和渲染器的程式碼在打包時歸到一處。(拷貝程式碼通常不是優化“打包後文件”(bundle)體積的好辦法,但大多數React的使用者一次只需要一個渲染器,比如:<code>react-dom</code>(譯者注:因此可以忽略調節器的存在))</p> <p>The takeaway here 是<code>react</code>包僅僅讓你知道如何使用React的特性而無需瞭解他們是如何被實現的。渲染器(<code>react-dom,react-native</code>等等)會提供React特性的實現和平臺相關的邏輯;一些關於調節器的程式碼被分享出來了,但那只是單獨渲染器的實現細節而已。</p> <hr> <p>現在我們知道了為什麼<code>react</code>和<code>react-dom</code>包需要為新特定更新程式碼了。比如:當React16.3新增了Context介面時,<code>React.createContext()</code>方法會在React包中被暴露出來。</p> <p>但是<code>React.createContext()</code>實際上不會實現具體的邏輯(譯者注:只定義介面,由其他渲染器來實現邏輯)。並且,在React DOM和React DOM Server上實現的邏輯也會有區別。所以<code>createContext()</code>會返回一些純粹的物件(定義如何實現):</p> <pre><code class="react">// 一個簡單例子 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)有自己的釋出時間表。大概幾周後,渲染器程式碼才會
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);
},
// …
};
<p>單獨的渲染器會在渲染你的元件之前設定分配器(dispatcher)。</p>
<pre><code class="react">// 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()
是怎麼知道它們該做什麼的,那我希望這篇文章將幫助到你。