Intro
这篇文章将通过一个使用 React Hook 常遇到的问题(
废话不多说,直接看示例Sandbox。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import React, { useState, useEffect } from "react"; import ReactDOM from "react-dom"; function Counter() { const [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); return <h1>{count}</h1>; } const rootElement = document.getElementById("root"); ReactDOM.render(<Counter />, rootElement); |
你能看出示例代码中存在的问题吗?(如果一眼看出来了,那么继续阅读这篇文章可能不会给你带来收益。)这段代码实际运行起来的效果是,页面从
setCount 也可以接受一个 Function
如果之前有过 React 开发经验,这里的第一反应可能会是
确实,
为什么两个 count 不一致?
到这里只是让程序可以运行起来而已,出现理解分歧的原因是啥?
在同一个 JS 方法中,在不同的位置读取同一个变量,得到的结果不一致。
在下一个示例中添加打印
1 2 3 4 5 6 7 8 9 10 | const [count, setCount] = useState(0); console.log('render val:', count) useEffect(() => { let id = setInterval(() => { console.log('interval val:', count) setCount(val => val + 1); }, 1000); return () => clearInterval(id); }, []); |
要理清这个问题,就不得不把【什么是闭包?】扯出来。
函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
这个简单的例子中,哪里会产生闭包?
由于我们给
并且 Function Component 每次
所以,当程序运行起来是,setInterval 内的闭包引用到的一直是最初的
既然跟
1 2 3 4 5 6 7 8 9 10 | const [count, setCount] = useState(0); console.log('render val:', count) useEffect(() => { let id = setInterval(() => { console.log('interval val:', count) setCount(val => val + 1); }, 1000); return () => clearInterval(id); }); |
运行以上代码,确实 interval 中能读到最新的 count 了。
原理是这个
每次都是重新渲染,为什么 useState 可以读到最新的 value
到这里,产生了另一个疑惑点,既然每次
跟踪到 React renderWithHooks 源码处,可以发现 Function Component 在被渲染时确实就是当做普通方法调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 | export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderExpirationTime: ExpirationTime, ): any { // ... let children = Component(props, secondArg); //... return children; } |
组件被调用时,会执行
和
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 | // 首次 render useState hook 时执行 function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { const hook = mountWorkInProgressHook(); // 创建新的 hook 挂载到链表尾部 hook.memoizedState = hook.baseState = initialState; // ... const dispatch: Dispatch< BasicStateAction<S>, > = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, queue, ): any)); // 返回缓存的 memoizedState (这里是 initialState) return [hook.memoizedState, dispatch]; } // 更新 render useState hook 时执行 function updateState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { // useState 的实现也是基于 reducer return updateReducer(basicStateReducer, (initialState: any)); } function updateReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch<A>] { const hook = updateWorkInProgressHook(); // 获取缓存的 hook // ... const dispatch: Dispatch<A> = (queue.dispatch: any); // 返回缓存的 memoizedState return [hook.memoizedState, dispatch]; } |
React 源码在关于 Hook 首次执行和更新是分开处理的,但逻辑都是一样,获取或新建一个 hook,暴露给外部
useRef
再回到原来的问题,要在 setInterval 中能读取到正确的 count,应该怎么做?
另一个钩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | const [count, setCount] = useState(0); const countRef = useRef(count); useEffect(() => { // 及时更新 count 值 countRef.current = count; }); console.log('render val:', count) useEffect(() => { let id = setInterval(() => { // 不直接读取 count,而是 countRef.current console.log('interval val:', countRef.current) setCount(val => val + 1); }, 1000); return () => clearInterval(id); }, []); |
借助
打印结果:
而
1 2 3 4 5 6 7 8 9 10 11 12 | function mountRef<T>(initialValue: T): {|current: T|} { const hook = mountWorkInProgressHook(); const ref = { current: initialValue }; hook.memoizedState = ref; return ref; } function updateRef<T>(initialValue: T): {|current: T|} { const hook = updateWorkInProgressHook(); return hook.memoizedState; } |
这个问题还有另一种常见场景,就是回调事件,例如在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function SwipeToConfirm ({ onChange }) { const onChangeRef = useRef(onChange) useEffect(() => { onChangeRef.current = onChange }) const panResponder = useRef(PanResponder.create({ //... onPanResponderRelease: (evt, gestureState) => { // 一些逻辑处理,符合条件时执行 onChange onChangeRef.current() } })).current; return ( <Animated.View {...panResponder.panHandlers} > </Animated.View> ) } |
???♂?,看到这里相信你对Hook有了更加深入得了解。
推荐阅读
- https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/
- https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e