閱讀原始碼後,來講講React Hooks是怎麼實現的

Jokcy發表於2018-11-05

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的幾個問題:

  1. 很難複用邏輯(只能用HOC,或者render props),會導致元件樹層級很深
  2. 會產生巨大的元件(指很多程式碼必須寫在類裡面)
  3. 類元件很難理解,比如方法需要bindthis指向不明確

這些確實是存在的問題,比如我們如果用了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 = () => {}來快捷繫結,但也就解決了一個宣告的問題,整體的複雜度還是在的。

然後還有在componentDidMountcomponentDidUpdate中訂閱內容,還需要在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有誤解,認為statelifeCycle都是自己主動呼叫的,因為我們繼承了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)
}
複製程式碼

先來看一張圖

react-hooks

在我們執行functionalComponent的時候,在第一次執行到useState的時候,他會對應Fiber物件上的memoizedState,這個屬性原來設計來是用來儲存ClassComponentstate的,因為在ClassComponentstate是一整個物件,所以可以和memoizedState一一對應。

但是在Hooks中,React並不知道我們呼叫了幾次useState,所以在儲存state這件事情上,React想出了一個比較有意思的方案,那就是呼叫useState後設定在memoizedState上的物件長這樣:

{
  baseState,
  next,
  baseUpdate,
  queue,
  memoizedState
}
複製程式碼

我們叫他Hook物件。這裡面我們最需要關心的是memoizedStatenextmemoizedState是用來記錄這個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

上面講了Hooksstate是如何儲存的,那麼接下去來講講如何更新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學習。更多的文章看這裡

相關文章