【力扣】2715. 执行可取消的延迟函数
【力扣】2715. 执行可取消的延迟函数
文章目录
- 【力扣】2715. 执行可取消的延迟函数
- 一、题目
- 二、解决方案
- 1、概述
- 2、闭包
- (1)工作原理:
- 3、setTimeout
- (1)工作原理:
- 方法 1:使用闭包
- (1)思路:
- (2)算法:
- (3)实现:
- (4)复杂度分析:
- 方法 2:使用布尔标志
- (1)思路:
- (2)算法:
- (3)实现:
- (4)复杂度分析:
- 4、面试提示
一、题目
给定一个函数 fn
,一个参数数组 args
和一个以毫秒为单位的超时时间 t
,返回一个取消函数 cancelFn
。
在 cancelTimeMs
的延迟后,返回的取消函数 cancelFn
将被调用。
setTimeout(cancelFn, cancelTimeMs)
最初,函数 fn
的执行应该延迟 t
毫秒。
如果在 t
毫秒的延迟之前调用了函数 cancelFn
,它应该取消 fn
的延迟执行。否则,如果在指定的延迟 t
内没有调用 cancelFn
,则应执行 fn
,并使用提供的 args
作为参数。
示例 1
输入:fn = (x) => x * 5, args = [2], t = 20
输出:[{"time": 20, "returned": 10}]
解释:
const cancelTimeMs = 50;
const cancelFn = cancellable((x) => x * 5, [2], 20);
setTimeout(cancelFn, cancelTimeMs);取消操作被安排在延迟了 cancelTimeMs(50毫秒)后进行,这发生在 fn(2) 在20毫秒时执行之后。
示例 2:
输入:fn = (x) => x**2, args = [2], t = 100
输出:[]
解释:
const cancelTimeMs = 50;
const cancelFn = cancellable((x) => x**2, [2], 100);
setTimeout(cancelFn, cancelTimeMs);取消操作被安排在延迟了 cancelTimeMs(50毫秒)后进行,这发生在 fn(2) 在100毫秒时执行之前,导致 fn(2) 从未被调用。
示例 3:
输入:fn = (x1, x2) => x1 * x2, args = [2,4], t = 30
输出:[{"time": 30, "returned": 8}]
解释:
const cancelTimeMs = 100;
const cancelFn = cancellable((x1, x2) => x1 * x2, [2,4], 30);
setTimeout(cancelFn, cancelTimeMs);取消操作被安排在延迟了 cancelTimeMs(100毫秒)后进行,这发生在 fn(2,4) 在30毫秒时执行之后。
提示:
fn
是一个函数args
是一个有效的 JSON 数组1 <= args.length <= 10
20 <= t <= 1000
10 <= cancelTimeMs <= 1000
二、解决方案
1、概述
在这个教程中,我们将学习如何创建一个函数,它可以在指定的延迟之后执行给定的函数,除非在延迟到期之前调用了一个取消函数 cancelFn
。取消函数应该能够阻止延迟函数的执行。
2、闭包
在 JavaScript
中,闭包是函数和函数被声明时的词法环境的组合。词法环境包括在闭包创建时可用的变量、函数和作用域。
(1)工作原理:
当一个函数定义在另一个函数内部时,就创建了一个闭包。内部函数保留对外部函数的变量和作用域的引用。
当外部函数执行完成并返回时,闭包仍然保持其捕获的变量和作用域链的引用。
闭包允许内部函数访问和操作其外部函数的变量,即使外部函数的执行已经完成。
这种行为是可能的,因为闭包保持对其外部函数的变量和作用域链的引用,防止它们被垃圾回收。
在问题的上下文中,闭包被用于保持对定时器变量的引用,即使创建闭包的函数已经返回。这使得取消函数能够访问并修改定时器变量,有效地取消了延迟函数的执行。
3、setTimeout
setTimeout
是JavaScript
中的一个内置函数,它允许您在指定的延迟之后安排一个函数的执行。它可以接受无限数量的参数,但通常前两个参数总是要执行的函数和以毫秒为单位的延迟时间。
注意:setTimeout
实际上是一个可变参数函数,可以接受无限数量的参数。
以下是如何使用 setTimeout
的示例:
function delayedFunction() {console.log("延迟函数执行!");
}const delay = 2000;const timerId = setTimeout(delayedFunction, delay);// 在延迟到期之前取消执行:
clearTimeout(timerId);
(1)工作原理:
- 当调用
setTimeout
时,它启动一个计时器,并设置它在指定的延迟后运行。 - 在延迟到期后,
JavaScript
事件循环将指定的函数放入执行队列。 - 一旦调用堆栈为空,函数就会被执行,其中的任何相关代码都会运行。
- 如果在延迟到期之前取消了
setTimeout
函数,计划的函数将不会被执行。
在问题的上下文中,setTimeout
用于在可取消函数中安排延迟函数(fn
)的执行,延迟时间为t。
总的来说,闭包和 setTimeout
在这个问题中共同工作,以创建一个可取消的延迟函数执行机制。闭包保持对 timeoutId
变量的引用,而setTimeout
在指定的延迟后安排函数的执行。
在该问题的上下文中,在cancellable
函数内使用 setTimeout
来调度在指定的延迟(t)之后延迟函数(fn)的执行。
方法 1:使用闭包
(1)思路:
我们可以使用 setTimeout
函数安排在指定的延迟 t 之后执行延迟函数 fn
。然后,我们使用 apply
方法将参数从args
数组传递给 fn
。
此外,通过将 setTimeout
返回的timeoutId
存储在 timeoutId
变量中,我们可以通过使用timeoutId
来调用clearTimeout
来取消延迟函数的执行。
(2)算法:
- 在
cancellable
函数内部,我们使用setTimeout
安排延迟时间t
后执行fn
。使用apply
方法将参数数组args
作为参数传递给fn
。另外,setTimeout
函数返回一个计时器ID
,它存储在timeoutId
变量中。 - 接下来,定义一个
cancelFn
函数,它使用timeoutId
调用clearTimeout
以取消延迟函数的执行。 - 最后,从
cancelFn
函数中返回cancellable
函数。
(3)实现:
/*** @param {Function} fn* @param {Array} args* @param {number} t* @return {Function}
*/
var cancellable = function(fn, args, t) {const timeoutId = setTimeout(function() {fn.apply(null, args);}, t);const cancelFn = function() {clearTimeout(timeoutId);};return cancelFn;
};
(4)复杂度分析:
时间复杂度:O(1)
空间复杂度:O(1)
虽然可取消函数本身的时间和空间复杂度是 O(1),但需要注意,作为参数传递的函数 fn
的时间复杂度可能会有所不同。
方法 2:使用布尔标志
(1)思路:
我们可以使用一个布尔变量来决定是否允许调用函数 fn
。
(2)算法:
- 将
isCancelled
初始化为false
的布尔变量以跟踪取消状态。 - 使用
setTimeout()
安排在t
毫秒后执行fn
,但仅在isCancelled
为false
时。 - 返回一个函数,该函数将
isCancelled
的值更改为true
,从而取消fn
的执行。如果在延迟到期之前调用该取消函数,它确保如果在延迟到期之前调用它,fn
将永远不会被调用。
虽然此方法确实防止了在调用取消函数后执行
fn
,但值得注意的是,当延迟结束时,setTimeout
回调仍然会执行。这意味着即使取消,函数仍然会在 JavaScript 事件循环队列中占用一个位置。因此,从计算效率的角度来看,这种方法可能略逊色于方法 1,后者完全取消了setTimeout
。
(3)实现:
/*** @param {Function} fn* @param {Array} args* @param {number} t* @return {Function}
*/
var cancellable = function(fn, args, t) {let isCancelled = false;setTimeout(() => {if(!isCancelled)fn(...args);}, t);return () => {isCancelled = true;};
};
(4)复杂度分析:
时间复杂度:O(1)
空间复杂度:O(1)
虽然可取消函数本身的时间和空间复杂度是 O(1),但需要注意,作为参数传递的函数fn
的时间复杂度可能会有所不同。
4、面试提示
-
你能解释在
setTimeout
回调中使用的apply
方法的作用吗?apply
方法用于使用提供的args
数组作为参数调用延迟执行的函数fn
。它允许我们将args
数组中的参数动态传递给fn
,以确保在最终执行fn
时传递正确的参数。此外,使用null
作为第一个参数的apply
允许我们在不指定特定上下文(this
值)的情况下调用函数。由于延迟执行的函数不依赖于特定的上下文,因此使用null
是合适的。 -
如何处理延迟函数需要特定上下文(
this
值)的执行情况?在延迟函数依赖特定上下文(
this
值)的情况下,可以使用bind
方法将所需的上下文绑定到fn
。这会创建一个具有指定上下文的新函数,然后您可以将绑定的函数传递给setTimeout
以进行延迟执行。 -
是否可以修改实现以支持在执行过程中动态更改延迟?
是的,可以改进可取消函数的实现以支持动态更改延迟。您可以修改实现以存储计时器
ID
,并在设置新延迟之前使用clearTimeout
来清除计时器。这样,您可以动态更改延迟。 -
有哪些适用于带延迟的可取消函数的潜在用例?
具有延迟的可取消函数在需要在一定延迟后执行某个操作的情况下非常有用,但在可能需要在执行前取消该操作的情况下。例如,在用户界面中的场景中,用户执行某个动作后,可能需要在一定延迟后显示通知。但是,如果用户执行了使通知无关紧要的不同动作,可以取消通知的计划显示。
另一个场景可能是在游戏环境中,需要在延迟后执行某个动作,但是中间的用户动作或游戏事件可能需要取消计划的动作。需要注意的是,这些用例与防抖或节流的情况不同,后者旨在控制函数调用的速率,而不是安排和取消可能的操作。
-
使用
setTimeout
来安排延迟执行的函数的执行的潜在缺点或限制是什么?一个限制是
setTimeout
不是精确的,可能受到系统负载等其他因素的影响。如果需要精确的定时,一些情况下会使用替代方法,如Web Workers
或Web
动画API
,但它们用途不同,不能总是直接替代setTimeout
。使用
performance.now()
方法可以实现更精确的时间控制,它提供了子毫秒分辨率的时间戳测量,但仍然不能保证函数会在指定的延迟后运行,因为 JavaScript 是单线程的。 -
是否可以修改可取消函数以支持在执行过程中可以动态更改的延迟?
是的,可以增强可取消功能以支持延迟的动态变化。您可以修改实现以存储超时
ID
,并在使用更新的延迟设置新的超时之前使用clearTimeout
。 -
你能解释一下“防抖”的概念吗?它与可取消的延迟功能有何关系?
防抖是一种编程实践,用于确保耗时的任务不会如此频繁地触发,这在处理用户输入事件等可能频繁而快速触发事件的情况下尤其有用。防抖的核心概念是在执行函数之前设置延迟,然后在延迟到期之前每次请求函数时重置该延迟。
虽然带有延迟的可取消函数与防抖有相似之处,因为两者都涉及可以取消的延迟函数执行,但它们并没有内在联系。可取消函数更适合动作或计算在执行前就被废弃的情况。另一方面,防抖通常不涉及显式创建可取消函数;相反,它直接在函数内清除并重置计时器。