对于每个使用 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
mdn上说:async function 声明用于定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。
回调函数就是传递一个参数化的函数,就是将这个函数作为一个参数传到另一个主函数里面,当那一个主函数执行完之后,再执行传进去的作为参数的函数。走这个过程的参数化的函数 就叫做回调函数
JavaScript回调函数是成为一名成功的 JavaScript 开发人员必须要了解的一个重要概念。但是我相信,在阅读本文之后,你将能够克服以前使用回调方法遇到的所有障碍。在开始之前,首先要确保我们对函数的理解是扎实的
一般我们使用函数,在顺序上是先定义函数,在去调用它。而回调函数则在写代码的过程中反了过来,先去设计函数的调用场景,然后到了需要调用的时候再去定义它。
简而言之,只要一个组件中某个属性的值是函数,那么就可以说该组件使用了 Render Props 这种技术。听起来好像就那么回事儿,那到底 Render Props 有哪些应用场景呢
nodejs中I/O的操作结果基本上都需要在回调函数中处理,当处理多个事件时回调函数就会一层层的嵌套,这就是回调地狱。Nodejs最大的亮点就在于事件驱动, 非阻塞I/O 模型
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!