探索react hook的誕生背景和實現過程

最近終因而下定決心將我負責的一個公司內部系統由react v15.4.1升級到v16.8.6。v16.8.6中最被推崇的一個特性應該就是react hook了,實際上手後爲能更順手的使用也是花了2個多小時的時間把react hook的實現源碼看了一遍。vue

useState

一,探索版本

一個函數式組件react

const FunctionComponent = () => {
  return (
    <div>hello functional component</div>
  )
}
複製代碼

要在這個組件中觸發更新要怎麼作,下面咱們就作點努力,改造一下。api

改造1

// 下面的實現中click事件被觸發而後執行sayHello對say賦值hello,
// 這個過程並無觸發react的更新機制,因此頁面不會顯示hello
const FunctionComponent = () => {
  let say = 'hello functional component'
  const sayHello = () => {
    say = 'hello'
  }
  return (
    <>
      <div>{say}</div>
      <button onClick={sayHello}>say hello</button>
    </>
  )
}
複製代碼

改造2

在1中咱們已經改變了say的值,如今我只需想辦法觸發react的更新就能夠了。如何手動觸發react的更新,我想到了forceUpdate數組

// 須要將say做爲一個FunctionComponent的外部變量
// 避免在更新觸發後函數式組件執行對say從新賦值
let say = 'hello functional component'

const FunctionComponent = props => {
  const sayHello = () => {
    // 改變狀態
    say = 'hello'
    // 手動觸發父組件的更新從而更新本身
    props.update()
  }
  return (
    <>
      <div>{say}</div>
      <button onClick={sayHello}>say hello</button>
    </>
  )
}

class Stage extends React.Component {
  // 經過觸發父組件的更新來更新函數式組件
  // update屬性用於觸發更新函數式組件
  handleUpdate = () => {
    this.forceUpdate()
  }

  render() {
    return (
      <div className="App">
        <FunctionComponent update={this.handleUpdate} />
      </div>
    );
  }
}
複製代碼

到此咱們已經達到了使用functional組件實現相似class組件更新機制的目的。緩存

經過上面的探索在不借助useState的狀況咱們也能實現functional組件的狀態更新。bash

問題是咱們每次都得手動調用forceUpdate(),不夠友好。並且很容易相信這個方式性能極低。函數

二,useState版本:

在整個hook的使用中有兩個對象你不能忽視(具體區別請查看源碼):post

  • HooksDispatcherOnMountInDEV —— functional組件在首次調用是使用
  • HooksDispatcherOnUpdateInDEV —— functional組件在更新時調用

使用示例性能

const [count, setCount] = useState(0)
複製代碼

1,useState(0)發生了什麼

每次執行useState都會生成一個hook對象ui

var hook = {
    memoizedState: 傳入的初時值,若是useState接收的是函數則是函數執行的結果,
    baseState: null,
    queue: 保存對state的更新隊列,
    baseUpdate: null,
    next: null
};
複製代碼

在一個functional組件中調用多少次useState()則會新建多少個hook對象,爲了維護這些hook對象react使用了一個WorkInProgressHook的鏈表保存——這一步處理你能夠理解成咱們在探索版本中將functional組件的狀態變量say作全局處理相同的目的

2,返回了什麼count = ?、setCount = ?

var queue = hook.queue = {
  last: null,
  dispatch: null,
  lastRenderedReducer: reducer,
  lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null,
    currentlyRenderingFiber$1, queue);

return [hook.memoizedState, dispatch];
複製代碼
  • hook.memoizedState —— 其實就是咱們探索版本中的say只不過咱們是經過window.say保存,而它經過以前建立的WorkInProgressHook.hook保存
  • dispatch —— 其實也就是咱們探索版本中的props.update,探索版本中咱們使用forceUpdate更新父組件的方式來觸發更新,而它採用了一個更底層的方法scheduleWork。不知道scheduleWork?請參考我以前的react fiber簡單瞭解下

總結:

functional組件有兩個特性註定他不能擁有狀態:
1,一個純函數每次調用都是全新的東西。要破壞這種特性就須要讓這個函數產生副總用——依賴全局變量(window.say、WorkInProgressHook.hook)。
2,以前版本中react官方提供setState用於改變狀態觸發更新,但這個只存在與class組件,新版本的dispatch給了咱們多一個選擇

小知識點:functional組件執行過程當中生成的WorkInProgressHook最終會保存到當前組件對應fiber對象上

