React 16.7-alpha中新增了新功能:Hooks
。總結他的功能就是:讓FunctionalComponent
具有ClassComponent
的功能。
import React, { useState, useEffect } from 'react'
function FunComp(props) {
const [data, setData] = useState('initialState')
function handleChange(e) {
setData(e.target.value)
}
useEffect(() => {
subscribeToSomething()
return () => {
unSubscribeToSomething()
}
})
return (
<input value={data} onChange={handleChange} />
)
}
複製程式碼
按照Dan的說法,設計Hooks
主要是解決ClassComponent
的幾個問題:
- 很難複用邏輯(只能用HOC,或者render props),會導致元件樹層級很深
- 會產生巨大的元件(指很多程式碼必須寫在類裡面)
- 類元件很難理解,比如方法需要
bind
,this
指向不明確
這些確實是存在的問題,比如我們如果用了react-router
+redux
+material-ui
,很可能隨便一個元件最後export
出去的程式碼是醬紫的:
export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))
複製程式碼
這就是一個4層巢狀的HOC
元件
同時,如果你的元件內事件多,那麼你的constructor
裡面可能會醬紫:
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
的視訊,我這裡就不多講了,這篇文章的主題是從原始碼的角度講講Hooks
是如何實現的
先來了解一些基礎概念
首先useState
是一個方法,它本身是無法儲存狀態的
其次,他執行在FunctionalComponent
裡面,本身也是無法儲存狀態的
useState
只接收一個引數initial value
,並看不出有什麼特殊的地方。所以React在一次重新渲染的時候如何獲取之前更新過的state
呢?
在開始講解原始碼之前,大家先要建立一些概念:
React Element
JSX
翻譯過來之後是React.createElement
,他最終返回的是一個ReactElement
物件,他的資料解構如下:
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
物件,他的資料解構如下:
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
為什麼能夠儲存無狀態元件的原理就比較好理解了。
我們假設有這麼一段程式碼:
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
上的物件長這樣:
{
baseState,
next,
baseUpdate,
queue,
memoizedState
}
複製程式碼
我們叫他Hook物件。這裡面我們最需要關心的是memoizedState
和next
,memoizedState
是用來記錄這個useState
應該返回的結果的,而next
指向的是下一次useState
對應的`Hook物件。
也就是說:
hook1 => Fiber.memoizedState
state1 === hook1.memoizedState
hook1.next => hook2
state2 === hook2.memoizedState
hook2.next => hook3
state3 === hook2.memoizedState
複製程式碼
每個在FunctionalComponent
中呼叫的useState
都會有一個對應的Hook
物件,他們按照執行的順序以類似連結串列的資料格式存放在Fiber.memoizedState
上
重點來了:就是因為是以這種方式進行state
的儲存,所以useState
(包括其他的Hooks)都必須在FunctionalComponent
的根作用域中宣告,也就是不能在if
或者迴圈中宣告,比如
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
返回的方法是醬紫的:
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [workInProgressHook.memoizedState, dispatch];
複製程式碼
呼叫這個方法會建立一個update
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
一起使用,詳情,所以你可以考慮等到正式版再用。
我是Jocky,一個專注於React技巧和深度分析的前端工程師,React絕對是一個越深入學習,越能讓你覺得他的設計精巧,思想超前的框架。關注我獲取最新的React動態,以及最深度的React學習。更多的文章看這裡