会使用 React Hook,还要学会简单手写 React Hook
前言 首先使用create-react-app
新建个项目,然后在index.js
写我们的代码,阅读本文前需要知道常用 React Hooks 的基本用法。
useState 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import React from 'react' import ReactDOM from 'react-dom' let memorizedStateconst useState = initialState => { memorizedState = memorizedState || initialState const setState = newState => { memorizedState = newState render() } 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 都是改同一个值,同步变化,于是,我们通过使用数组的方式下标来方式来区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import React from 'react' import ReactDOM from 'react-dom' let memorizedState = [] let index = 0 const useState = initialState => { let currentIndex = index memorizedState[currentIndex] = memorizedState[index] || initialState const setState = newState => { memorizedState[currentIndex] = newState render() } 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()
这样就实现效果了:
分析:
第一次页面渲染的时候,根据 useState
顺序,声明了 count1 和 count2 两个 state,并按下标顺序依次存入数组中 当调用setState
的时候,更新 count1/count2 的值,触发重新渲染的时候,index 被重置为 0。然后又重新按 useState
的声明顺序,依次拿出最新 state 的值;由此也可见为什么当我们使用 hook 时,要注意点 hook 不能在循环、判断语句内部使用,要声明在组件顶部。 memorizedState
这个数组我们下面实现部分 hook 都会用到,现在memorizedState
数组长度为 2,依次存放着两个使用useState
后返回的 state 值;
每次更改数据后,调用render
方法,App
函数组件重新渲染,又重新调用useState
,但外部变量memorizedState
之前已经依次下标记录下了 state 的值,故重新渲染是直接赋值之前的 state 值做初始值。知道这个做法,下面的useEffect
,useCallback
,useMemo
也是这个原理。
当然实际源码,useState
是用链表记录顺序的,这里我们只是模拟效果。
useReducer useReducer 接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的 dispatch 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 let reducerStateconst 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 钩子,当没有依赖值的时候,很容易想到雏形代码:
1 2 3 4 5 6 const useEffect = (callback, dependencies ) => { if (!dependencies) { callback() } }
但如果有依赖 state 值,即是我们使用 useState 后返回的值,这部分我们就需要用上方定义的数组 memorizedState 来记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 let memorizedState = [] let index = 0 const useState = initialState => { let currentIndex = index memorizedState[currentIndex] = memorizedState[index] || initialState const setState = newState => { memorizedState[currentIndex] = newState render() } return [memorizedState[index++], setState] } const useEffect = (callback, dependencies ) => { if (memorizedState[index]) { let lastDependencies = memorizedState[index] let hasChanged = !dependencies.every((item, index ) => item === lastDependencies[index]) if (hasChanged) { memorizedState[index++] = dependencies setTimeout(callback) } else { index++ } } 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 数组值如下
1 2 3 4 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
的话,会看到页面渲染完成后才进入死循环。
1 2 3 useLayoutEffect(() => { while (true ) {} }, [])
useCallback useCallback
和useMemo
会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个 hooks 都返回缓存的值,useMemo 返回缓存计算数据的值,useCallback
返回缓存函数的引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
也很类似,只不过它返回的函数执行后的计算返回值,直接把函数执行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 代码出乎意料的少吧…
1 2 3 const useContext = context => { return context._currentValue }
useRef useRef
返回一个可变的 ref
对象,其 .current
属性被初始化为传入的参数(initialValue)。返回的 ref
对象在组件的整个生命周期内保持不变。
1 2 3 4 5 let lastRefconst useRef = value => { lastRef = lastRef || { current : value } return lastRef }
后面几个例子我就没展示出 Demo 了,附上 Github 地址:上述 hooks 实现和案例