主要内容是关于学习react hooks中遇到的一些问题。
在使用react hooks的时候遇到以下场景:
function App() {
const [count, setCount] = useState(1)
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000)
}, [])
return (
<div>
count: {count}
<br />
<button
onClick={() => {
setCount((val) => val + 1);
}}
>
增加 1
</button>
</div>
)
}
会发现,我们页面上展示的count会因为点击按钮而增加,但是useEffect
中打印出来的count
始终是1,为什么呢?
因为我们的react hooks的定义用的是链表的数据结构,大致定义如下:
type Hook = {
memorizedState: any,
baseState: any,
baseUpdate: Update,
next: Hook | null
}
那么我们分析上面的情况,
首次执行时,我们的第一个hooks就是useState
,它的next指向第二个hooks,也就是useEffect
。
当我们点击按钮,count+1,此时会触发组件的rerender,此时又会触发useState
,但是初始值baseState
还是1。
因为useEffect
第二个参数为空数组,依赖项为空也就是只有第一次渲染时执行,所以interval
还是第一次渲染时的interval
,因为useEffect
中的callback
没有被调用。
我们要解决以上的问题,就要让useEffect
中的callback
在每次rerender后,接着useState
去执行,所以要在第二个参数中添加count
为依赖项。
并且,因为过程中会多次设置定时器,所以每次要判断是否存在定时器,并且进行清除,这里用的是useRef
去缓存timer
function App() {
const [count, setCount] = useState(1)
const timer = useRef<number | null>(null);
useEffect(() => {
if (timer.current) {
clearInterval(timer.current);
}
timer.current = setInterval(() => {
console.log(count);
}, 1000)
}, [count])
return (
<div>
count: {count}
<br />
<button
onClick={() => {
setCount((val) => val + 1);
}}
>
增加 1
</button>
</div>
)
}
上一步骤用到的是useRef
去缓存定时器,但其实我们可以直接用useRef
去缓存count
function App() {
const [count, setCount] = useState(1)
const lastCount = useRef(count)
useEffect(() => {
setInterval(() => {
console.log(lastCount.current);
}, 1000)
}, [])
return (
<div>
count: {count}
<br />
<button
onClick={() => {
setCount((val) => val + 1)
lastCount.current += 1
}}
>
增加 1
</button>
</div>
)
}
可以看出方法2比较简便,所以我们可以基于方法2封装一个自定义hooks,用于获取最新的值。
import { useRef } from 'react';
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export default useLatest;
react官方也有相似的处理方法,现处于RFC阶段,它能在保持事件处理函数不变的情况下处理函数中可变的props
或state
。
上面的思路可以做个总结,就是不管我们的状态怎么变,我们只要拿到最新的状态就好了,对此我们可以封装如下的hooks,对传入的function
进行持久化处理。
使用useRef
来保证它的引用地址不变,值是最新的。
使用useMemo来缓存fn
,不用每次都创建一个新的fn
function useMemorizedFn(fn) {
const fnRef = useRef(fn);
fnRef.current = useMemo(() => fn, [fn]);
const memorizedFn = useRef();
if (!memorizedFn.current) {
memorizedFn.current = function (this, ...args) {
return fnRef.current.apply(this, args);
}
}
return memorizedFn.current
}
这样写实现了useState
的基本功能,其中render
表示组件的渲染,但是rerender之后还是初始值,因为会重新调用useState
。
function useState(initvalue) {
let state = initValue;
function setState(newValue) {
state = newState;
render()
}
return [state, setState]
}
在useState
之外定义了state
,虽然解决了初始值的问题,但是这样只能有一个state
。
let _state;
function useState(initValue) {
_state = _state || initValue;
function setState(newValue) {
_state = newState;
render()
}
return [_state, useState]
}
这里用了数组来代替链表,用指针代替next
,cursor++
来避免hooks之间的相互影响。
let memorizedState = []; // hooks的数组,用以模拟链表
let cursor = 0;
function useState(initValue) {
memorizedState[cursor] = memorizedState[cursor] || initValue;
const currentCursor = cursor;
function setState(newValue) {
memorizedState[currentCursor] = newValue;
render()
}
return [memorizedState[cursor++], setState]
}
useEffect的实现如下,主要是遍历依赖数组,判断依赖有无变化,有变化就执行callback
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memorizedState[cursor];
const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true;
if (hasNoDeps || hasChangedDeps) {
callback();
memorizedState[cursor] = depArray;
}
cursor++;
}