基础问答
问题:谈一谈你对 React Hook的闭包陷阱的理解。
产生问题的原因:JavaScript 闭包特性 + Hooks 渲染机制
闭包的本质:函数能够访问其定义时所在的词法作用域,即使函数在作用域外执行,也可以记住定义时的词法作用域的内容,后续执行时,使用这些信息。- function callback(index) {
- let idx = index;
- let op;
- return (type) => {
- op = type;
- console.log(op);
- switch (type) {
- case 'add':
- idx++;
- break;
- case 'sub':
- idx--;
- break;
- }
- return idx;
- }
- }
- const fn = callback(8);
- console.log(fn('add')); // 9
- console.log(fn('sub')); // 8
复制代码 这里的 idx 正常会在 callback 函数执行结束后释放,但是由于我们返回的是一个函数,函数中依赖这个 idx 变量,所以未能释放,此时这个变量被这个匿名函数持有,而在 fn 变量存续期间,idx 和 op 都是不会释放的,这也就形成了一个闭包。
不过经典闭包还是 for 循环
Hooks 渲染逻辑:React 组件每次渲染都是独立的快照,可以理解为,每次重新执行相关钩子的时候,组件都会重新生成一个新的作用域。
闭包陷阱:根据上面两点,React Hooks 的闭包陷阱产生过程应当是这样的,React 在渲染开始前创建了新的状态包(作用域),而我们写代码的时候无意中创建了一个闭包,持有了 React 的当前状态,再下次渲染开始时,React 重新创建了状态包,但是我们在一开始创建的闭包持有的依旧是前一次 React 创建的状态,是旧的,这就是产生闭包陷阱的根源。这里我们以一个具体例子来看:- import { useEffect, useState } from "react"
- const App = () => {
- const [count, setCount] = useState(1);
- useEffect(()=> {
- const timer = setInterval(() => console.log(count), 1000);
- return () => clearInterval(timer)
- }, []);
- const addOne = () => {
- setCount(pre => pre+1);
- }
- return (
-
- <p>Hello: {count}</p>
- <button onClick={addOne}>+1</button>
-
- )
- }
- export default App
复制代码 这里在组件首次渲染的时候,useEffect 帮我们设置了一个定时器,定时器执行的函数持有了外部作用域的 count 变量,产生了一个闭包。
再之后,我们在页面上点击按钮时,触发了 setCount(pre => pre+1) 状态更新,但是由于没有配置 useEffect 的更新依赖,所以定时器还是持有旧的状态包。此时打印的还是 1,没有更新。
闭包陷阱破解方式:
- 使用 useRef:useRef 在初始化后,是一个形如 { current: xxx } 的不可变对象,不可变可以理解为,这个对象的地址不会发生变化,所以在浅层次的比较(===)中,更新后的前后对象是一个。所以取值的时候,总是能拿到最新的值。
- 添加 Hooks 依赖:在 useEffect 钩子的依赖列表中增加 count,当 count 发生变化的时候,会重新执行 useEffect ,内部的 timer 会重新生成,拿到最新的作用域的值。
- 修改 state 为一个对象:类似于 useRef,我们在更新 state 的时候,可以直接把内容写入该对象中,避免直接替换 state 对象。
扩展知识
React 官方要求我们不能将 hooks 用 if 条件判断包裹,其原因是 React 的 Fiber 架构中收集 Hooks 信息的时候是按顺序收集的,并以链表的形式进行存储的。如下示例:- function App() {
- const [count, setCount] = useState(0);
- const [isFirst, setIsFirst] = useState(false);
- useEffect(() => {
- console.log('hello init');
- }, []);
- useEffect(() => {
- console.log('count change: ', count);
- }, [count]);
- const a = 1;
- }
复制代码 示例中存在 4 个 hooks,所以 React 收集完成后形成的链表应当是这样的:
React 为链表节点设计了如下数据结构:- type Hook = {
- memoizedState: any,
- /** 省略这里不需要的内容 */
- next: Hook | null,
- };
复制代码 其中 next 就是链表节点用于指向下一个节点的指针,memoizedState 则是上一次更新后的相关 state。组件更新的时候,hooks 会严格按照这个顺序进行执行,按顺序拿到对应的 Hook 对象,所以如果我们用 if else 包裹了其中一个 hook,就会出现链表执行过程中,Hooks 对象取值错误的情况。
同样的,React 官方告诉我们,如果想在更新的时候拿到当前 state 的值,建议使用回调函数的写法,即:setCount(pre => pre + 1) 这种写法,这个原因,通过 Hook 的数据结构也大致可以判断,因为 memoizedState 存储了前一次更新的数据,使用回调时,这个 memoizedState 就可以作为参数提供给我们,并且保证总是正确的。
面试追问
参考前文代码。
useRef 在初始化后,是形如 { current: xxx } 的对象,这个对象地址不会变化,所以我们监听 ref 是不起作用的,同时,和 useState 不同,useRef 内容的变更不会触发组件重新渲染。
- 请谈谈 hooks 在 React 中的更新逻辑?
React 是以链表形式来组织管理 hooks 的,在收集过程中按照顺序组装成链表,然后每次触发状态更新时,会从链表头开始依次判断执行更新。
- 那 hooks 中,useState 的更新是同步还是异步?
可以理解为异步的,展开来说,则是: state 更新函数(如触发 setCount)是同步触发的,React 执行更新(即 count 被更新)是异步的。这种设计主要是出于性能考虑,避免重复渲染,减少重绘重排。
- useEffect 依赖数组传空数组和不传依赖,二者有什么区别?
空数组:effect 仅在组件首次渲染时执行一次,后续不会再执行,相当于组件挂载阶段。
不传依赖:effect 会在组件首次渲染时、每次重新渲染后都执行。这种形式隐含存在渲染循环的风险,即 effect 中存在修改 state 的操作,那么按照不传依赖时执行的规则,就会陷入渲染 -> 更新 -> 触发重渲染 -> 更新 -> 触发重渲染……这样的循环。
友情链接:webfem.com
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |