React 架構(gòu)的演變 - Hooks 的實(shí)現(xiàn)
React Hooks 可以說(shuō)完全顛覆了之前 Class Component 的寫(xiě)法,進(jìn)一步增強(qiáng)了狀態(tài)復(fù)用的能力,讓 Function Component 也具有了內(nèi)部狀態(tài),對(duì)于我個(gè)人來(lái)說(shuō),更加喜歡 Hooks 的寫(xiě)法。當(dāng)然如果你是一個(gè)使用 Class Component 的老手,初期上手時(shí)會(huì)覺(jué)得很苦惱,畢竟之前沉淀的很多 HOC、Render Props 組件基本沒(méi)法用。而且之前的 Function Component 是無(wú)副作用的無(wú)狀態(tài)組件,現(xiàn)在又能通過(guò) Hooks 引入狀態(tài),看起來(lái)真的很讓人疑惑。Function Component 的另一個(gè)優(yōu)勢(shì)就是可以完全告別 this ,在 Class Component 里面 this 真的是一個(gè)讓人討厭的東西?? 。
Hook 如何與組件關(guān)聯(lián)
在之前的文章中多次提到,F(xiàn)iber 架構(gòu)下的 updateQueue、effectList 都是鏈表的數(shù)據(jù)結(jié)構(gòu),然后掛載的 Fiber 節(jié)點(diǎn)上。而一個(gè)函數(shù)組件內(nèi)所有的 Hooks 也是通過(guò)鏈表的形式存儲(chǔ)的,最后掛載到 fiber.memoizedState 上。
- function App() {
- const [num, updateNum] = useState(0)
- return <div
- onClick={() => updateNum(num => num + 1)}
- >{ num }</div>
- }
- export default App
我們先簡(jiǎn)單看下,調(diào)用 useState 時(shí),構(gòu)造鏈表的過(guò)程:
- var workInProgressHook = null
- var HooksDispatcherOnMount = {
- useState: function (initialState) {
- return mountState(initialState)
- }
- }
- function function mountState(initialState) {
- // 新的 Hook 節(jié)點(diǎn)
- var hook = mountWorkInProgressHook()
- // 緩存初始值
- hook.memoizedState = initialState
- // 構(gòu)造更新隊(duì)列,類(lèi)似于 fiber.updateQueue
- var queue = hook.queue = {
- pending: null,
- dispatch: null,
- lastRenderedState: initialState
- }
- // 用于派發(fā)更新
- var dispatch = queue.dispatch = dispatchAction.bind(
- null, workInProgress, queue
- )
- // [num, updateNum] = useState(0)
- return [hook.memoizedState, dispatch]
- }
- function mountWorkInProgressHook() {
- var hook = {
- memoizedState: null,
- baseState: null,
- baseQueue: null,
- queue: null,
- next: null
- }
- if (workInProgressHook === null) {
- // 構(gòu)造鏈表頭節(jié)點(diǎn)
- workInProgress.memoizedState = workInProgressHook = hook
- } else {
- // 如果鏈表已經(jīng)存在,在掛載到 next
- workInProgressHook = workInProgressHook.next = hook
- }
- return workInProgressHook
- }
如果此時(shí)有兩個(gè) Hook,第二個(gè) Hook 就會(huì)掛載到第一個(gè) Hook 的 next 屬性上。
- function App() {
- const [num, updateNum] = useState(0)
- const [str, updateStr] = useState('value: ')
- return <div
- onClick={() => updateNum(num => num + 1)}
- >{ str } { num }</div>
- }
- export default App
Hook
Hook 的更新隊(duì)列
Hook 通過(guò) .next 彼此相連,而每個(gè) Hook 對(duì)象下,還有個(gè) queue 字段,該字段和 Fiber 節(jié)點(diǎn)上的 updateQueue 一樣,是一個(gè)更新隊(duì)列在,上篇文章 《React 架構(gòu)的演變-更新機(jī)制》中有講到,React Fiber 架構(gòu)中,更新隊(duì)列通過(guò)鏈表結(jié)構(gòu)進(jìn)行存儲(chǔ)。
- class App extends React.Component {
- state = { val: 0 }
- click () {
- for (let i = 0; i < 3; i++) {
- this.setState({ val: this.state.val + 1 })
- }
- }
- render() {
- return <div onClick={() => {
- this.click()
- }}>val: { this.state.val }</div>
- }
- }
點(diǎn)擊 div 之后,產(chǎn)生的 3 次 setState 通過(guò)鏈表的形式掛載到 fiber.updateQueue 上,待到 MessageChannel 收到通知后,真正執(zhí)行更新操作時(shí),取出更新隊(duì)列,將計(jì)算結(jié)果更新到 fiber.memoizedState。
setState
而 hook.queue 的邏輯和 fiber.updateQueue 的邏輯也是完全一致的。
- function App() {
- const [num, updateNum] = useState(0)
- return <div
- onClick={() => {
- // 連續(xù)更新 3 次
- updateNum(num => num + 1)
- updateNum(num => num + 1)
- updateNum(num => num + 1)
- }}
- >
- { num }
- </div>
- }
- export default App;
- var dispatch = queue.dispatch = dispatchAction.bind(
- null, workInProgress, queue
- )
- // [num, updateNum] = useState(0)
- return [hook.memoizedState, dispatch]
調(diào)用 useState 的時(shí)候,返回的數(shù)組第二個(gè)參數(shù)為 dispatch,而 dispatch 由 dispatchAction bind 后得到。
- function dispatchAction(fiber, queue, action) {
- var update = {
- next: null,
- action: action,
- // 省略調(diào)度相關(guān)的參數(shù)...
- };
- var pending = queue.pending
- if (pending === null) {
- update.next = update
- } else {
- update.next = pending.next
- pending.next = update
- }
- queue.pending = update
- // 執(zhí)行更新
- scheduleUpdateOnFiber()
- }
可以看到這里構(gòu)造鏈表的方式與 fiber.updateQueue 如出一轍。之前我們通過(guò) updateNum 對(duì) num 連續(xù)更新了 3 次,最后形成的更新隊(duì)列如下:
更新隊(duì)列
函數(shù)組件的更新
前面的文章分享過(guò),F(xiàn)iber 架構(gòu)下的更新流程分為遞(beginWork)、歸(completeWork)兩個(gè)步驟,在 beginWork 中,會(huì)依據(jù)組件類(lèi)型進(jìn)行 render 操作構(gòu)造子組件。
- function beginWork(current, workInProgress) {
- switch (workInProgress.tag) {
- // 其他類(lèi)型組件代碼省略...
- case FunctionComponent: {
- // 這里的 type 就是函數(shù)組件的函數(shù)
- // 例如,前面的 App 組件,type 就是 function App() {}
- var Component = workInProgress.type
- var resolvedProps = workInProgress.pendingProps
- // 組件更新
- return updateFunctionComponent(
- current, workInProgress, Component, resolvedProps
- )
- }
- }
- }
- function updateFunctionComponent(
- current, workInProgress, Component, nextProps
- ) {
- // 構(gòu)造子組件
- var nextChildren = renderWithHooks(
- current, workInProgress, Component, nextProps
- )
- reconcileChildren(current, workInProgress, nextChildren)
- return workInProgress.child
- }
看名字就能看出來(lái),renderWithHooks 方法就是構(gòu)造帶 Hooks 的子組件。
- function renderWithHooks(
- current, workInProgress, Component, props
- ) {
- if (current !== null && current.memoizedState !== null) {
- ReactCurrentDispatcher.current = HooksDispatcherOnUpdate
- } else {
- ReactCurrentDispatcher.current = HooksDispatcherOnMount
- }
- var children = Component(props)
- return children
- }
從上面的代碼可以看出,函數(shù)組件更新或者首次渲染時(shí),本質(zhì)就是將函數(shù)取出執(zhí)行了一遍。不同的地方在于給 ReactCurrentDispatcher 進(jìn)行了不同的賦值,而 ReactCurrentDispatcher 的值最終會(huì)影響 useState 調(diào)用不同的方法。
根據(jù)之前文章講過(guò)的雙緩存機(jī)制,current 存在的時(shí)候表示是更新操作,不存在的時(shí)候表示首次渲染。
- function useState(initialState) {
- // 首次渲染時(shí)指向 HooksDispatcherOnMount
- // 更新操作時(shí)指向 HooksDispatcherOnUpdate
- var dispatcher = ReactCurrentDispatcher.current
- return dispatcher.useState(initialState)
- }
HooksDispatcherOnMount.useState 的代碼前面已經(jīng)介紹過(guò),這里不再著重介紹。
- // HooksDispatcherOnMount 的代碼前面已經(jīng)介紹過(guò)
- var HooksDispatcherOnMount = {
- useState: function (initialState) {
- return mountState(initialState)
- }
- }
我們重點(diǎn)看看 HooksDispatcherOnMount.useState 的邏輯。
- var HooksDispatcherOnUpdateInDEV = {
- useState: function (initialState) {
- return updateState()
- }
- }
- function updateState() {
- // 取出當(dāng)前 hook
- workInProgressHook = nextWorkInProgressHook
- nextWorkInProgressHook = workInProgressHook.next
- var hook = nextWorkInProgressHook
- var queue = hook.queue
- var pendingQueue = queue.pending
- // 處理更新
- var first = pendingQueue.next
- var state = hook.memoizedState
- var update = first
- do {
- var action = update.action
- state = typeof action === 'function' ? action(state) : action
- update = update.next;
- } while (update !== null && update !== first)
- hook.memoizedState = state
- var dispatch = queue.dispatch
- return [hook.memoizedState, dispatch]
- }
如果有看之前的 setState 的代碼,這里的邏輯其實(shí)是一樣的。將更新對(duì)象的 action 取出,如果是函數(shù)就執(zhí)行,如果不是函數(shù)就直接對(duì) state 進(jìn)行替換操作。
總結(jié)
React 系列的文章終于寫(xiě)完了,這一篇文章應(yīng)該是最簡(jiǎn)單的一篇,如果想拋開(kāi) React 源碼,單獨(dú)看 Hooks 實(shí)現(xiàn)可以看這篇文章:《React Hooks 原理》。Fiber 架構(gòu)為了能夠?qū)崿F(xiàn)循環(huán)的方式更新,將所有涉及到數(shù)據(jù)的地方結(jié)構(gòu)都改成了鏈表,這樣的優(yōu)勢(shì)就是可以隨時(shí)中斷,為異步模式讓路,F(xiàn)iber 樹(shù)就像一顆圣誕樹(shù),上面掛滿了各種彩燈(alternate、EffectList、updateQueue、Hooks)。
本文轉(zhuǎn)載自微信公眾號(hào)「 更了不起的前端 」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系更了不起的前端 公眾號(hào)。