手寫模擬實現 React Hooks

前言

首先使用create-react-app新建個項目,而後在index.js寫咱們的代碼,閱讀本文前須要知道經常使用 React Hooks 的基本用法。javascript

useState

import React from 'react'
import ReactDOM from 'react-dom'

let memorizedState
const useState = initialState => {
  memorizedState = memorizedState || initialState // 初始化
  const setState = newState => {
    memorizedState = newState
    render() // setState 以後觸發從新渲染
  }
  return [memorizedState, setState]
}

const App = () => {
  const [count1, setCount1] = useState(0)

  return (
    <div>
      <div>
        <h2>useState: {count1}</h2>
        <button
          onClick={() => {
            setCount1(count1 + 1)
          }}
        >
          添加count1
        </button>
      </div>
    </div>
  )
}

const render = () => {
  ReactDOM.render(<App />, document.getElementById('root'))
}
render()

但到這裏會有一個問題,就是當增長第二個 useState 的時候會發現改變兩個 state 都是改同一個值,同步變化,因而,咱們經過使用數組的方式下標來方式來區分。java

import React from 'react'
import ReactDOM from 'react-dom'

let memorizedState = [] // 經過數組形式存儲有關使用hook的值
let index = 0 // 經過下標記錄 state 的值
const useState = initialState => {
  let currentIndex = index
  memorizedState[currentIndex] = memorizedState[index] || initialState
  const setState = newState => {
    memorizedState[currentIndex] = newState
    render() // setState 以後觸發從新渲染
  }
  return [memorizedState[index++], setState]
}

const App = () => {
  const [count1, setCount1] = useState(0)
  const [count2, setCount2] = useState(10)

  return (
    <div>
      <div>
        <h2>
          useState: {count1}--{count2}
        </h2>
        <button
          onClick={() => {
            setCount1(count1 + 1)
          }}
        >
          添加count1
        </button>
        <button
          onClick={() => {
            setCount2(count2 + 10)
          }}
        >
          添加count2
        </button>
      </div>
    </div>
  )
}

const render = () => {
  index = 0
  ReactDOM.render(<App />, document.getElementById('root'))
}
render()

這樣就實現效果了:
react

分析:git

  • 第一次頁面渲染的時候,根據 useState 順序,聲明瞭 count1 和 count2 兩個 state,並按下標順序依次存入數組中
  • 當調用setState的時候,更新 count1/count2 的值,觸發從新渲染的時候,index 被重置爲 0。而後又從新按 useState 的聲明順序,依次拿出最新 state 的值;由此也可見爲何當咱們使用 hook 時,要注意點 hook 不能在循環、判斷語句內部使用,要聲明在組件頂部。

memorizedState這個數組咱們下面實現部分 hook 都會用到,如今memorizedState數組長度爲 2,依次存放着兩個使用useState後返回的 state 值;github

0: 0
1: 10

每次更改數據後,調用render方法,App函數組件從新渲染,又從新調用useState,但外部變量memorizedState以前已經依次下標記錄下了 state 的值,故從新渲染是直接賦值以前的 state 值作初始值。知道這個作法,下面的useEffect,useCallback,useMemo也是這個原理。數組

固然實際源碼,useState是用鏈表記錄順序的,這裏咱們只是模擬效果。瀏覽器

useReducer

useReducer 接受 Reducer 函數和狀態的初始值做爲參數,返回一個數組。數組的第一個成員是狀態的當前值,第二個成員是發送 action 的 dispatch 函數。緩存

let reducerState

const useReducer = (reducer, initialArg, init) => {
  let initialState
  if (init !== undefined) {
    initialState = init(initialArg) // 初始化函數賦值
  } else {
    initialState = initialArg
  }

  const dispatch = action => {
    reducerState = reducer(reducerState, action)
    render()
  }
  reducerState = reducerState || initialState
  return [reducerState, dispatch]
}

const init = initialNum => {
  return { num: initialNum }
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { num: state.num + 1 }
    case 'decrement':
      return { num: state.num - 1 }
    default:
      throw new Error()
  }
}

const App = () => {
  const [state, dispatch] = useReducer(reducer, 20, init)

  return (
    <div>
      <div>
        <h2>useReducer:{state.num}</h2>
        <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
        <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      </div>
    </div>
  )
}

useEffect

對於 useEffect 鉤子,當沒有依賴值的時候,很容易想到雛形代碼:app

const useEffect = (callback, dependencies) => {
  if (!dependencies) {
    // 沒有添加依賴項則每次執行,添加依賴項爲空數組
    callback()
  }
}

但若是有依賴 state 值,便是咱們使用 useState 後返回的值,這部分咱們就須要用上方定義的數組 memorizedState 來記錄dom

let memorizedState = [] // 存放 hook
let index = 0 // hook 數組下標位置
/**
 * useState 實現
 */
