閱讀原始碼後,來講講React Hokks是怎麼實現的
React 17-alpha中新增了新功能:`Hooks`。總結他的功能就是:讓`FunctionalComponent`具有`ClassComponent`的功能。
```js
import React, { useState, useEffect } from 'react'
function FunComp( props ) {
const [data, setData] = useStat('initialState')
function handleChange( e ) {
setData(e.target.value)
}
useEffect(() => {
subscribeToSomething()
return () => {
unSubscribeToSomething()
}
})
return (
<input value ={data} onChange ={handleChange} />
)
}
```
按照 *Dan* 的說法,設計`Hooks`主要是解決`ClassComponent`的幾個問題:
1. 很難複用邏輯(只能用HOC,或者render props),會導致元件樹層級很深
2. 會產生巨大的元件(指很多程式碼必須寫在類裡面)
3. 類元件很難理解,比如方法需要`bind`,`this`指向不明確
這些確實是存在的問題,比如我們如果用了`react-router`+`redux`+`material-ui`,很可能隨便一個元件最後`export`出去的程式碼是醬紫的:
```js
export default withStyle(style)(connect( /*something*/ )(withRouter(MyComponent)))
```
這就是一個4層巢狀的`HOC`元件
同時,如果你的元件內事件多,那麼你的`constructor`裡面可能會醬紫:
```js
class MyComponent extends React.Component {
constructor() {
// initiallize
this .handler1 = this .handler1.bind( this )
this .handler2 = this .handler2.bind( this )
this .handler3 = this .handler3.bind( this )
this .handler4 = this .handler4.bind( this )
this .handler5 = this .handler5.bind( this )
// ...more
}
}
```
雖然最新的`class`語法可以用`handler = () => {}`來快捷繫結,但也就解決了一個宣告的問題,整體的複雜度還是在的。
然後還有在`componentDidMount`和`componentDidUpdate`中訂閱內容,還需要在`componentWillUnmount`中取消訂閱的程式碼,裡面會存在很多重複性工作。最重要的是,在一個`ClassComponent`中的生命週期方法中的程式碼,是很難在其他元件中複用的,這就導致了了程式碼複用率低的問題。
還有就是`class`程式碼對於打包工具來說,很難被壓縮,比如方法名稱。
更多詳細的大家可以去看[`ReactConf`的視訊]( ofollow,noindex"> https://www. youtube.com/watch? v=V-QO-KO90iQ&t=3060s ),我這裡就不多講了, **這篇文章的主題是從原始碼的角度講講`Hooks`是如何實現的**
### 先來了解一些基礎概念
首先`useState`是一個方法,它本身是無法儲存狀態的
其次,他執行在`FunctionalComponent`裡面,本身也是無法儲存狀態的
`useState`只接收一個引數`initial value`,並看不出有什麼特殊的地方。所以React在一次重新渲染的時候如何獲取之前更新過的`state`呢?
在開始講解原始碼之前,大家先要建立一些概念:
###### React Element
`JSX`翻譯過來之後是`React.createElement`,他最終返回的是一個`ReactElement`物件,他的資料解構如下:
```js
const element = {
$$typeof: REACT_ELEMENT_TYPE, // 是否是普通Element_Type
// Built-in properties that belong on the element
type: type, // 我們的元件,比如`class MyComponent`
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
```
這其中需要注意的是`type`,在我們寫`<MyClassComponent {...props} />`的時候,他的值就是`MyClassComponent`這個`class`,而不是他的例項,例項是在後續渲染的過程中建立的。
###### Fiber
每個節點都會有一個對應的`Fiber`物件,他的資料解構如下:
```js
function FiberNode(
tag : WorkTag,
pendingProps : mixed,
key : null | string,
mode : TypeOfMode,
) {
// Instance
this .tag = tag;
this .key = key;
this .elementType = null; // 就是ReactElement的`$$typeof`
this .type = null; // 就是ReactElement的type
this .stateNode = null;
// Fiber
this .return = null;
this .child = null;
this .sibling = null;
this .index = 0;
this .ref = null;
this .pendingProps = pendingProps;
this .memoizedProps = null;
this .updateQueue = null;
this .memoizedState = null;
this .firstContextDependency = null;
// ...others
}
```
在這裡我們需要注意的是`this.memoizedState`,這個`key`就是用來儲存在上次渲染過程中最終獲得的節點的`state`的,每次執行`render`方法之前,React會計算出當前元件最新的`state`然後賦值給`class`的例項,再呼叫`render`。
**所以很多不是很清楚React原理的同學會對React的`ClassComponent`有誤解,認為`state`和`lifeCycle`都是自己主動呼叫的,因為我們繼承了`React.Component`,它裡面肯定有很多相關邏輯。事實上如果有興趣可以去看一下`Component`的原始碼,大概也就是100多行,非常簡單。所以在React中,`class`僅僅是一個載體,讓我們寫元件的時候更容易理解一點,畢竟元件和`class`都是封閉性較強的**
### 原理
在知道上面的基礎之後,對於`Hooks`為什麼能夠儲存無狀態元件的原理就比較好理解了。
我們假設有這麼一段程式碼:
```js
function FunctionalComponent () {
const [state1, setState1] = useState(1)
const [state2, setState2] = useState(2)
const [state3, setState3] = useState(3)
}
```
先來看一張圖

在我們執行`functionalComponent`的時候,在第一次執行到`useState`的時候,他會對應`Fiber`物件上的`memoizedState`,這個屬性原來設計來是用來儲存`ClassComponent`的`state`的,因為在`ClassComponent`中`state`是一整個物件,所以可以和`memoizedState`一一對應。
但是在`Hooks`中,React並不知道我們呼叫了幾次`useState`,所以在儲存`state`這件事情上,React想出了一個比較有意思的方案,那就是呼叫`useState`後設置在`memoizedState`上的物件長這樣:
```js
{
baseState,
next,
baseUpdate,
queue,
memoizedState
}
```
我們叫他 *Hook* 物件。這裡面我們最需要關心的是`memoizedState`和`next`,`memoizedState`是用來記錄這個`useState`應該返回的結果的,而`next`指向的是下一次`useState`對應的`Hook物件。
也就是說:
```js
hook1 => Fiber.memoizedState
state1 === hoo1.memoizedState
hook1.next => hook2
state2 === hook2.memoizedState
hook2.next => hook3
state3 === hook2.memoizedState
```
每個在`FunctionalComponent`中呼叫的`useState`都會有一個對應的`Hook`物件,他們按照執行的順序以類似連結串列的資料格式存放在`Fiber.memoizedState`上
**重點來了:就是因為是以這種方式進行`state`的儲存,所以`useState`(包括其他的Hooks)都必須在`FunctionalComponent`的根作用域中宣告,也就是不能在`if`或者迴圈中宣告,比如**
```js
if (something) {
const [state1] = useState(1)
}
// or
for (something) {
const [state2] = useState(2)
}
```
**最主要的原因就是你不能確保這些條件語句每次執行的次數是一樣的** ,也就是說如果第一次我們建立了`state1 => hook1, state2 => hook2, state3 => hook3`這樣的對應關係之後,下一次執行因為`something`條件沒達成,導致`useState(1)`沒有執行,那麼執行`useState(2)`的時候,拿到的`hook`物件是`state1`的,那麼整個邏輯就亂套了, **所以這個條件是必須要遵守的!**
### setState
上面講了`Hooks`中`state`是如何儲存的,那麼接下去來講講如何更新`state`
我們呼叫的呼叫`useState`返回的方法是醬紫的:
```js
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [workInProgressHook.memoizedState, dispatch];
```
呼叫這個方法會建立一個`update`
```js
var update = {
expirationTime: _expirationTime,
action: action,
callback: callback !== undefined ? callback : null,
next: null
}
```
這裡的`action`是我們呼叫`setState1`傳入的值,而這個`update`會被加入到`queue`上,因為可能存在一次性呼叫多次`setState1`的清空(跟React的batchUpdate有關,以後有機會講。)
在收集完這所有`update`之後,會排程一次`React`的更新,在更新的過程中,肯定會執行到我們的`FunctionalComponent`,那麼就會執行到對應的`useState`,然後我們就拿到了`Hook`物件,他儲存了`queue`物件表示有哪些更新存在,然後依次進行更新,拿到最新的`state`儲存在`memoizedState`上,並且返回,最終達到了`setState`的效果。
### 總結
其實本質上跟`ClassComponent`是差不多的,只不過因為`useState`拆分了單一物件`state`,所以要用一個相對獨特的方式進行資料儲存,而且會存在一定的規則限制。
但是這些條件完全不能掩蓋`Hooks`的光芒,他的意義是在是太大了,讓`React`這個 **函數語言程式設計** 正規化的框架終於拜託了要用類來建立元件的尷尬場面。事實上類的存在意義確實不大,比如`PuerComponent`現在也有對應的`React.memo`來讓函式元件也能達到相同的效果。
最後,因為真的要把原始碼攤開來講,就會涉及到一些其他的原始碼內容,比如`workInProgress => current`的轉換,`expirationTime`涉及的排程等,反而會導致大家無法理解本篇文章的主體`Hooks`,所以我在寫完完整原始碼解析後又總結歸納了這篇文章來單獨釋出。希望能幫助各位童鞋更好得理解`Hooks`,並能大膽用到實際開發中去。
因為:真的很好用啊!!!
# 注意
目前`react-hot-loader`不能和`hooks`一起使用,[詳情]( https:// github.com/gaearon/reac t-hot-loader/issues/1088 ),所以你可以考慮等到正式版再用。