React實戰系列-setState原理總結

React實戰系列-setState原理總結

我唯一能確定的就是自己的無知 ——蘇格拉底 (哲學之父)

目標

  • 理解setState為何知道更新
  • 理解hooks的執行者

原文鏈接: How Does setState Know What to Do?

疑惑:

當你在組件中調用> setState> 的時候,你認為發生了些什麼?

<code>

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'));

/<code>

當然是:React根據下一個狀態{clicked:true}重新渲染組件,同時更新DOM以匹配返回的

Thanks h1>元素啊。

看起來很直白。但是等等,是 _React_做了這些嗎 ?還是_React DOM _? **

疑惑: 我們或許會認為:React.Component類包含了DOM更新的邏輯。

但是如果是這樣的話,this.setState()又如何能在其他環境下使用呢?舉個例子,React Native app中的組件也是繼承自React.Component。他們依然可以像我們在上面做的那樣調用this.setState(),而且React Native渲染的是安卓和iOS原生的界面而不是DOM。 因此,**React.Component以某種未知的方式將處理狀態(state)更新的任務委託給了特定平臺的代碼。**在我們理解這些是如何發生的之前,讓我們深挖一下包(packages)是如何分離的以及為什麼這樣分離。 **

疑惑: 有一個很常見的誤解就是React“引擎”是存在於react包裡面的。 然而事實並非如此。

** 實際上從React 0.14我們將代碼拆分成多個包以來,react包故意只暴露一些定義組件的API。絕大多數React的_實現_都存在於“渲染器(renderers)”中。

react-dom、react-dom/server、 react-native、 react-test-renderer、 react-art都是常見的渲染器(當然你也可以創建屬於你的渲染器)。

這就是為什麼不管你的目標平臺是什麼,react包都是可用的。從react包中導出的一切,比如React.Component、React.createElement、 React.Children 和(最終的)Hooks,都是獨立於目標平臺的。無論你是運行React DOM,還是 React DOM Server,或是 React Native,你的組件都可以使用同樣的方式導入和使用。 ** 相比之下,渲染器包暴露的都是特定平臺的API,比如說:ReactDOM.render(),可以讓你將React層次結構(hierarchy)掛載進一個DOM節點。每一種渲染器都提供了類似的API。理想狀況下,絕大多數_組件_都不應該從渲染器中導入任何東西。只有這樣,組件才會更加靈活。

✌️ 和大多數人現在想的一樣,React “引擎”就是存在於各個渲染器的內部。

** 很多渲染器包含一份同樣代碼的複製 —— 我們稱為“協調器”(“reconciler”)。構建步驟(build step)將協調器代碼和渲染器代碼平滑地整合成一個高度優化的捆綁包(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()只返回了一些普通對象:

<code> 

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; }/<code>

** ** 當你在代碼中使用 或 的時候, 是**_渲染器決定如何處理這些接口。React DOM也許用某種方式追蹤context的值,但是React DOM Server用的可能是另一種不同的方式。

**所以,如果你將react升級到了16.3+,但是不更新react-dom,那麼你就使用了一個尚不知道Provider 和 Consumer類型的渲染器。**這就是為什麼一個老版本的react-dom會報錯說這些類型是無效的。

疑問 react包並不包含任何有趣的東西,除此之外,具體的實現也是存在於react-dom,react-native之類的渲染器中。但是這並沒有回答我們的問題。React.Component中的setState()如何與正確的渲染器“對話”?

** **答案是:每個渲染器都在已創建的類上設置了一個特殊的字段。**這個字段叫做updater。這並不是_你_要設置的的東西——而是,React DOM、React DOM Server 或 React Native在創建完你的類的實例之後會立即設置的東西:

<code> 
 

const

inst =

new

YourComponent(); inst.props = props; inst.updater = ReactDOMUpdater;

const

inst =

new

YourComponent(); inst.props = props; inst.updater = ReactDOMServerUpdater;

const

inst =

new

YourComponent(); inst.props = props; inst.updater = ReactNativeUpdater;/<code>

** 查看 React.Component中setState的實現, setState所做的一切就是委託渲染器創建這個組件的實例:

<code> 
setState(partialState, callback) {
   
  

this

.updater.enqueueSetState(

this

, partialState, callback); }/<code>

** 這就是this.setState()儘管定義在React包中,卻能夠更新DOM的原因。它讀取由React DOM設置的this.updater`,讓React DOM安排並處理更新。

✌️✌️✌️小結

  1. setState緣由
  2. 存放位置以及如何通信
  3. 渲染器被指派處理state的變化。

疑惑:

當使用Hooks時,useState是怎麼 “知道要做什麼”的 ?

當人們第一次看見Hooks proposal API,他們可能經常會想: useState是怎麼 “知道要做什麼”的?然後假設它比那些包含this.setState()的React.Component類更“神奇”。

但是正如我們今天所看到的,基類中setState()的執行一直以來都是一種錯覺。它除了將調用轉發給當前的渲染器外,什麼也沒做。useState Hook也是做了同樣的事情。 ** **Hooks使用了一個“dispatcher”對象,代替了updater字段。**當你調用React.useState()、React.useEffect()、 或者其他內置的Hook時,這些調用被轉發給了當前的dispatcher。

<code> 

const

React = { __currentDispatcher:

null

, useState(initialState) {

return

React.__currentDispatcher.useState(initialState); }, useEffect(initialState) {

return

React.__currentDispatcher.useEffect(initialState); }, };/<code>

各個渲染器會在渲染你的組件之前設置dispatcher:

<code> 

const

prevDispatcher = React.__currentDispatcher; React.__currentDispatcher = ReactDOMDispatcher;

let

result;

try

{ result = YourComponent(props); }

finally

{ React.__currentDispatcher = prevDispatcher; }/<code>

舉個例子, React DOM Server的實現是在這裡,還有就是React DOM 和 React Native共享的協調器的實現在這裡。

這就是為什麼像react-dom這樣的渲染器需要訪問那個你調用Hooks的react包。否則你的組件將不會“看見”dispatcher!如果在一個組件樹中存在React的多個副本,也許並不會這樣。但是,這總是導致了一些模糊的錯誤,因此Hooks會強迫你在出現問題之前解決包的重複問題。

在高級工具用例中,你可以在技術上覆蓋dispatcher,儘管我們不鼓勵這種操作。(對於__currentDispatcher這個名字我撒謊了,但是你可以在React倉庫中找到真實的名字。)比如說, React DevTools將會使用一個專門定製的dispatcher通過捕獲JavaScript堆棧跟蹤來觀察Hooks樹。請勿模仿。

這也意味著Hooks本質上並沒有與React綁定在一起。如果未來有更多的庫想要重用同樣的原生的Hooks, 理論上來說dispatcher可以移動到一個分離的包中,然後暴露成一個一等(first-class)的API,然後給它起一個不那麼“嚇人”的名字。但是在實踐中,我們會盡量避免過早抽象,直到需要它為止。

updater字段和__currentDispatcher對象都是稱為**依賴注入**的通用編程原則的形式。在這兩種情況下,渲染器將諸如setState之類的功能的實現“注入”到通用的React包中,以使組件更具聲明性。

使用React時,你無需考慮這其中的原理。我們希望React用戶花更多時間考慮他們的應用程序代碼,而不是像依賴注入這樣的抽象概念。但是如果你想知道this.setState()或useState()是如何知道該做什麼的,我希望這篇文章會有所幫助。


分享到:


相關文章: