當你在組件中調用`setState`的時候,你認為發生了些什么?
~~~jsx
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 DOM的東西。 而且我們組件的父類`React.Component`也是在React本身定義的。
所以存在于`React.Component`內部的`setState()`是如何更新DOM的呢?
**免責聲明: 就像本博客里[絕大多數](https://overreacted.io/zh-hans/why-do-react-elements-have-typeof-property/)[其他的](https://overreacted.io/zh-hans/how-does-react-tell-a-class-from-a-function/)[帖子](https://overreacted.io/zh-hans/why-do-we-write-super-props/)一樣, 其實你不*需要*知道其中的任何知識,就可以有效地使用React。本文面向的是那些想要了解React背后原理的人。而這完全是可選的!**
我們或許會認為:`React.Component`類包含了DOM更新的邏輯。
但是如果是這樣的話,`this.setState()`又如何能在其他環境下使用呢?舉個例子,React Native app中的組件也是繼承自`React.Component`。他們依然可以像我們在上面做的那樣調用`this.setState()`,而且React Native渲染的是安卓和iOS原生的界面而不是DOM。
你或許對React Test Renderer 或是 Shallow Renderer很熟悉。這些測試策略能讓你正常渲染組件,也可以在組件內部調用`this.setState()`。但是這兩個渲染器并不與DOM相關。
如果你曾使用過一些渲染器像[React ART](https://github.com/facebook/react/tree/master/packages/react-art),你也許也知道在一個頁面中我們是可以使用多個渲染器的。(舉個例子,ART 組件在React DOM樹的內部起作用。)這使得全局標志或變量無法維持。
因此,**`React.Component`以某種未知的方式將處理狀態(state)更新的任務委托給了特定平臺的代碼。**在我們理解這些是如何發生的之前,讓我們深挖一下包(packages)是如何分離的以及為什么這樣分離。
> 有一個很常見 的誤解就是React“引擎” 是存在于`react`包里面的。 然而事實并非如此。
>實際上從[React 0.14](https://reactjs.org/blog/2015/07/03/react-v0.14-beta-1.html#two-packages)我們將代碼拆分成多個包以來,`react`包故意只暴露一些定義組件的API。絕大多數React的*實現*都存在于“渲染器(renderers)”中。
`react-dom`、`react-dom/server`、`react-native`、`react-test-renderer`、`react-art`都是常見的渲染器(當然你也可以[創建屬于你的渲染器](https://github.com/facebook/react/blob/master/packages/react-reconciler/README.md#practical-examples))。
這就是為什么不管你的目標平臺是什么,`react`包都是可用的。從`react`包中導出的一切,比如`React.Component`、`React.createElement`、`React.Children`和(最終的)[Hooks](https://reactjs.org/docs/hooks-intro.html),都是獨立于目標平臺的。無論你是運行React DOM,還是 React DOM Server,或是 React Native,你的組件都可以使用同樣的方式導入和使用。
相比之下,渲染器包暴露的都是特定平臺的API,比如說:`ReactDOM.render()`,可以讓你將React層次結構(hierarchy)掛載進一個DOM節點。每一種渲染器都提供了類似的API。理想狀況下,絕大多數*組件*都不應該從渲染器中導入任何東西。只有這樣,組件才會更加靈活。
**和大多數人現在想的一樣,React “引擎”就是存在于各個渲染器的內部。**很多渲染器包含一份同樣代碼的復制 —— 我們稱為[“協調器”(“reconciler”)](https://github.com/facebook/react/tree/master/packages/react-reconciler)。[構建步驟(build step)](https://reactjs.org/blog/2017/12/15/improving-the-repository-infrastructure.html#migrating-to-google-closure-compiler)將協調器代碼和渲染器代碼平滑地整合成一個高度優化的捆綁包(bundle)以獲得更高的性能。(代碼復制通常來說不利于控制捆綁包的大小,但是絕大多數React用戶同一時間只會選用一個渲染器,比如說`react-dom`。)
這里要注意的是:`react`包僅僅是讓你*使用* React 的特性,但是它完全不知道這些特性是*如何*實現的。而渲染器包(`react-dom`、`react-native`等)提供了React特性的實現以及平臺特定的邏輯。這其中的有些代碼是共享的(“協調器”),但是這就涉及到各個渲染器的實現細節了。
現在我們知道為什么當我們想使用新特性時,`react`和`react-dom`*都*需要被更新。舉個例子,當React 16.3添加了Context API,`React.createContext()`API會被React包暴露出來。
但是`React.createContext()`其實并沒有*實現*context。因為在React DOM 和 React DOM Server 中同樣一個 API 應當有不同的實現。所以`createContext()`只返回了一些普通對象:
~~~jsx
// 簡化版代碼
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`會[報錯說這些類型是無效的](https://stackoverflow.com/a/49677020/458193)。
同樣的警告也會出現在React Native中。然而不同于React DOM的是, 一個React新版本的發布并不立即“強制”發布新的 React Native 版本。他們具有獨立的發布日程。 每隔幾周,更新后的渲染器代碼就會[單獨同步到](https://github.com/facebook/react-native/commits/master/Libraries/Renderer/oss)React Native倉庫。這就是相比 React DOM,React Native 特性可用時間不同的原因。
* * *
好吧,所以現在我們知道了`react`包并不包含任何有趣的東西,除此之外,具體的實現也是存在于`react-dom`,`react-native`之類的渲染器中。但是這并沒有回答我們的問題。`React.Component`中的`setState()`如何與正確的渲染器“對話”?
> 答案是:每個渲染器都在已創建的類上設置了一個特殊的字段。**這個字段叫做`updater`。這并不是*你*要設置的的東西——而是,React DOM、React DOM Server 或 React Native在創建完你的類的實例之后會立即設置的東西:
~~~jsx
// 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;
~~~
查看[`React.Component`中`setState`的實現](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react/src/ReactBaseClasses.js#L58-L67),`setState`所做的一切就是**委托渲染器創建這個組件的實例**:

~~~jsx
// 適當簡化的代碼
setState(partialState, callback) {
// 使用`updater`字段回應渲染器!
this.updater.enqueueSetState(this, partialState, callback);
}
~~~
React DOM Server[也許想](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-dom/src/server/ReactPartialRenderer.js#L442-L448)忽略一個狀態更新并且警告你,而React DOM 與 React Native卻想要讓他們協調器(reconciler)的副本[處理它](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-reconciler/src/ReactFiberClassComponent.js#L190-L207)。
> 這就是this.setState()`盡管定義在React包中,卻能夠更新DOM的原因。它讀取由React DOM設置的`this.updater`,讓React DOM安排并處理更新。
* * *
現在關于類的部分我們已經知道了,那關于Hooks的呢?
當人們第一次看見[Hooks proposal API](https://reactjs.org/docs/hooks-intro.html),他們可能經常會想:`useState是`怎么 “知道要做什么”的?然后假設它比那些包含`this.setState()`的`React.Component`類更“神奇”。
但是正如我們今天所看到的,基類中`setState()`的執行一直以來都是一種錯覺。它除了將調用轉發給當前的渲染器外,什么也沒做。`useState`Hook[也是做了同樣的事情](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react/src/ReactHooks.js#L55-L56)。
> **Hooks使用了一個“dispatcher”對象,代替了`updater`字段。**當你調用`React.useState()`、`React.useEffect()`、 或者其他內置的Hook時,這些調用被轉發給了當前的dispatcher。
~~~jsx
// React內部(適當簡化)
const React = {
// 真實屬性隱藏的比較深,看你能不能找到它!
__currentDispatcher: null,
useState(initialState) {
return React.__currentDispatcher.useState(initialState);
},
useEffect(initialState) {
return React.__currentDispatcher.useEffect(initialState);
},
// ...
};
~~~
各個渲染器會在渲染你的組件之前設置dispatcher:
~~~jsx
// React DOM 內部
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;let result;
try {
result = YourComponent(props);
} finally {
// 恢復原狀 React.__currentDispatcher = prevDispatcher;}
~~~
舉個例子, React DOM Server的實現是在[這里](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-dom/src/server/ReactPartialRendererHooks.js#L340-L354),還有就是React DOM 和 React Native共享的協調器的實現在[這里](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-reconciler/src/ReactFiberHooks.js)。
這就是為什么像`react-dom`這樣的渲染器需要訪問那個你調用Hooks的`react`包。否則你的組件將不會“看見”dispatcher!如果在一個組件樹中存在[React的多個副本](https://github.com/facebook/react/issues/13991),也許并不會這樣。但是,這總是導致了一些模糊的錯誤,因此Hooks會強迫你在出現問題之前解決包的重復問題。
在高級工具用例中,你可以在技術上覆蓋dispatcher,盡管我們不鼓勵這種操作。(對于`__currentDispatcher`這個名字我撒謊了,但是你可以在React倉庫中找到真實的名字。)比如說, React DevTools將會使用[一個專門定制的dispatcher](https://github.com/facebook/react/blob/ce43a8cd07c355647922480977b46713bd51883e/packages/react-debug-tools/src/ReactDebugHooks.js#L203-L214)通過捕獲JavaScript堆棧跟蹤來觀察Hooks樹。*請勿模仿。*
這也意味著Hooks本質上并沒有與React綁定在一起。如果未來有更多的庫想要重用同樣的原生的Hooks, 理論上來說dispatcher可以移動到一個分離的包中,然后暴露成一個一等(first-class)的API,然后給它起一個不那么“嚇人”的名字。但是在實踐中,我們會盡量避免過早抽象,直到需要它為止。
`updater`字段和`__currentDispatcher`對象都是稱為*依賴注入*的通用編程原則的形式。在這兩種情況下,渲染器將諸如`setState`之類的功能的實現“注入”到通用的React包中,以使組件更具聲明性。
使用React時,你無需考慮這其中的原理。我們希望React用戶花更多時間考慮他們的應用程序代碼,而不是像依賴注入這樣的抽象概念。但是如果你想知道`this.setState()`或`useState()`是如何知道該做什么的,我希望這篇文章會有所幫助。
## 摘自
[ setState如何知道該做什么?](https://overreacted.io/zh-hans/how-does-setstate-know-what-to-do/)
- 文檔說明
- 大廠面試題
- HTML
- 001.如何遍歷一個dom樹
- 002.為什么操作DOM會很慢
- 003.瀏覽器渲染HTML的步驟
- 004.DOM和JavaScript的關系
- JS
- 001.數組扁平化并去重排序
- 002.高階函數
- 003.sort() 對數組進行排序
- 004.call 、 apply 和bind的區別
- 006.0.1+0.2為什么等于0.30000000000000004
- 011.var、let、const 的區別及實現原理?
- 010.new操作符都做了什么
- 009.a.b.c.d 和 a['b']['c']['d'],哪個性能更高?
- 016.什么是防抖和節流?有什么區別?如何實現?
- 017.['1', '2', '3'].map(parseInt) what & why ?
- 018.為什么 for 循環嵌套順序會影響性能?
- 019.介紹模塊化發展歷程
- 020.push輸出問題
- 021.判斷數組的三個方法
- 022.全局作用域中,用 const 和 let 聲明的變量不在 window 上,那到底在哪里?如何去獲取?
- 023.輸出以下代碼的執行結果并解釋為什么
- 024.ES6 代碼轉成 ES5 代碼的實現思路是什么
- 025.為什么普通 for 循環的性能遠遠高于 forEach 的性能,請解釋其中的原因。
- 026.數組里面有10萬個數據,取第一個元素和第10萬個元素的時間相差多少
- 027.變量類型
- 028.原型和原型鏈
- 029.作用域和閉包
- 030. 異步
- 031.ES6/7 新標準的考查
- 024.事件冒泡/事件代理
- 025.手寫 XMLHttpRequest 不借助任何庫
- 026.什么是深拷貝?
- 0027.克隆數組的方法
- 0028.ES6之展開運算符(...)
- 0029.arguments
- 0030. requestAnimationFrame
- 0031.遞歸爆棧問題與解決
- 021.簡單改造下面的代碼,使之分別打印 10 和 20
- 032.箭頭函數與普通函數
- 033.去除掉html標簽字符串里的所有屬性
- 034.查找公共父節點
- 035.Promise
- 0036.JSON.stringify ()
- CSS
- 001. BFC
- 002.介紹下 BFC、IFC、GFC 和 FFC
- 003.分析比較 opacity: 0、visibility: hidden、display: none 優劣和適用場景
- 004.怎么讓一個 div 水平垂直居中
- 005.重排重繪
- 006.inline/block/inline-block的區別
- 007.選擇器的權重和優先級
- 008.盒模型
- 009.清除浮動
- 010.flex
- 011.nth-child和nth-of-type的區別
- 0012.overflow
- 0013.CSS3中translate、transform和translation的區別和聯系
- 0014.flex
- 0015.px、em、rem
- 0016.width:100%
- 網絡
- 001.講解下HTTPS的工作原理
- 002.介紹下 HTTPS 中間人攻擊
- 003.談談你對TCP三次握手和四次揮手的理解
- 004.A、B 機器正常連接后,B 機器突然重啟,問 A 此時處于 TCP 什么狀態
- 005.簡單講解一下http2的多路復用
- 006. 介紹下 http1.0、1.1、2.0 協議的區別?
- 007.永久性重定向(301)和臨時性重定向(302)對 SEO 有什么影響
- 008.URL從輸入到頁面展示的過程
- 009.接口如何防刷
- 010.http狀態碼?
- 0111.跨域/如何解決?
- 012.cookie 和 localStorage 有何區別?
- 013.Fetch API
- 014.跨域Ajax請求時是否帶Cookie的設置
- 0015.協商緩存和強緩存
- 性能優化
- 001.前后端分離的項目如何seo
- 002.性能優化的方法
- 003.防抖和節流
- React
- 001.React 中 setState 什么時候是同步的,什么時候是異步的?
- 002.Virtual DOM 真的比操作原生 DOM 快嗎?談談你的想法。
- 003.Hooks 的特別之處
- 004.元素和組件有什么區別?
- 005.什么是 Pure Components?
- 006.HTML 和 React 事件處理有什么區別?
- 007.如何將參數傳遞給事件處理程序或回調函數?
- 008.如何創建 refs?
- 009.什么是 forward refs?
- 010.什么是 Virtual DOM?
- 011.什么是受控組件、非受控組件?
- 012.什么是 Fragments ?
- 013.為什么React元素有一個$$typeof屬性?
- 014.如何在 React 中創建組件?
- 015.React 如何區分 Class 和 Function?
- 016.React 的狀態是什么?
- 017.React 中的 props 是什么?
- 018.狀態和屬性有什么區別?
- 019.如何在 JSX 回調中綁定方法或事件處理程序?
- 020.什么是 "key" 屬性,在元素數組中使用它們有什么好處?
- 021.為什么順序調用對 React Hooks 很重要?
- 022.setState如何知道該做什么?
- 023.hook規則?
- 024.Hooks 與 Class 中調用 setState 有不同的表現差異么?
- 025.useEffect
- 026.fiber的作用
- 027.context的作用?
- 028.setState何時同步何時異步?
- 029.react性能優化
- 030.fiber
- 031.React SSR
- 異步
- 001.介紹下promise
- 002.Async/Await 如何通過同步的方式實現異步
- 003.setTimeout、Promise、Async/Await 的區別
- 004.JS 異步解決方案的發展歷程以及優缺點
- 005.Promise 構造函數是同步執行還是異步執行,那么 then 方法呢?
- 006.模擬實現一個 Promise.finally
- 012.簡單手寫實現promise
- 015.用Promise對象實現的 Ajax
- 007.簡單實現async/await中的async函數
- 008.設計并實現 Promise.race()
- 009.Async/await
- 010.珠峰培訓promise
- git
- 001.提交但沒有push
- 002.gitignore沒有作用?
- Node
- 001.用nodejs,將base64轉化成png文件
- Koa
- 001.koa和express的區別
- 數據庫
- redux
- 001.redux 為什么要把 reducer 設計成純函數
- 002.在 React 中如何使用 Redux 的 connect() ?
- 003.mapStateToProps() 和 mapDispatchToProps() 之間有什么區別?
- 004.為什么 Redux 狀態函數稱為 reducers ?
- 005.如何在 Redux 中發起 AJAX 請求?
- 006.訪問 Redux Store 的正確方法是什么?
- 007.React Redux 中展示組件和容器組件之間的區別是什么?
- 008.Redux 中常量的用途是什么?
- 009.什么是 redux-saga?
- 設計模式
- 公司題目
- 001.餓了么
- 001.div垂直水平居中(flex、絕對定位)
- 002.React子父組件之間如何傳值
- 003.Emit事件怎么發,需要引入什么
- 004.介紹下React高階組件,和普通組件有什么區別
- 005.一個對象數組,每個子對象包含一個id和name,React如何渲染出全部的name
- 006.在哪個生命周期里寫
- 007.其中有幾個name不存在,通過異步接口獲取,如何做
- 008.渲染的時候key給什么值,可以使用index嗎,用id好還是index好
- 009.webpack如何配sass,需要配哪些loader
- 010.配css需要哪些loader
- 011.如何配置把js、css、html單獨打包成一個文件
- 012.監聽input的哪個事件,在什么時候觸發
- 013.兩個元素塊,一左一右,中間相距10像素
- 014.上下固定,中間滾動布局如何實現
- 016.取數組的最大值(ES5、ES6)
- 017.apply和call的區別
- 018.ES5和ES6有什么區別
- 019.some、every、find、filter、map、forEach有什么區別
- 020.上述數組隨機取數,每次返回的值都不一樣
- 021.如何找0-5的隨機數,95-99呢
- 022.頁面上有1萬個button如何綁定事件
- 023.如何判斷是button
- 024.頁面上生成一萬個button,并且綁定事件,如何做(JS原生操作DOM)
- 025.循環綁定時的index是多少,為什么,怎么解決
- 026.頁面上有一個input,還有一個p標簽,改變input后p標簽就跟著變化,如何處理
- 瀏覽器相關
- 001.性能優化
- 002.web安全
- 003.獲取瀏覽器大小
- 004.從輸入 URL 到頁面加載完成的過程中都發生了什么事情?
- 后端
- 001.分布式
- zuku
- 字節
- webpack
- webpack的打包原理是什么
- Webpack-- 常見面試題
- webscoket