usestate中的回调函数_React Hooks 中使用 setInterval 的若干方法

更新日期: 2021-05-11阅读: 964标签: 回调

对于每个使用 react Hooks 的开发者来说,setInterval 是一个绕不过去的”坑“。由于React Hooks 特有的设计理念,如果用固有的思维模式去写 setInterval,很容易触发意想不到的 bug。当然避开 setInterval 的陷阱并不是一个难题,而且在不同的场景下有着不同的 setInterval 书写方式。接下来我们就对这些方法进行一个详细的总结。

错误的写法

如下是 React Hooks 中一个经典的 setInterval 错误写法:

function Index() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}

这样写确实很简洁,也符合开发者固有的思维模式,但是它的实现效果却与开发者的目标背道而驰。

预期的效果是页面上的数字会每秒增加 1 ,但其实数字增加到 1 后便静止不动了。由于 useEffect 的依赖为空数组,所以 setInterval 只会在组件完成初次渲染后被调用一次,从而使得回调函数在之后每次被定时调用时,取到的 count 都是初次渲染时的值 0(闭包的原因),页面上的数值也会永远停留在 1。

简单的场景

如何用最低成本的办法让上面的代码变得正确?我们可以给 setCount 传一个函数作为参数:

function Index() {
const [count, setCount] = useState(0);

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

由于函数参数取到的是最新的 count,所以定时器在执行每次定时任务时,能够给 count 在原来的基础上加 1。

对于只需要定时修改 state 的场景,传函数的方法确实管用。但它并不是解决所有问题的灵丹妙药,只要遇到稍微复杂点的场景,传函数的方法就玩不转了。

首先,最新的 state 虽可以在 setState 的函数参数里取到,但是最新的 props 是无法通过这种方式被获取的,如果 setInterval 的回调函数里包含了 props,这种方法将无法奏效。另外,有些场景中 setInterval 回调函数执行的任务并不是更新数据,而是读取数据,这种情况下压根用不到 setState 方法,从参数里去取最新的 state 就更无从谈起了。

稍加复杂的场景

怎样修改上面的代码,才能让 setInterval 回调函数获取到最新的 state 和props 呢?最便捷的方式应该是给 useEffect 添加依赖数组了:

function Index() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count]);
return <div>{count}</div>;
}

由于 useEffect 添加了 count 为依赖,每当 count 被更新,useEffect 里的函数都会被执行。再加上 React Hooks 中「下一个 effect 执行前,上一个 effect 会被清除」的特性,count 更新后上一个定时器会被清除,下一个定时器会启动。由于定时器启动时 count 已是最新值,所以 setInterval 回调函数在后续执行时能拿到最新的 count。

这里有一点需要注意,useEffect 里的函数包含的所有 state 和 props 都需要写到依赖数组里,如果有遗漏,将可能出现不可预知的 bug。

function Index() {
const [stationId, setStationId] = useState(1);
const [startDate, setStartDate] = useState('2019-01-01');
const [endDate, setEndDate] = useState('2019-01-10');
useEffect(() => {
const timer = setInterval(() => {
axios.get('url', {
params: {
stationId,
startDate,
endDate,
},
})
.then(res => console.log(res.data));
}, 1000);
return () => clearInterval(timer);
}, [stationId, startDate, endDate]);
// 省略若干代码
}

正如上面的代码,虽然 useEffect 里的函数包含了多个 state,但它们都必须被写进依赖数组里。

那么如果函数里包含了多个类似于上面的 axios 请求,那岂不是需要在依赖数组里写上一长串的 state?那样做不仅书写不美观,而且难维护并容易出错。要解决这个问题,可以使用官方提供的 useCallback。

function Index() {
const [stationId, setStationId] = useState(1);
const [startDate, setStartDate] = useState('2019-01-01');
const [endDate, setEndDate] = useState('2019-01-10');
const fetchData = useCallback(() => {
axios.get('url', {
params: {
stationId,
startDate,
endDate,
},
})
.then(res => console.log(res.data));
}, [stationId, startDate, endDate]);
useEffect(() => {
const timer = setInterval(() => {
fetchData();
}, 1000);
return () => clearInterval(timer);
}, [fetchData]);
}

上面的代码将 setInterval 里的函数移到了 useCallback 里,并将 useEffect 的依赖数组拆分给了 useCallback。换句话说,这是把监听 state 转变为了监听函数。这样做本质上并没有改变什么,但却让包含许多个 state 的 useEffect 函数变得更好管理。

虽然添加依赖数组的方法能够应付绝大多数场景的 setInterval 问题,但它的缺点也是显而易见的。

一方面,开发者需要手动补全依赖数组里的 state、props,这样的做法显然不够智能。当多个函数被引用进来、包含的 state、props 数量较大的时候,开发者很容易忘记将一些 state、props 添加到依赖数组当中。而且让开发者自行检查依赖是否添加完整也是一个相对耗时、低效的步骤。

另一方面,添加依赖后,定时器会随着依赖的更新不断被清除又重新建立,这与我们对定时器期望的运行方式是有差别的。在我们固有的思维模式里,定时器应该是一经启动便一直运行的,而不是每隔一段时间换一个新的定时器。除此之外,如果依赖更新频率较高的话,频繁清除、启动 setInterval 会导致定时器无法在准确的时间执行。

依赖较多的场景

那有没有什么办法让 setInterval 在 React Hooks 里只启动一次,便一直正常运行下去呢?我们先回顾一下文章开头给出的错误 setInterval 写法:

