React系列:useEffect的持续学习

327 阅读6分钟

useEffect的设计意图就是要强迫你关注数据流的改变,然后决定我们的effects该如何和它同步。

对useEffect的不理解

在最近的开发过程中,遇到了一种情况针对单独的数据点发送 websocket,在 useEffect 里面使用 websocket,进入组件的时候发布订阅,卸载组件的时候取消订阅;无论是发布订阅和取消订阅,都需要向后台发送消息(两次消息的内容保持一致,用与后台判断要取消哪一次订阅),消息依赖于外部的一个变量,我写出了如下形式的代码:

 // 类似于:
 const [message, setMessage] = React.useState<string>('') // 保存数据点信息
 const savePreMessage = React.useRef<string>();
 const handleMessage = () => {}
 // 当message改变时,取消上一次的订阅,重新发起新的订阅
 React.useEffect(() => {
     ws.send(message) // 最新的message
     ws.subscribe(handleMessage)
     return () => {
         ws.send(savePreMessage.current) // 上一次的message,使用useRef来保存
         ws.unsubscribe(handleMessage)
     }
 }, [message])

虽然,上面的代码功能是实现了,但是做了一步无用的操作:就是使用 ref 来保存上一次的message,这是没有必要的。因为在组件卸载的时候,拿取的message就是上一次的message,并不是最新的message。

示例demo

 const MockCom = () => {
   const [count, setCount] = React.useState<number>(1);
   const btn = () => {
     setCount((prev) => prev + 1);
   };
   React.useEffect(() => {
     console.log("加载的", count);
     return () => {
       console.log("卸载时", count); // 这里的count是拿取的上一次的值
     };
   }, [count]);
   return <button onClick={btn}>改变count</button>;
 };

当点击按钮,改变count值的时候:

04_1.png

卸载的时候,count就是拿取的上一次的值。

那么问题来了,如何解释组件在卸载的时候,拿取的是上一次的值呢?

推荐阅读

useEffect完整指南

这里面就涉及到了一个概念: Capture Value 值捕获。理解了它,你就知道答案了。

认识Capture Value

首先理解函数组件的渲染(render) ,函数组件也是函数,当函数不停的被调用时候,就是一个全新的自己,都有着属于函数作用域里面的变量。So?

 function Counter() {
   const [count, setCount] = useState(0);
 ​
   return (
     <div>
       <p>You clicked {count} times</p>
       <button onClick={() => setCount(count + 1)}>
         Click me
       </button>
     </div>
   );
 }

点击按钮,改变count,就会不停的重复调用 Counter 函数。那么?

 // 第一次渲染
 function Counter() {
   const count = 0; // useState的返回值
   // ...
   <p>You clicked {count} times</p>
   // ...
 }
 ​
 // 点击一次后, 第二次渲染
 function Counter() {
   const count = 1; // useState的返回值
   // ...
   <p>You clicked {count} times</p>
   // ...
 }
 ​
 // 再击一次后, 第三次渲染
 function Counter() {
   const count = 2; // useState的返回值
   // ...
   <p>You clicked {count} times</p>
   // ...
 }

每次调用 Count 函数时, 里面的count变量就已经确定了,所以拿取的值也就确定了。

简单总结:

当我们更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的count 状态,这个状态值是函数中的一个常量。

函数同样如此。

无论是普通函数,还是hooks函数(useRef除外)。

 function Counter() {
   const [count, setCount] = useState(0);
   
   // 普通函数
   const getCount = () => {
     setTimeout(() => {
       alert('You clicked on: ' + count);
     }, 3000);
   }
   
   // hooks
   useEffect(() => {
     document.title = `You clicked ${count} times`;
   });
 ​
   return (
     <div>
       <p>You clicked {count} times</p>
       <button onClick={() => setCount(count + 1)}>
         Click me
       </button>
     </div>
   );
 }

