本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
歌德说过:读一本好书,就是在和高尚的人谈话。
同理,读优秀的开源项目的源码,就是在和优秀的大佬交流,是站在巨人的肩膀上学习 —— 那么今天如何通过读源码来完成这道 手写 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 laterbaz();})();
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 millisecondsconsole.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 laterconsole.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 laterconsole.log(await delayedPromise);//=> 'Done'})();
这里没有等到一开始设定的 1000ms 后返回结果,而是 500ms。也就是清除了原来的定时器,直接提前触发了
2.5.2 code
修改 createDelay 方法
用变量settle存储回调函数,以及定时器id timeoutId,当需要提前触发时就清除原先定时器,而直接调用settle
const createDelay = ({willResolve}) => (ms, {value} = {}) => {let timeoutId;let settle;//返回这个Promiseconst 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 laterconsole.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} = {}) => {// 传参再接收一个signalif (signal && signal.aborted) { //如果存在signal,并且其中止标记aborted为truereturn 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);}};//返回这个Promiseconst 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 millisecondsconsole.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为truereturn Promise.reject(createAbortError());//直接返回}let timeoutId; //定时器idlet 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);}};//返回这个Promiseconst 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
🌊如果有所帮助,欢迎点赞关注,一起进步⛵