function Index() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}

这个错误的写法只启动了一次定时器,只可惜由于 setInterval 里的回调函数只能取到闭包里的 count 值,所以无法达到预期的效果。但是再细看这段代码,我们会发现除了 setInterval 里的回调函数,其他部分都是没问题的。那如何才能在不添加依赖的前提下,让回调函数得到及时的更新?

这就要用到 useRef 了。useRef 并不仅仅是 React Hooks 版本的 createRef,它还具有一个非常有用的特性。在每次渲染中,useRef 所返回的值指向的都是同一个对象,而且该对象的 current 属性是可变的。说白了,这个 current 属性是一个可被直接修改、修改后可被直接读取且能够被传递到下次渲染的变量。根据这个特性,我们可以写出如下代码:

function useSetInterval(callback) {
const ref = useRef();
useEffect(() => {
ref.current = callback;
});
useEffect(() => {
const cb = () => {
ref.current();
};
const timer = setInterval(cb, 1000);
return () => clearInterval(timer);
}, []);
}
export default function Index() {
const [count, setCount] = useState(0);
useSetInterval(() => {
setCount(count + 1);
});
return <div>{count}</div>;
}

在上面的代码中,setInterval 被封装进了一个名为 useSetInterval 的 custom hook 中。每当组件被重新渲染,useEffect 都会将最新的回调函数赋值给 ref.current,接着 ref.current 会被放到 setInterval 里去执行。所以 setInterval 虽只在组件初始化时被启动了一次,但它在执行每次定时任务时,使用的都是最新的回调函数。回调函数不再被困于闭包当中,最新的 state、props 自然能够被取到。

逻辑复杂的场景

虽然添加依赖数组和使用 useRef 的方式能够解决问题,但它们并不是最理想的解决方案。添加依赖数组的方式容易遗漏依赖,useRef 又多少显得有些 hack 了,遇到逻辑较为复杂的场景,这两种方式都容易出问题。

最适合处理复杂场景的方式应该是最符合开发者固有思维模式的。什么办法能够让开发者不用考虑依赖、闭包等问题,静静得在 React Hooks 里写一个 setInterval 呢?我们再来回顾一下文章开头给出的错误但符合固有思维模式的写法,看看是否能够从中获取到一些灵感:

function Index() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}

在介绍 useRef 的时候我们说过,上面代码无法正确执行的原因在于 setInterval 里的回调函数没有及时更新。如果我们再进一步追究问题的根源,会发现问题其实出在回调函数里的 count 上。因为闭包的原因,回调函数无法取到最新的 state,而大多数场景下我们更新 state 前又必须先获取 state 的当前值。那如何才能够在不读取最新 state 的前提下,对 state 进行增量更新?useReducer 给我们提供了解决方案。

function reducer(state = 0) {
return {count: state.count + 1};
}
export default function Index() {
const [state, dispatch] = useReducer(reducer, {count: 0});
useEffect(() => {
setInterval(() => {
dispatch();
}, 1000);
}, []);
return <div>{state.count}</div>;
}

得益于 redux 模型中开发者无法直接修改 store 中的数据,而需要通过 action 行为更新数据的特点,在 setInterval 里使用 dispatch 进行变更时不需要直接读取 state,从而闭包的问题迎刃而解。

虽然在简单场景中,使用 useReducer 会增加代码的体量,带来的不必要的开发量,但遇到复杂场景时,useReducer 的优点便凸显出来。一方面,使用 useReducer 后开发者可以用固有的思维模式去写 setInterval,即使遇到很复杂的页面逻辑也不容易出错;另一方面,在复杂场景下用 reducer 去管理状态,会使数据更新的过程变得清晰有条理,便于后期的维护。

总结

React Hooks 中的 setInterval 看似如同华山一般险峻,但要征服它并非难事,而且也不像攀登华山只有一条路可选。面多众多的技术方案,很难说出哪一个是最好的,只有挑选出最适合应用场景的那一个,才是最理想的选择。

来自:https://blog.csdn.net/weixin_39771351/article/details/111122975

链接: https://fly63.com/article/detial/11500

js中async和await

mdn上说:async function 声明用于定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。

js回调函数的简单理解

回调函数就是传递一个参数化的函数,就是将这个函数作为一个参数传到另一个主函数里面,当那一个主函数执行完之后,再执行传进去的作为参数的函数。走这个过程的参数化的函数 就叫做回调函数

深入理解Js回调函数

JavaScript回调函数是成为一名成功的 JavaScript 开发人员必须要了解的一个重要概念。但是我相信,在阅读本文之后,你将能够克服以前使用回调方法遇到的所有障碍。在开始之前,首先要确保我们对函数的理解是扎实的

js中的回调函数

一般我们使用函数,在顺序上是先定义函数,在去调用它。而回调函数则在写代码的过程中反了过来,先去设计函数的调用场景,然后到了需要调用的时候再去定义它。

如何解决 Render Props 的回调地狱?

简而言之,只要一个组件中某个属性的值是函数,那么就可以说该组件使用了 Render Props 这种技术。听起来好像就那么回事儿,那到底 Render Props 有哪些应用场景呢

nodejs中什么是回调地狱?

nodejs中I/O的操作结果基本上都需要在回调函数中处理,当处理多个事件时回调函数就会一层层的嵌套,这就是回调地狱。Nodejs最大的亮点就在于事件驱动, 非阻塞I/O 模型

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!