改变count时,

 // 第一次渲染
 function Counter() {
   const count = 0; // useState的返回值
   const getCount = () => {
     setTimeout(() => {
       alert('You clicked on: ' + count);
     }, 3000);
   }
   
   // hooks
   useEffect(() => {
     document.title = `You clicked ${count} times`; //count:1
   });
   // ...
   <p>You clicked {count} times</p>
   // ...
 }
 ​
 // 点击一次后, 第二次渲染
 function Counter() {
   const count = 1; // useState的返回值
   const getCount = () => {
     setTimeout(() => {
       alert('You clicked on: ' + count);
     }, 3000);
   }
   
   // hooks
   useEffect(() => {
     document.title = `You clicked ${count} times`; // count: 2
   });
   // ...
   <p>You clicked {count} times</p>
   // ...
 }
 ​
 // 再击一次后, 第三次渲染
 function Counter() {
   const count = 2; // useState的返回值
   const getCount = () => {
     setTimeout(() => {
       alert('You clicked on: ' + count);
     }, 3000);
   }
   
   // hooks
   useEffect(() => {
     document.title = `You clicked ${count} times`; //count: 3
   });
   // ...
   <p>You clicked {count} times</p>
   // ...
 }

上面这一些列现象,就是所谓的值捕获(Capture Value) : 寻找变量,在自己的独立作用域中寻找。

答案真相

针对最开始的疑问:组件卸载的时候,为什么拿取的是上一次的值?

组件卸载的时机:当组件新的渲染完成之后,才会去卸载上一次的effect的清除操作,目的就是为了浏览器的流畅。

在加载组件的时候,变量message就已经确定了,所以卸载组件的时候,捕获到的message已经是固定的(变向说明,在进行下一次的加载,message就是所谓的上一次的message)。

想到就补充(解释一种现象):

 const [count, setCount] = React.useState(0)
 ​
 // 打印count值
 const log = () => {
     consolg.log(count)
 }
 ​
 // 改变count值,就打印
 const btn = () => {
     setCount(count + 1)
     log()
 }

点击修改之后, log函数中,是打印不到最新的值。为什么?

以前的理解思维:当修改state时,并不会立即修改,而是会先看有不有其他的state也要修改,然后一起批量更新,过程表现成了异步的形式,所以拿不到最新的值。

现在Capture value的理解: log函数是具有值捕获特性的,它只会捕获当前作用域中的变量state,所以打印的还是以前的值。

上面的说法,没有对与错,只看自己怎么理解。

脱离Capture value

理解了Capture Value,可以明确地喊出下面重要的事实:

每一个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state。

在组件内什么时候去读取props或者state是无关紧要的。 因为它们不会改变。在单次渲染的范围内,props和state始终保持不变。

那有时候的需求就是:就是想要拿取最新的变量,那么就使用 useRef, 相当于全局属性(window属性),改变了,在其他的作用域中也能拿到。

既然都已经写到这里了,把平时学习 useEffect 的知识也回顾一篇吧。

useEffect 中的定时器

情况:想每隔一秒,改变state中的状态(就比如修改count,每秒加一)

 React.useEffect(() => {
     const timer = setInterval(() => {
       setCount(count + 1);
     }, 1000);
     return () => {
       clearInterval(timer)
     }
 }, []);

错误写法一: 定时器的时间间隔是对的,但是count的值获取不对。

 React.useEffect(() => {
     const timer = setInterval(() => {
       setCount(count + 1);
     }, 1000);
     return () => {
       clearInterval(timer)
     }
 }, [count]);

错误写法二: count可以拿到最新的值,但是定时器不停的被重新定义,时间间隔有误

 React.useEffect(() => {
     const timer = setInterval(() => {
       setCount(prev => prev + 1);
     }, 1000);
     return () => {
       clearInterval(timer)
     }
 }, []);

正确写法一setCount(count + 1) 也是直接算出 count + 1的值返回给react内部,然后更改;如果直接写函数形式,react内部已经知道了最新的值,只需要告诉它该怎么做就行了。

 // 自定义hooks,抽离逻辑
 function useInterval(fn, time) {
   const ref = React.useRef(fn);
   React.useLayoutEffect(() => {
     ref.current = fn;
   });
   useEffect(() => {
     setInterval(() => ref.current(), time);
   }, []);
 }
 // 使用
 const fn = () => {
     setCount(count + 1)
 }
 useInterval(fn, 1000)

正确写法二

  1. 在自定义hooks中,useEffect 里面的逻辑,是ref保存的函数调用,没有依赖state。
  2. 在使用的时候,fn 也是具有 Capture value特性, 拿取的count的也是最新值,保存在ref中,也能实现定时器。

useEffect的第一个参数不能写成 async 的形式

从类型上分析:

 function useEffect(effect: EffectCallback, deps?: DependencyList): void;
 type EffectCallback = () => (void | Destructor); // 第一个参数:函数返回的类型

async 返回的是一个promise,类型不匹配。