閱讀源碼後,來說講React Hooks是怎麼實現的

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

  1. 很難複用邏輯(只能用HOC,或者render props),會致使組件樹層級很深
  2. 會產生巨大的組件(指不少代碼必須寫在類裏面)
  3. 類組件很難理解,好比方法須要bindthis指向不明確

這些確實是存在的問題,好比咱們若是用了react-router+redux+material-ui,極可能隨便一個組件最後export出去的代碼是醬紫的:git

export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))
複製代碼

這就是一個4層嵌套的HOC組件github

同時,若是你的組件內事件多,那麼你的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 = () => {}來快捷綁定,但也就解決了一個聲明的問題,總體的複雜度仍是在的。redux

而後還有在componentDidMountcomponentDidUpdate中訂閱內容,還須要在componentWillUnmount中取消訂閱的代碼,裏面會存在不少重複性工做。最重要的是,在一個ClassComponent中的生命週期方法中的代碼,是很難在其餘組件中複用的,這就致使了了代碼複用率低的問題。數組

還有就是class代碼對於打包工具來講,很難被壓縮,好比方法名稱。markdown

更多詳細的你們能夠去看ReactConf的視頻,我這裏就很少講了,這篇文章的主題是從源碼的角度講講Hooks是如何實現的前端工程師

先來了解一些基礎概念

首先useState是一個方法,它自己是沒法存儲狀態的react-router

其次,他運行在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學習。更多的文章看這裏

相關文章
相關標籤/搜索