首先使用create-react-app
新建個項目,而後在index.js
寫咱們的代碼,閱讀本文前須要知道經常使用 React Hooks 的基本用法。javascript
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 接受 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 鉤子,當沒有依賴值的時候,很容易想到雛形代碼: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
和useMemo
會在組件第一次渲染的時候執行,以後會在其依賴的變量發生改變時再次執行;而且這兩個 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
實現與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() } }
代碼出乎意料的少吧...
const useContext = context => { return context._currentValue }
useRef
返回一個可變的 ref
對象,其 .current
屬性被初始化爲傳入的參數(initialValue)。返回的 ref
對象在組件的整個生命週期內保持不變。
let lastRef const useRef = value => { lastRef = lastRef || { current: value } return lastRef }
後面幾個例子我就沒展現出 Demo 了,附上 Github 地址:上述hooks實現和案例