原文: How Does setState Know What to Do?
譯:可能看到標題的時候會想,怎麼去做還不是看程式碼嗎?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-dom
,react-dom / server
,react-native
,react-test-renderer
,react-art
是在renderers
中的一些例子(你也可以建立屬於你自己的)。
所以,無論在什麼平臺,react
包都可以正常工作。他對外暴露的所有的內容,例如: React.Component
,React.createElement
,React.Children
的作用和Hook,都獨立於目標平臺。無論執行React DOM
,React 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-dom
,react-native
等)提供了React功能和特定於平臺的邏輯的實現。其中一些程式碼是共享的(“reconciler”),但這是各個渲染器的實現細節。
現在我們知道了為什麼每次有新的功能都會同時更新react
和react-dom
包。例如,當React 16.3
新增了Context API
時,React.createContext()
在React包上對外暴露。
但是React.createContext
實際上並沒有實現上下文功能。例如,React DOM
和React 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
將是一個無法解析Provider
和Consumer
型別的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 DOM
,React DOM Server
或React 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
做了同樣的事情。
相對於setState
的updater
欄位而言,Hooks
使用dispatcher
物件。 當呼叫React.useState
,React.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 DOM
和React 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.setState
或useState
如何知道該怎麼做,我希望本文會對你有所幫助。