本文由云+社区宣布
作者:Dan Abramov
打仗 React Hooks 肯定时刻的你,或许会遇到一个奇异的题目: setInterval
用起来没你想的简朴。
Ryan Florence 在他的推文内里说到:
不少朋侪跟我提起,setInterval 和 hooks 一升引的时刻,有种蛋蛋的难过。
老实说,这些朋侪也不是胡扯。刚最先打仗 Hooks 的时刻,确切还挺让人迷惑的。
但我以为谈不上 Hooks 的缺点,而是 React 编程模子和 setInterval
之间的一种形式差别。比拟类(Class),Hooks 更切近 React 编程模子,使得这类差别越发凸起。
虽然有点绕,然则让二者调和相处的要领,照样有的。
本文就来探究一下,怎样让 setInterval 和 Hooks 调和地游玩,为什么是这类体式格局,以及这类体式格局给你带来了什么新才能。
声明:本文采纳循规蹈矩的示例来诠释题目。所以有一些示例虽然看起来可以有捷径可走,然则我们照样一步步来。
假如你是 Hooks 新手,不太邃晓我在纠结啥,无妨读一下 React Hooks 的引见和官方文档。本文假定读者已运用 Hooks 凌驾一个小时。
代码呢?
经由历程下面的体式格局,我们可以轻松地完成一个每秒自增的计数器:
import React, { useState, useEffect, useRef } from 'react';
function Counter() {
let [count, setCount] = useState(0);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
上述 useInterval
并非内置的 React Hook,而是我完成的一个自定义 Hook:
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
});
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
(假如你在错过了,这里也有一个一样的 CodeSandbox 线上示例)
我完成的 useInterval Hook 设置了一个计时器,而且在组件 unmount 的时刻清算掉了。 这是经由历程组件生命周期上绑定 setInterval
与 clearInterval
的组合完成的。
这是一份可以在项目中随便复制粘贴的完成,你以至可以宣布到 NPM 上。
不关心为什么如许完成的读者,就不必继承浏览了。下面的内容是为愿望深切明白 React Hooks 的读者而预备的。
哈?! 🤔
我晓得你想什么:
Dan,这代码不对劲。说好的“地道 JavaScript”呢?React Hooks 打了 React 哲学的脸?
哈,我一最先也是这么想的,然则厥后我转变了,如今,我预备也转变你的主意。最先之前,我先引见下这份完成的才能。
为什么 useInterval()
是一个更合理的 API?
注意下,useInterval
Hook 吸收一个函数和一个延时作为参数:
useInterval(() => {
// ...
}, 1000);
这个跟原生的 setInterval
异常的相似:
setInterval(() => {
// ...
}, 1000);
那为啥不痛快运用 setInterval 呢?
setInterval
和 useInterval
Hook 最大的区分在于,useInterval
Hook 的参数是“动态的”。乍眼一看,可以不是那末显著。
我将经由历程一个现实的例子来申明这个题目:
假如我们愿望 interval 的距离是可调的:
一个延时可输入的计时器
此时无需手动掌握延时,直接动态调解 Hooks 参数就好了。比方说,我们可以在用户切换到另一个选项卡时,下降 AJAX 更新数据的频次。
假如根据类(Class)的体式格局,怎样经由历程 setInterval
完成上述需求呢?我折腾出这个:
class Counter extends React.Component {
state = {
count: 0,
delay: 1000,
};
componentDidMount() {
this.interval = setInterval(this.tick, this.state.delay);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.delay !== this.state.delay) {
clearInterval(this.interval);
this.interval = setInterval(this.tick, this.state.delay);
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
tick = () => {
this.setState({
count: this.state.count + 1
});
}
handleDelayChange = (e) => {
this.setState({ delay: Number(e.target.value) });
}
render() {
return (
<>
<h1>{this.state.count}</h1>
<input value={this.state.delay} onChange={this.handleDelayChange} />
</>
);
}
}
太熟习了!
那改成运用 Hooks 怎样完成呢?
🥁🥁🥁扮演最先了!
function Counter() {
let [count, setCount] = useState(0);
let [delay, setDelay] = useState(1000);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, delay);
function handleDelayChange(e) {
setDelay(Number(e.target.value));
}
return (
<>
<h1>{count}</h1>
<input value={delay} onChange={handleDelayChange} />
</>
);
}
没了,就这么多!
不必于 class 完成的版本,useInterval
Hook “升级到”支撑到支撑动态调解延时的版本,没有增添任何庞杂度。
运用 useInterval
新增动态延时才能,几乎没有增添任何庞杂度。这个上风是运用 class 没法比拟的。
// 牢固延时
useInterval(() => {
setCount(count + 1);
}, 1000);
// 动态延时
useInterval(() => {
setCount(count + 1);
}, delay);
当 useInterval
吸收到另一个 delay 的时刻,它就会从新设置计时器。
我们并没有经由历程实行代码来设置或许清算计时器,而是声清楚明了具有特定延时的计时器 – 这是我们完成的 useInterval 的基础缘由。
假如想暂时停息计时器呢?我可以如许来:
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
(线上示例)
这就是 Hooks 和 React 再一次让我高兴的缘由。我们可以把原有的挪用式 API,包装成声明式 API,从而越发贴切地表达我们的企图。就跟衬着一样,我们可以形貌当前时刻每一个点的状况,而无需战战兢兢地经由历程详细的敕令来操纵它们。
到这里,我愿望你已确信 useInterval
Hook 是一个更好的 API – 至少在组件层面运用的时刻是如许。
但是为什么在 Hooks 里运用 setInterval 和 clearInterval 这么让人恼火? 回到刚最先的计时器例子,我们尝试手动去完成它。
第一次
最简朴的,衬着初始状况:
function Counter() {
const [count, setCount] = useState(0);
return <h1>{count}</h1>;
}
如今我愿望它每秒定时更新。我预备运用 useEffect()
而且返回一个清算要领,因为它是一个须要清算的 Side Effect:
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
});
return <h1>{count}</h1>;
}
(检察 CodeSandbox 线上示例)
看起来很简朴?
但是,这段代码有个诡异的行动。
React 默许会在每次衬着时,都从新实行 effects。这是相符预期的,这机制规避了初期在 React Class 组件中存在的一系列题目。
一般来讲,这是一个好特征,因为大部份的定阅 API 都许可移除旧的定阅并增加一个新的定阅来替代。然则,这不包含 setInterval
。挪用了 clearInterval
后从新 setInterval
的时刻,计时会被重置。假如我们频仍从新衬着,致使 effects 频仍实行,计时器可以基础没有机会被触发!
经由历程运用在一个更小的时刻距离从新衬着我们的组件,可以重现这个 BUG:
setInterval(() => {
// 从新衬着致使的 effect 从新实行会让计时器在挪用之前,
// 就被 clearInterval() 清算掉,以后 setInterval()
// 从新设置的计时器,会从新最先计时
ReactDOM.render(<Counter />, rootElement);
}, 100);
(检察这个 BUG 的线上示例)
第二次
部份读者可以晓得,useEffect
许可我们掌握从新实行的现实。经由历程在第二个参数指定依靠数组,React 就会只在这个依靠数组变动的时刻从新实行 effect。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
假如我们愿望 effect 只在组件 mount 的时刻实行,而且在 unmount 的时刻清算,我们可以通报空数组 []
作为依靠。
然则!不是迥殊熟习 JavaScript 闭包的读者,很可以会犯一个共性毛病。我来树模一下!(我们在设想 lint 划定规矩来协助定位此类毛病,不过如今还没有预备好。)
第一次的题目在于,effect 的从新实行致使计时器太早被清算掉了。假如不从新实行它们,或许可以处置惩罚这个题目:
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
假如如许完成,计时器更新到 1 以后,就住手不动了。(检察这个 BUG 的线上示例)
发生了啥?
题目在于,useEffect 运用的 count 是在第一次衬着的时刻猎取的。 猎取的时刻,它就是 0
。因为一向没有从新实行 effect,所以 setInterval
在闭包中运用的 count
一直是从第一次衬着时来的,所以就有了 count + 1
一直是 1
的征象。呵呵哒!
我觉得你已最先怼天怼地了。Hooks 是什么鬼嘛!
处置惩罚这个题目的一个计划,是把 setCount(count + 1)
替代成“更新回调”的体式格局 setCount(c => c + 1)
。从回调参数中,可以猎取到最新的状况。此非万全之策,新的 props 就没法读取到。
另一个处置惩罚计划是运用 useReducer()
。此计划更加天真。在 reducer 内部,可以接见当前的状况,以及最新的 props。dispatch
要领自身不会转变,所以你可以在闭包里往内里灌任何数据。运用 useReducer()
的一个限定是,你不能在内部触发 effects。(不过,你是可以经由历程返回一个新 state 来触发一些 effect)。
为什么云云困难?
阻抗不婚配
这个术语(译者注:术语原文为 “Impedance Mismatch”)在许多处所被人人运用,Phil Haack 是如许诠释的:
有人说数据库来自火星,对象来自金星。数据库不能自然的和对象模子竖立映照关联。这就像尝试将两块磁铁的 N 极挤在一同一样。
我们此处的“阻抗不婚配”,说的不是数据库和对象。而是 React 编程模子,与敕令式的 setInterval
API 之间的不婚配。
一个 React 组件可以会被 mount 一段时刻,而且阅历多个差别的状况,不过它的 render 效果一次性地形貌了所有这些状况
// 形貌了每一次衬着的状况
return <h1>{count}</h1>
同理,Hooks 让我们声明式地运用一些 effect:
// 形貌每一个计数器的状况
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
我们不须要去设置计时器,然则指清楚明了它是不是应该被设置,以及设置的距离是多少。我们事前的 Hook 就是这么做的。经由历程离散的声明,我们形貌了一个一连的历程。
相对应的,setInterval 却没有形貌到全部历程 – 一旦你设置了计时器,它就没法转变了,只能消灭它。
这就是 React 模子和 setInterval
API 之间的“阻抗不婚配”。
React 组件的 props 和 state 会变化时,都会被从新衬着,而且把之前的衬着效果“忘记”的一尘不染。两次衬着之间,是互不相关的。
useEffect()
Hook 同样会“忘记”之前的效果。它清算上一个 effect 而且设置新的 effect。新的 effect 猎取到了新的 props 和 state。所以我们第一次的事前在某些简朴的情况下,是可以实行的。
然则 setInterval() 不会 “忘记”。 它会一向引用着旧的 props 和 state,除非把它换了。然则只要把它换了,就没法不从新设置时刻了。
等会,真的不能吗?
Refs 是救星!
先把题目整顿下:
- 第一次衬着的时刻,运用
callback1
举行setInterval(callback1, delay)
- 下一次衬着的时刻,运用
callback2
可以接见到新的 props 和 state - 我们没法用 callback2 替代掉 callback1 然则又不重设想时器
假如我们压根不替代计时器,而是传入一个 savedCallback 变量,一直指向最新的计时器回调呢??
如今我们的计划看起来是如许的:
- 设置计时器
setInterval(fn, delay)
,个中fn
挪用savedCallback
。 - 第一次衬着,设置
savedCallback
为callback1
- 第二次衬着,设置
savedCallback
为callback2
- ???
- 行了
可变的 savedCallback
须要在屡次衬着之间“耐久化”,所以不能运用通例变量。我们须要像相似实例字段的手腕。
从 Hooks 的 FAQ 中,我们得知 useRef()
可以帮我们做到这点:
const savedCallback = useRef();
// { current: null }
(你可以已对 React 的 DOM refs 比较熟习了。Hooks 引用了雷同的观点,用于持有恣意可变的值。一个 ref 就行一个“盒子”,可以放东西进去。)
useRef()
返回了一个字面量,持有一个可变的 current
属性,在每一次衬着之间同享。我们可以把最新的计时器回调保留进去。
function callback() {
// 可以读取到最新的 state 和 props
setCount(count + 1);
}
// 每次衬着,保留最新的回调到 ref 中
useEffect(() => {
savedCallback.current = callback;
});
后续就可以在计时器回调中挪用它了:
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
因为传入了 []
,我们的 effect 不会从新实行,所以计时器不会被重置。另一方面,因为设置了 savedCallback
ref,我们可以猎取到最后一次衬着时设置的回调,然后在计时器触发时挪用。
再看一遍完全的完成:
function Counter() {
const [count, setCount] = useState(0);
const savedCallback = useRef();
function callback() {
setCount(count + 1);
}
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
(检察 CodeSandbox 线上示例)
提取为自定义 Hook
不能不认可,上面的代码有点迷。种种花狸狐哨的操纵让人费解不说,另有可以让 state 和 refs 与别的逻辑里的搞混。
我以为,虽然 Hooks 比拟 Class 供应了更底层的才能 – 不过 Hooks 的牛逼在于许可我们重组、笼统后创造出声明语意更优的 Hooks
事实上,我就想如许来写:
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
因而我把我的完成中心拷贝到自定义 Hook 中:
function useInterval(callback) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
}
延时价 1000
是硬编码的,把它参数化:
function useInterval(callback, delay) {
在设置计时器的时刻运用:
let id = setInterval(tick, delay);
如今 delay
可以在屡次衬着之间变动,我须要把它声明为计时器 effect 的依靠:
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
慢着,我们之前不是为了防止计时器重设,才传入了一个 []
的吗?不完满是。我们只是愿望 Hooks 不要在 callback 变动的从新实行。假如 delay
变动了,我们是想要从新启动计时器的。
如今来看下我们的代码是不是是能跑:
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
(读者可以在 CodeSandbox 上试一下)
棒棒的!如今,我们可以无需关注完成细节,在任何组件内里须要的时刻,直接运用 useInterval()
了。
Bonus: 停息计时器
我们愿望在给 delay
传 null
的时刻停息计时器:
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
怎样完成?简朴:不设置计时器就可以了。
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
就如许了。这段代码可以处置惩罚种种可以的变动了:延时价转变、停息和继承。虽然 useEffect()
API 须要我们前期花更多的精神举行设置和清算事情,增加新才能倒是轻松了。
Bonus: 风趣的 Demo
这个 useInterval()
Hook 实在很好玩。如今 side effects 是声明式的,所以组合运用变得轻松多了。
比方说,我们可以运用一个计时器来掌握另一个计时器的 delay:
自动加快的计时器
function Counter() {
const [delay, setDelay] = useState(1000);
const [count, setCount] = useState(0);
// Increment the counter.
useInterval(() => {
setCount(count + 1);
}, delay);
// Make it faster every second!
useInterval(() => {
if (delay > 10) {
setDelay(delay / 2);
}
}, 1000);
function handleReset() {
setDelay(1000);
}
return (
<>
<h1>Counter: {count}</h1>
<h4>Delay: {delay}</h4>
<button onClick={handleReset}>
Reset delay
</button>
</>
);
}
总结
Hooks 须要我们逐步顺应 – 尤其是在面临敕令式和声明式代码的区分时。你可以创造出像 React Spring 一样壮大的声明式笼统,然则他们庞杂的用法偶然会让你慌张。
Hooks 还很年青,另有许多我们可以研讨和对照的形式。假如你习惯于根据“最好实践”来的话,大可不必焦急运用 Hooks。社区还需时刻来尝试和发掘更多的内容。
运用 Hooks 的时刻,涉及到相似 setInterval()
的 API,会遇到一些题目。浏览本文后,愿望读者可以明白而且处置惩罚它们,同时,经由历程建立越发语义化的声明式 API,享用其带来的优点。
此文已由腾讯云+社区在各渠道宣布
猎取更多新颖手艺干货,可以关注我们腾讯云手艺社区-云加社区官方号及知乎机构号