🧑 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
场景
web端有这样一种场景,广告方投放了一些广告,并要求用户停留在当前页面一定时间(例如看完一段30秒的广告)才能收到活动奖励。
用户可能会打开广告后切到其他tab页或者最小化当前页面,去做自己的事情,如何强制用户停留在当前页面,看完广告?

直接使用定时器
一开始比较容易想到的方案是在页面组件中设置一个定时器,组件渲染时开始计时,倒计时结束后调用成功的回调,代码如下
import { useEffect, useRef, useState } from "react";
const Comp = () => {
const [seconds, setSeconds] = useState(5)
const timer = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
timer.current = setInterval(() => {
setSeconds(s => s - 1)
}, 1000);
return () => {
timer.current && clearInterval(timer.current)
}
}, [])
const callback = () => {
console.log('倒计时结束,获得奖励')
}
useEffect(() => {
if(seconds <= 0) {
callback()
timer.current && clearInterval(timer.current)
}
}, [seconds])
return (
<div>
观看 {seconds} 秒广告后获得现金奖励!!!
</div>
)
};
代码运行后也确实起到了倒计时的作用,但是用户切到其他页面时,倒计时依然在继续。
更好的方案
有时候用户可能打开广告页之后又切到了其他页面,或者将页面最小化,然后去做自己的事情,如何强制用户停留在当前页面看完广告呢? 问题其实出在setInterval上,简单的定时器即使页面不在前台,也依然会默默执行。
有没有一旦用户离开当前页,倒计时就停止执行的方法呢?
有!答案就是requestAnimationFrame。由于浏览器的性能优化的考虑,当页面不在前台时(被最小化或者隐藏),浏览器会暂停requestAnimationFrame的执行。
所以我们可以用requestAnimationFrame实现setInterval,实现如下
const mySetInterval = (callback: () => void, delay: number = 0) => {
let start = new Date().getTime();
const obj = { timer: 0 }
const cb = () => {
const current = new Date().getTime();
if (current - start >= delay) {
callback();
start = new Date().getTime();
}
obj.timer = requestAnimationFrame(cb);
};
obj.timer = requestAnimationFrame(cb);
return obj;
};
解释
入参: callback: 一个无参数的回调函数,当达到延迟时间时将调用这个函数。 delay: 一个表示延迟时间的数字(以毫秒为单位)。
返回值: obj 是一个对象,其中包含一个名为 timer 的属性。这个属性用于存储 requestAnimationFrame 的标识符,以便取消动画。
start 变量记录了当前的时间戳(以毫秒为单位),这是用来计算时间差的。
requestAnimationFrame会在浏览器渲染一帧的空闲时间执行参数的回调,这里在回调cb的中间又加入了requestAnimationFrame(cb),即通过递归实现每一帧都会执行cb函数,直到累积的时间大于传入的延迟时间,这里的延迟时间即setInterval的第二个参数,当累积时间达到delay之后执行setInterval的第一个参数,同时更新start时间戳,从而模拟出setInterval的效果。
值得注意的点是,该函数返回的其实是一个对象,而不是requestAnimationFrame的返回值(标识符),这里其实利用了闭包,使得返回的是同一个倒计时的标识,方便清空interval的函数使用。
在组件中使用
利用我们实现的setInterval,用来代替原生的setInterval,页面在最小化或者用户切到其他tab页时,倒计时不再继续运行,直到用户打开该页面,实现强制用户停留在当前页面观看广告。
import { useEffect, useRef, useState } from "react";
const Comp = () => {
const [seconds, setSeconds] = useState(30)
const intervalRef = useRef<{timer: number} | null>(null)
const mySetInterval = (callback: () => void, delay: number = 0) => {
let start = new Date().getTime();
const obj = {
timer: 0
}
const cb = () => {
const current = new Date().getTime();
if (current - start >= delay) {
callback();
start = new Date().getTime();
}
obj.timer = requestAnimationFrame(cb);
};
obj.timer = requestAnimationFrame(cb);
return obj;
};
useEffect(() => {
intervalRef.current = mySetInterval(() => {
setSeconds(s => s - 1)
}, 1000);
return () => {
intervalRef.current && cancelAnimationFrame(intervalRef.current.timer)
}
}, [])
const callback = () => {
console.log('倒计时结束,获得奖励')
}
useEffect(() => {
if (seconds <= 0) {
callback()
intervalRef.current && cancelAnimationFrame(intervalRef.current.timer)
}
}, [seconds])
return (
<div>
观看 {seconds} 秒广告后获得现金奖励!!!
</div>
)
};
参考
can i use 查看浏览器支持版本
MDN关于requestAnimationFrame的说明
ahooks中useRefInterval源码
本文转载于:https://juejin.cn/post/7461206048140361754
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

来源链接:https://www.cnblogs.com/smileZAZ/p/18772446










没有回复内容