// WorkInProgressHook是一個鏈表結構
// firstWorkInProgressHook表示這個鏈表頭部指針
var renderedWork = currentlyRenderingFiber$1;
renderedWork.memoizedState = firstWorkInProgressHook;
複製代碼

useEffect

當你認真看完前面useState的實現過程,你會發現這樣一個過程:

  • 生成hook
  • 用WorkInProgressHook鏈表保存
  • 最終將WorkInProgressHook保存爲當前fiber的memoizedState

若是你對react的fiber有必定了解的話對memoizedState必定不會陌生,咱們在class組件中的state最終也是保存到fiber的memoizedState

手動分割線-----------------------------------------

useEffect發生了什麼

  • 生成effect
var effect = {
  tag: tag,
  create: useEffect的第一個參數傳入的函數,
  destroy: destroy,
  deps: useEffect的第二個參數更新依賴,
  // Circular
  next: null
};
複製代碼
  • 用componentUpdateQueue鏈表保存
  • 最終將componentUpdateQueue保存爲當前fiber的updateQueue,
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;
renderedWork.updateQueue = componentUpdateQueue;
複製代碼

若是你對fiber的調用過程必定了解的話updateQueue你也不會陌生,這個地方存有react diff出來的反作用,用於在commit階段執行。

總結:

和useState有相同的實現過程。
不一樣點:

  • useState須要處理影響頁面更新的數據,而在react fiber中將此類數據所有保存到memoizedState,對memoizedState的使用發送在react fiber的reconciler階段
  • useEffect的目的是在組件mounted後進行做用,fiber中將此類操做做爲effect維護這一個鏈表,effect的觸發過程發生在react fiber的commit階段

useCallback

和useState的生成過程基本相似,不過生成的hook對象有點區別

var hook = {
    memoizedState: [傳入的callback,傳人的更新依賴],
    ...
};
複製代碼

useCallback算是我我的比較喜歡的一個功能

class Stage extends React.Component{
  handleClick = () => {
    console.log('what are you 弄啥呢?')
  }
  render () {
    return (
      <>
        <!-- 大多數狀況下都會建議這個寫法 -->
        <div onClick={this.handleClick}>弄你</div>
        <!-- 通常會吐槽這個寫法 -->
        <div onClick={() => this.handleClick()}>弄你</div>
      </>
    )
  }
}
複製代碼

第一種寫法保證執行render時屬性onClick的值都相同;而第二種寫法每次執行render都會生成一個新的函數,因此在diff時onClick每次都不一樣。

useCallback就用來幫助咱們在functional組件中實現了第一種性能更優的方式

useMemo

和useState的生成過程基本相似,不過生成的hook對象有點區別

var hook = {
    memoizedState: [計算過程的結果,傳人的更新依賴],
    ...
複製代碼

useMemo一樣是我我的比較喜歡的一個功能,在這以前一直但願有官方的支持,簡單說就是對一個計算過程的結果進行緩存。使用過vue的同窗能夠把它想象成vue的computed屬性(vue computed屬性的數據響應和依賴緩存實現過程)。從這一點上看vue領先react好幾年~~~~哈哈哈哈哈哈~~~~

hook的第二個參數

官方提供的hook api不少,其中有幾個能夠接受第二個參數(不傳,或者是一個數組)。

做用:在某個依賴項改變時從新操做第一個參數

若是你使用過React.PureComponent就會知道,他經過對新舊props作一個淺比較來判斷是否須要更新組件。這第二個參數的原理一樣如此。

// 首先對deps作是否爲空的判斷
if (nextDeps !== null) {
  var prevDeps = prevState[1];
  // 而後比較新舊deps
  if (areHookInputsEqual(nextDeps, prevDeps)) {
    return prevState[0];
  }
}

// 比較新舊deps
function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) {
    return false;
  }
  // 對傳入的數組成員作淺比較
  for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
   return false;
  }
  
  return true;
}
複製代碼

其它hook

剩下的幾個hook API你們有興趣能夠自行研讀源碼,實現過程基本和上面幾個相同的思路

總結

最後想說的是沒有黑魔法,若是你react的更新機制、fiber的過程、以及函數的特性有清晰的認識,理解hook也是很容易的。 這篇文章本質上也是一片源碼解讀類型的,可是不多涉及到一些具體實現。經過開篇的一個探索版本模糊認識hook的設計思路。全部這一切都是創建在reactv16版本fiber的優秀設計上。

相關文章
相關標籤/搜索