React 架構的演變 - Hooks 的實現

語言: CN / TW / HK

這是這個系列的最後一篇文章了,終於收尾了🐶 。

React Hooks 可以說完全顛覆了之前 Class Component 的寫法,進一步增強了狀態複用的能力,讓 Function Component 也具有了內部狀態,對於我個人來說,更加喜歡 Hooks 的寫法。當然如果你是一個使用 Class Component 的老手,初期上手時會覺得很苦惱,畢竟之前沉澱的很多 HOC、Render Props 元件基本沒法用。而且之前的 Function Component 是無副作用的無狀態元件,現在又能通過 Hooks 引入狀態,看起來真的很讓人疑惑。Function Component 的另一個優勢就是可以完全告別 this ,在 Class Component 裡面 this 真的是一個讓人討厭的東西 😶 。

Hook 如何與元件關聯

在之前的文章中多次提到,Fiber 架構下的 updateQueueeffectList 都是連結串列的資料結構,然後掛載的 Fiber 節點上。而一個函式元件內所有的 Hooks 也是通過連結串列的形式儲存的,最後掛載到 fiber.memoizedState 上。

function App() {
  const [num, updateNum] = useState(0)

  return <div
    onClick={() => updateNum(num => num + 1)}
  >{ num }</div>
}

export default App
複製程式碼

我們先簡單看下,呼叫 useState 時,構造連結串列的過程:

var workInProgressHook = null
var HooksDispatcherOnMount = {
  useState: function (initialState) {
    return mountState(initialState)
  }
}

function function mountState(initialState) {
  // 新的 Hook 節點
  var hook = mountWorkInProgressHook()
  // 快取初始值
  hook.memoizedState = initialState
  // 構造更新佇列,類似於 fiber.updateQueue
  var queue = hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedState: initialState
  }
  // 用於派發更新
  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) {
    // 構造連結串列頭節點
    workInProgress.memoizedState = workInProgressHook = hook
  } else {
    // 如果連結串列已經存在,在掛載到 next
    workInProgressHook = workInProgressHook.next = hook
  }

  return workInProgressHook
}
複製程式碼

Hook

如果此時有兩個 Hook,第二個 Hook 就會掛載到第一個 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 的更新佇列

Hook 通過 .next 彼此相連,而每個 Hook 物件下,還有個 queue 欄位,該欄位和 Fiber 節點上的 updateQueue 一樣,是一個更新佇列在,上篇文章 《React 架構的演變-更新機制》中有講到,React Fiber 架構中,更新佇列通過連結串列結構進行儲存。

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>
  }
}
複製程式碼

點選 div 之後,產生的 3 次 setState 通過連結串列的形式掛載到 fiber.updateQueue 上,待到 MessageChannel 收到通知後,真正執行更新操作時,取出更新佇列,將計算結果更新到 fiber.memoizedState

setState

hook.queue 的邏輯和 fiber.updateQueue 的邏輯也是完全一致的。

function App() {
  const [num, updateNum] = useState(0)

  return <div
    onClick={() => {
      // 連續更新 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]
複製程式碼

呼叫 useState 的時候,返回的陣列第二個引數為 dispatch,而 dispatchdispatchAction bind 後得到。

function dispatchAction(fiber, queue, action) {
  var update = {
    next: null,
    action: action,
    // 省略排程相關的引數...
  };

  var pending = queue.pending
  if (pending === null) {
    update.next = update
  } else {
    update.next = pending.next
    pending.next = update
  }
  queue.pending = update

  // 執行更新
  scheduleUpdateOnFiber()
}
複製程式碼

可以看到這裡構造連結串列的方式與 fiber.updateQueue 如出一轍。之前我們通過 updateNumnum 連續更新了 3 次,最後形成的更新佇列如下:

更新佇列

函式元件的更新

前面的文章分享過,Fiber 架構下的更新流程分為遞(beginWork)、歸(completeWork)兩個步驟,在 beginWork 中,會依據元件型別進行 render 操作構造子元件。

function beginWork(current, workInProgress) {
  switch (workInProgress.tag) {
    // 其他型別元件程式碼省略...
    case FunctionComponent: {
      // 這裡的 type 就是函式元件的函式
      // 例如,前面的 App 元件,type 就是 function App() {}
      var Component = workInProgress.type
      var resolvedProps = workInProgress.pendingProps
      // 元件更新
      return updateFunctionComponent(
        current, workInProgress, Component, resolvedProps
      )
    }
  }
}

function updateFunctionComponent(
	current, workInProgress, Component, nextProps
) {
  // 構造子元件
  var nextChildren = renderWithHooks(
    current, workInProgress, Component, nextProps
  )
  reconcileChildren(current, workInProgress, nextChildren)
  return workInProgress.child
}

複製程式碼

看名字就能看出來,renderWithHooks 方法就是構造帶 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
}
複製程式碼

從上面的程式碼可以看出,函式元件更新或者首次渲染時,本質就是將函式取出執行了一遍。不同的地方在於給 ReactCurrentDispatcher 進行了不同的賦值,而 ReactCurrentDispatcher 的值最終會影響 useState 呼叫不同的方法。

根據之前文章講過的雙快取機制,current 存在的時候表示是更新操作,不存在的時候表示首次渲染。

function useState(initialState) {
  // 首次渲染時指向 HooksDispatcherOnMount
  // 更新操作時指向 HooksDispatcherOnUpdate
  var dispatcher = ReactCurrentDispatcher.current
  return dispatcher.useState(initialState)
}
複製程式碼

HooksDispatcherOnMount.useState 的程式碼前面已經介紹過,這裡不再著重介紹。

// HooksDispatcherOnMount 的程式碼前面已經介紹過
var HooksDispatcherOnMount = {
  useState: function (initialState) {
    return mountState(initialState)
  }
}
複製程式碼

我們重點看看 HooksDispatcherOnMount.useState 的邏輯。

var HooksDispatcherOnUpdateInDEV = {
  useState: function (initialState) {
    return updateState()
  }
}

function updateState() {
  // 取出當前 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 的程式碼,這裡的邏輯其實是一樣的。將更新物件的 action 取出,如果是函式就執行,如果不是函式就直接對 state 進行替換操作。

總結

React 系列的文章終於寫完了,這一篇文章應該是最簡單的一篇,如果想拋開 React 原始碼,單獨看 Hooks 實現可以看這篇文章:《React Hooks 原理》。Fiber 架構為了能夠實現迴圈的方式更新,將所有涉及到資料的地方結構都改成了連結串列,這樣的優勢就是可以隨時中斷,為非同步模式讓路,Fiber 樹就像一顆聖誕樹,上面掛滿了各種彩燈(alternateEffectListupdateQueueHooks)。

推薦大家可以將這個系列從頭到尾看一遍,相信會特別有收穫的。