const useState = initialState => {
  let currentIndex = index
  memorizedState[currentIndex] = memorizedState[index] || initialState
  const setState = newState => {
    memorizedState[currentIndex] = newState
    render() // setState 以後觸發從新渲染
  }
  return [memorizedState[index++], setState]
}

/**
 * useEffect 實現
 */
const useEffect = (callback, dependencies) => {
  if (memorizedState[index]) {
    // 不是第一次執行
    let lastDependencies = memorizedState[index] // 依賴項數組
    let hasChanged = !dependencies.every(
      (item, index) => item === lastDependencies[index]
    ) // 循環遍歷依賴項是否與上次的值相同
    if (hasChanged) {
      // 依賴項有改變就執行 callback 函數
      memorizedState[index++] = dependencies
      setTimeout(callback) // 設置宏任務,在組件render以後再執行
    } else {
      index++ // 每一個hook佔據一個下標位置,防止順序錯亂
    }
  } else {
    // 第一次執行
    memorizedState[index++] = dependencies
    setTimeout(callback)
  }
}

const App = () => {
  const [count1, setCount1] = useState(0)
  const [count2, setCount2] = useState(10)

  useEffect(() => {
    console.log('useEffect1')
  }, [count1, count2])

  useEffect(() => {
    console.log('useEffect2')
  }, [count1])

  return (
    <div>
      <div>
        <h2>
          useState: {count1}--{count2}
        </h2>
        <button
          onClick={() => {
            setCount1(count1 + 1)
          }}
        >
          添加count1
        </button>
        <button
          onClick={() => {
            setCount2(count2 + 10)
          }}
        >
          添加count2
        </button>
      </div>
    </div>
  )
}

const render = () => {
  index = 0
  ReactDOM.render(<App />, document.getElementById('root'))
}
render()

程序第一次執行完畢後,memorizedState 數組值以下

0: 0
1: 10
2: [0, 10]
3: [0]

上述代碼回調函數執行,原本咱們能夠用callback()執行便可,但由於useEffect在渲染時是異步執行,而且要等到瀏覽器將全部變化渲染到屏幕後纔會被執行;

由於是異步且等頁面渲染完畢才執行,根據對 JS 事件循環的理解,咱們想要它異步執行任務,就在此建立一個宏任務setTimeout(callback)讓它進入宏任務隊列等待執行,固然這其中具體的渲染過程我這裏就不細說了。

還有一個 hook 是useLayoutEffect,除了執行回調的兩處地方代碼實現不一樣,其餘代碼相同,callback這裏我用微任務Promise.resolve().then(callback),把函數執行加入微任務隊列。

由於useLayoutEffect在渲染時是同步執行,會在全部的 DOM 變動以後同步調用,通常可使用它來讀取 DOM 佈局並同步觸發重渲染。在瀏覽器執行繪製以前,useLayoutEffect 將被同步刷新。

怎麼證實呢?若是你在useLayoutEffect加了死循環,而後從新打開網頁,你會發現看不到頁面渲染的內容就進入死循環了;而若是是useEffect的話,會看到頁面渲染完成後才進入死循環。

useLayoutEffect(() => {
  while (true) {}
}, [])

useCallback

useCallbackuseMemo會在組件第一次渲染的時候執行,以後會在其依賴的變量發生改變時再次執行;而且這兩個 hooks 都返回緩存的值,useMemo 返回緩存計算數據的值,useCallback返回緩存函數的引用。

const useCallback = (callback, dependencies) => {
  if (memorizedState[index]) {
    // 不是第一次執行
    let [lastCallback, lastDependencies] = memorizedState[index]

    let hasChanged = !dependencies.every(
      (item, index) => item === lastDependencies[index]
    ) // 判斷依賴值是否發生改變
    if (hasChanged) {
      memorizedState[index++] = [callback, dependencies]
      return callback
    } else {
      index++
      return lastCallback // 依賴值不變,返回上次緩存的函數
    }
  } else {
    // 第一次執行
    memorizedState[index++] = [callback, dependencies]
    return callback
  }
}

useMemo

useMemo實現與useCallback也很相似,只不過它返回的函數執行後的計算返回值,直接把函數執行了。

const useMemo = (memoFn, dependencies) => {
  if (memorizedState[index]) {
    // 不是第一次執行
    let [lastMemo, lastDependencies] = memorizedState[index]

    let hasChanged = !dependencies.every(
      (item, index) => item === lastDependencies[index]
    )
    if (hasChanged) {
      memorizedState[index++] = [memoFn(), dependencies]
      return memoFn()
    } else {
      index++
      return lastMemo
    }
  } else {
    // 第一次執行
    memorizedState[index++] = [memoFn(), dependencies]
    return memoFn()
  }
}

useContext

代碼出乎意料的少吧...

const useContext = context => {
  return context._currentValue
}

useRef

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變。

let lastRef
const useRef = value => {
  lastRef = lastRef || { current: value }
  return lastRef
}

後面幾個例子我就沒展現出 Demo 了,附上 Github 地址:上述hooks實現和案例

相關文章
相關標籤/搜索