本文翻譯自: How Does setState Know What to Do?原做者:Dan Abramovhtml
若是有任何版權問題,請聯繫shuirong1997@icloud.comreact
當你在組件中調用setState
時,你以爲會發生什麼?git
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>
元素。github
聽起來彷佛簡潔明瞭。但別急,React(或者說React DOM)是怎麼作的?react-native
更新DOM聽起來像是React DOM的事兒,但別忘了咱們調用的但是this.setState()
,它是React的東西,可不是React DOM的。另外,咱們的基類React.Component
是被定義在React內部。數組
因此問題來了:React.Component
內部的setState
怎麼能去更新DOM呢?dom
事先聲明:就像個人其餘博客,你不須要熟練掌握React。這篇博客是爲那些想要看看面紗以後是什麼東西的人準備的。徹底可選!ide
咱們或許會認爲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()
是怎麼知道它們該作什麼的,那我但願這篇文章將幫助到你。