本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
歌德说过:读一本好书,就是在和高尚的人谈话。
同理,读优秀的开源项目的源码,就是在和优秀的大佬交流,是站在巨人的肩膀上学习 —— 那么今天如何通过读源码来完成这道 手写 delay 的题目呢
1. 前言
今天我们来看 delay 这个库
1.1 这个库,是干啥的
Delay a promise a specified amount of time
1.2 你能学到
- 面试中可能考到的手写一个 delay 方法
- “能失败”
- 随机时间结束
- 提前触发
- 取消
- 自定义
clearTimeout
等
- 如何用
AbortController
实现其中的取消功能
2. 实现
现在开始带你一步一步地”抄”源码~每一步都新增一个功能~ 建议借助 git
看清每一步的”进化”~
2.1 最简易版本
2.1.1 场景
首先我们需要一个delay
能满足这种场景
(async () => {
bar();
await delay(100);
// Executed 100 milliseconds later
baz();
})();
2.1.2 code
这很简单,借助setTimeout
+Promise
轻松实现:
const delay = (ms) =>{
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve()
}, ms)
})
}
2.2 传入value作为结果
2.2.1 场景
(async() => {
const result = await delay(100, {value: '🦄'});
// Executed after 100 milliseconds
console.log(result);
//=> '🦄'
})();
2.2.2 code
const delay = (ms,{ value } = {}) => { // 解构赋值传参
return new Promise((resove, reject) => {
setTimeout(()=>{
resolve(value)
}, ms)
})
}
2.3 还要”能失败”
2.3.1 场景
前面的Promise
始终为成功,这就失去了其一个重要的作用,所以我们还要使其能够失败
(async () => {
try {
await delay.reject(100, {value: new Error('🦄')});
console.log('This is never executed'); //这里不会被执行,因为已经犯了错被逮走了🥴
} catch (error) {
// 100 milliseconds later
console.log(error);
//=> [Error: 🦄]
}
})();
2.3.2 code
传入参数willResolve
来决定其成功还是失败
const delay = (ms, {value, willResolve} = {}) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if(willResolve){
resolve(value);
}
else{
reject(value);
}
}, ms);
});
}
2.4 一定范围内随机延迟
2.4.1 场景
我们可能不想延迟时间是写死的
(async() => {
const res = await delay.range(50, 50000, { value: '⛵' });
console.log(res);//50ms~50000ms后输出⛵
})();
2.4.2 code
这里开始采用源码的结构了
新增 randomInteger 方法
该方法用于传入上下界限,返回一个在区间中的随机数
const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);
Math.random() 函数返回一个浮点数, 伪随机数在范围从0到小于1
新增 createDelay 方法
该方法返回一个箭头函数,该箭头函数返回一个Promise
这里基本就是照搬前面版本的delay
具体实现代码,但是形式有所不同
const createDelay = ({willResolve}) => (ms, {value} = {}) => { //返回一个箭头函数
return new Promise((relove, reject) => {
setTimeout(() => {
if(willResolve){
relove(value);
}
else{
reject(value);
}
}, ms);
});
}
新增 createWithTimers 方法
然后就是将前面的整合起来了,返回一个Promise对象(createDelay)创造的,将delay
、reject
和range
分别单独地封装为一个函数,并且给他加到这个对象中。
我们
const createWithTimers = () => {
const delay = createDelay({willResolve: true});
delay.reject = createDelay({willResolve: false});
delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
return delay;
}
const delay = createWithTimers();
2.5 提前触发
2.5.1 场景
(async () => {
const delayedPromise = delay(1000, {value: 'Done'});
setTimeout(() => {
delayedPromise.clear();
}, 500);
// 500 milliseconds later
console.log(await delayedPromise);
//=> 'Done'
})();
这里没有等到一开始设定的 1000ms 后返回结果,而是 500ms。也就是清除了原来的定时器,直接提前触发了
2.5.2 code
修改 createDelay 方法
用变量settle
存储回调函数,以及定时器id timeoutId
,当需要提前触发时就清除原先定时器,而直接调用settle
const createDelay = ({willResolve}) => (ms, {value} = {}) => {
let timeoutId;
let settle;
//返回这个Promise
const delayPromise = new Promise((resolve, reject) => {
settle = () => {
if(willResolve){
resolve(value);
}
else{
reject(value);
}
}
timeoutId = setTimeout(settle, ms);
});
delayPromise.clear = () => {
clearTimeout(timeoutId);
timeoutId = null;
settle();
};
return delayPromise;
}
2.6 取消功能
2.6.1 场景
正如我们所知道的,fetch
返回一个 promise
。JavaScript
通常并没有“中止” promise
的概念。那么我们怎样才能取消一个正在执行的 fetch
呢?例如,如果用户在我们网站上的操作表明不再需要 fetch。
为此有一个特殊的内建对象:AbortController
。它不仅可以中止 fetch
,还可以中止其他异步任务。
⭐AbortController
- 它具有单个方法 abort(),
- 和单个属性 signal,我们可以在这个属性上设置事件监听器。
具体用法可以查看该文档
(async () => {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, 500);
try {
await delay(1000, {signal: abortController.signal});
} catch (error) {
// 500 milliseconds later
console.log(error.name)
//=> 'AbortError'
}
})();
2.6.2 code
新增 createAbortError 方法
创建一个Error
用于告知delay
给中止了
const createAbortError = () => {
const error = new Error('Delay aborted');
error.name = 'AbortError';
return error;
};
修改 createDelay 方法
const createDelay = ({willResolve}) => (ms, {value, signal} = {}) => {// 传参再接收一个signal
if (signal && signal.aborted) { //如果存在signal,并且其中止标记aborted为true
return Promise.reject(createAbortError());
}
let timeoutId;
let settle;
let rejectFn;
const signalListener = () => { //监听 abort 事件的回调
clearTimeout(timeoutId); // 清空原先的定时器
rejectFn(createAbortError()); //直接执行错误时的回调,参数为createAbortError返回的error
}
const cleanup = () => {
if (signal) {
//移除监听 abort 事件
signal.removeEventListener('abort', signalListener);
}
};
//返回这个Promise
const delayPromise = new Promise((resolve, reject) => {
settle = () => {
cleanup(); // **执行的时候** 就可以移除监听 abort 事件
if (willResolve) {
resolve(value);
} else {
reject(value);
}
};
rejectFn = reject;
timeoutId = setTimeout(settle, ms);
});
if (signal) { //有监听标志的话就要监听abort事件
signal.addEventListener('abort', signalListener, {once: true});
}
delayPromise.clear = () => {
clearTimeout(timeoutId);
timeoutId = null;
settle();
};
return delayPromise;
}
2.7 自定义clearTimeout 和 setTimeout 函数
2.7.1 场景
为了防止收到 fake-timers 这些库的影响,我们可以自定义这两个方法,达到这样的效果
const customDelay = delay.createWithTimers({clearTimeout, setTimeout});
(async() => {
const result = await customDelay(100, {value: '🦄'});
// Executed after 100 milliseconds
console.log(result);
//=> '🦄'
})();
2.7.2 code
修改 createDelay 方法
接收自定义的clearTimeout
、setTimeout
两个参数来替代前面的版本中的这两个方法,没有传入的话,就使用默认方法
const createDelay = ({clearTimeout: defaultClear, setTimeout: set, willResolve}) => (ms, {value, signal} = {}) => {
if (signal && signal.aborted) {
return Promise.reject(createAbortError());
}
let timeoutId;
let settle;
let rejectFn;
const clear = defaultClear || clearTimeout; //*
const signalListener = () => {
clear(timeoutId);
rejectFn(createAbortError());
}
const cleanup = () => {
if (signal) {
signal.removeEventListener('abort', signalListener);
}
};
const delayPromise = new Promise((resolve, reject) => {
settle = () => {
cleanup(); //*
if (willResolve) {
resolve(value);
} else {
reject(value);
}
};
rejectFn = reject;
timeoutId = (set || setTimeout)(settle, ms); //*
});
if (signal) {
signal.addEventListener('abort', signalListener, {once: true});
}
delayPromise.clear = () => {
clear(timeoutId); //*
timeoutId = null;
settle();
};
return delayPromise;
}
修改 createWithTimers 方法
接收自定义的定时器相关的两个操作,展开后传入createDelay
const createWithTimers = clearAndSet => {
const delay = createDelay({...clearAndSet, willResolve: true});
delay.reject = createDelay({...clearAndSet, willResolve: false});
delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
return delay;
};
4. 总结 & 收获
- 从0开始,一步一步地实现了多功能的
delay
方法 - 70多行,十分精妙,一个
delay
方法能有 几百个start ⭐,确实不一般
最后放一下整个代码,我也把前面的注释加上了,以供再梳理一遍
'use strict';
//该方法用于传入上下界限,返回一个在区间中的随机数
const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);
const createAbortError = () => {
//创建一个Error用于告知delay给中止了
const error = new Error('Delay aborted');
error.name = 'AbortError';
return error;
};
//该方法返回一个函数,该函数返回一个Promise
//该方法接收自定义定时器相关操作、返回成功还是拒绝的标记
//返回的函数接收延迟时间、返回的value、是否可能需要取消
const createDelay = ({clearTimeout: defaultClear, setTimeout: set, willResolve}) => (ms, {value, signal} = {}) => {
if (signal && signal.aborted) { //如果存在signal,并且其中止标记aborted为true
return Promise.reject(createAbortError());//直接返回
}
let timeoutId; //定时器id
let settle; //延迟时间结束后的回调函数
let rejectFn; //拒绝的回调
const clear = defaultClear || clearTimeout;
const signalListener = () => { //监听 abort 事件的回调
clear(timeoutId); // 清空原先的定时器
rejectFn(createAbortError()); //直接执行错误时的回调,参数为createAbortError返回的error
};
const cleanup = () => {
if (signal) {
//移除监听 abort 事件
signal.removeEventListener('abort', signalListener);
}
};
//返回这个Promise
const delayPromise = new Promise((resolve, reject) => {
settle = () => {
cleanup(); // **执行的时候** 就可以移除监听 abort 事件了
//判断返回成功还是失败
if (willResolve) {
resolve(value);
} else {
reject(value);
}
};
rejectFn = reject;
timeoutId = (set || setTimeout)(settle, ms);
});
if (signal) { //有监听标志的话就要监听abort事件
signal.addEventListener('abort', signalListener, {once: true});
}
//给返回的Promise对象加上清除定时器立即触发的方法
delayPromise.clear = () => {
clear(timeoutId);//清除前面的定时器
timeoutId = null;
settle();//直接触发回调函数
};
return delayPromise;
};
//接收自定义的定时器相关的两个操作
const createWithTimers = clearAndSet => {
const delay = createDelay({...clearAndSet, willResolve: true});
delay.reject = createDelay({...clearAndSet, willResolve: false});
delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
return delay;//返回添加了reject、range功能delay方法
};
const delay = createWithTimers();
delay.createWithTimers = createWithTimers; //再将其创造函数绑回自己身上,具体作用我也不知道是啥,欢迎评论区一起讨论~
//导出
module.exports = delay;
module.exports.default = delay;
5. 学习资源
- AbortController
- AbortController 兼容性
- yet-another-abortcontroller-polyfill
🌊如果有所帮助,欢迎点赞关注,一起进步⛵