本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

歌德说过:读一本好书,就是在和高尚的人谈话。
同理,读优秀的开源项目的源码,就是在和优秀的大佬交流,是站在巨人的肩膀上学习 —— 那么今天如何通过读源码来完成这道 手写 delay 的题目呢

1. 前言

今天我们来看 delay 这个库

1.1 这个库,是干啥的

Delay a promise a specified amount of time

Promise延迟一定的时间

1.2 你能学到

  • 面试中可能考到的手写一个 delay 方法
    • “能失败”
    • 随机时间结束
    • 提前触发
    • 取消
    • 自定义clearTimeout
  • 如何用 AbortController实现其中的取消功能

2. 实现

现在开始带你一步一步地”抄”源码~每一步都新增一个功能~ 建议借助 git 看清每一步的”进化”~

2.1 最简易版本

2.1.1 场景

首先我们需要一个delay能满足这种场景

  1. (async () => {
  2. bar();
  3. await delay(100);
  4. // Executed 100 milliseconds later
  5. baz();
  6. })();

2.1.2 code

这很简单,借助setTimeout+Promise轻松实现:

  1. const delay = (ms) =>{
  2. return new Promise((resolve, reject)=>{
  3. setTimeout(()=>{
  4. resolve()
  5. }, ms)
  6. })
  7. }

2.2 传入value作为结果

2.2.1 场景

  1. (async() => {
  2. const result = await delay(100, {value: '🦄'});
  3. // Executed after 100 milliseconds
  4. console.log(result);
  5. //=> '🦄'
  6. })();

2.2.2 code

  1. const delay = (ms,{ value } = {}) => { // 解构赋值传参
  2. return new Promise((resove, reject) => {
  3. setTimeout(()=>{
  4. resolve(value)
  5. }, ms)
  6. })
  7. }

2.3 还要”能失败”

2.3.1 场景

前面的Promise始终为成功,这就失去了其一个重要的作用,所以我们还要使其能够失败

  1. (async () => {
  2. try {
  3. await delay.reject(100, {value: new Error('🦄')});
  4. console.log('This is never executed'); //这里不会被执行,因为已经犯了错被逮走了🥴
  5. } catch (error) {
  6. // 100 milliseconds later
  7. console.log(error);
  8. //=> [Error: 🦄]
  9. }
  10. })();

2.3.2 code

传入参数willResolve来决定其成功还是失败

  1. const delay = (ms, {value, willResolve} = {}) => {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. if(willResolve){
  5. resolve(value);
  6. }
  7. else{
  8. reject(value);
  9. }
  10. }, ms);
  11. });
  12. }

2.4 一定范围内随机延迟

2.4.1 场景

我们可能不想延迟时间是写死的

  1. (async() => {
  2. const res = await delay.range(50, 50000, { value: '⛵' });
  3. console.log(res);//50ms~50000ms后输出⛵
  4. })();

2.4.2 code

这里开始采用源码的结构了

新增 randomInteger 方法

该方法用于传入上下界限,返回一个在区间中的随机数

  1. const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);

Math.random() 函数返回一个浮点数, 伪随机数在范围从0到小于1

新增 createDelay 方法

该方法返回一个箭头函数,该箭头函数返回一个Promise
这里基本就是照搬前面版本的delay具体实现代码,但是形式有所不同

  1. const createDelay = ({willResolve}) => (ms, {value} = {}) => { //返回一个箭头函数
  2. return new Promise((relove, reject) => {
  3. setTimeout(() => {
  4. if(willResolve){
  5. relove(value);
  6. }
  7. else{
  8. reject(value);
  9. }
  10. }, ms);
  11. });
  12. }

新增 createWithTimers 方法

然后就是将前面的整合起来了,返回一个Promise对象(createDelay)创造的,将delayrejectrange分别单独地封装为一个函数,并且给他加到这个对象中。
我们

  1. const createWithTimers = () => {
  2. const delay = createDelay({willResolve: true});
  3. delay.reject = createDelay({willResolve: false});
  4. delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
  5. return delay;
  6. }
  7. const delay = createWithTimers();

2.5 提前触发

2.5.1 场景

  1. (async () => {
  2. const delayedPromise = delay(1000, {value: 'Done'});
  3. setTimeout(() => {
  4. delayedPromise.clear();
  5. }, 500);
  6. // 500 milliseconds later
  7. console.log(await delayedPromise);
  8. //=> 'Done'
  9. })();

这里没有等到一开始设定的 1000ms 后返回结果,而是 500ms。也就是清除了原来的定时器,直接提前触发了

2.5.2 code

修改 createDelay 方法

用变量settle存储回调函数,以及定时器id timeoutId,当需要提前触发时就清除原先定时器,而直接调用settle

  1. const createDelay = ({willResolve}) => (ms, {value} = {}) => {
  2. let timeoutId;
  3. let settle;
  4. //返回这个Promise
  5. const delayPromise = new Promise((resolve, reject) => {
  6. settle = () => {
  7. if(willResolve){
  8. resolve(value);
  9. }
  10. else{
  11. reject(value);
  12. }
  13. }
  14. timeoutId = setTimeout(settle, ms);
  15. });
  16. delayPromise.clear = () => {
  17. clearTimeout(timeoutId);
  18. timeoutId = null;
  19. settle();
  20. };
  21. return delayPromise;
  22. }

2.6 取消功能

2.6.1 场景

正如我们所知道的,fetch返回一个 promiseJavaScript通常并没有“中止” promise的概念。那么我们怎样才能取消一个正在执行的 fetch呢?例如,如果用户在我们网站上的操作表明不再需要 fetch。
为此有一个特殊的内建对象:AbortController。它不仅可以中止 fetch,还可以中止其他异步任务。

AbortController

  • 它具有单个方法 abort(),
  • 和单个属性 signal,我们可以在这个属性上设置事件监听器。

具体用法可以查看该文档

  1. (async () => {
  2. const abortController = new AbortController();
  3. setTimeout(() => {
  4. abortController.abort();
  5. }, 500);
  6. try {
  7. await delay(1000, {signal: abortController.signal});
  8. } catch (error) {
  9. // 500 milliseconds later
  10. console.log(error.name)
  11. //=> 'AbortError'
  12. }
  13. })();

2.6.2 code

新增 createAbortError 方法

创建一个Error用于告知delay给中止了

  1. const createAbortError = () => {
  2. const error = new Error('Delay aborted');
  3. error.name = 'AbortError';
  4. return error;
  5. };

修改 createDelay 方法

  1. const createDelay = ({willResolve}) => (ms, {value, signal} = {}) => {// 传参再接收一个signal
  2. if (signal && signal.aborted) { //如果存在signal,并且其中止标记aborted为true
  3. return Promise.reject(createAbortError());
  4. }
  5. let timeoutId;
  6. let settle;
  7. let rejectFn;
  8. const signalListener = () => { //监听 abort 事件的回调
  9. clearTimeout(timeoutId); // 清空原先的定时器
  10. rejectFn(createAbortError()); //直接执行错误时的回调,参数为createAbortError返回的error
  11. }
  12. const cleanup = () => {
  13. if (signal) {
  14. //移除监听 abort 事件
  15. signal.removeEventListener('abort', signalListener);
  16. }
  17. };
  18. //返回这个Promise
  19. const delayPromise = new Promise((resolve, reject) => {
  20. settle = () => {
  21. cleanup(); // **执行的时候** 就可以移除监听 abort 事件
  22. if (willResolve) {
  23. resolve(value);
  24. } else {
  25. reject(value);
  26. }
  27. };
  28. rejectFn = reject;
  29. timeoutId = setTimeout(settle, ms);
  30. });
  31. if (signal) { //有监听标志的话就要监听abort事件
  32. signal.addEventListener('abort', signalListener, {once: true});
  33. }
  34. delayPromise.clear = () => {
  35. clearTimeout(timeoutId);
  36. timeoutId = null;
  37. settle();
  38. };
  39. return delayPromise;
  40. }

2.7 自定义clearTimeout 和 setTimeout 函数

2.7.1 场景

为了防止收到 fake-timers 这些库的影响,我们可以自定义这两个方法,达到这样的效果

  1. const customDelay = delay.createWithTimers({clearTimeout, setTimeout});
  2. (async() => {
  3. const result = await customDelay(100, {value: '🦄'});
  4. // Executed after 100 milliseconds
  5. console.log(result);
  6. //=> '🦄'
  7. })();

2.7.2 code

修改 createDelay 方法

接收自定义的clearTimeoutsetTimeout两个参数来替代前面的版本中的这两个方法,没有传入的话,就使用默认方法

  1. const createDelay = ({clearTimeout: defaultClear, setTimeout: set, willResolve}) => (ms, {value, signal} = {}) => {
  2. if (signal && signal.aborted) {
  3. return Promise.reject(createAbortError());
  4. }
  5. let timeoutId;
  6. let settle;
  7. let rejectFn;
  8. const clear = defaultClear || clearTimeout; //*
  9. const signalListener = () => {
  10. clear(timeoutId);
  11. rejectFn(createAbortError());
  12. }
  13. const cleanup = () => {
  14. if (signal) {
  15. signal.removeEventListener('abort', signalListener);
  16. }
  17. };
  18. const delayPromise = new Promise((resolve, reject) => {
  19. settle = () => {
  20. cleanup(); //*
  21. if (willResolve) {
  22. resolve(value);
  23. } else {
  24. reject(value);
  25. }
  26. };
  27. rejectFn = reject;
  28. timeoutId = (set || setTimeout)(settle, ms); //*
  29. });
  30. if (signal) {
  31. signal.addEventListener('abort', signalListener, {once: true});
  32. }
  33. delayPromise.clear = () => {
  34. clear(timeoutId); //*
  35. timeoutId = null;
  36. settle();
  37. };
  38. return delayPromise;
  39. }

修改 createWithTimers 方法

接收自定义的定时器相关的两个操作,展开后传入createDelay

  1. const createWithTimers = clearAndSet => {
  2. const delay = createDelay({...clearAndSet, willResolve: true});
  3. delay.reject = createDelay({...clearAndSet, willResolve: false});
  4. delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
  5. return delay;
  6. };

4. 总结 & 收获

  • 从0开始,一步一步地实现了多功能的delay方法
  • 70多行,十分精妙,一个delay方法能有 几百个start ⭐,确实不一般

最后放一下整个代码,我也把前面的注释加上了,以供再梳理一遍

  1. 'use strict';
  2. //该方法用于传入上下界限,返回一个在区间中的随机数
  3. const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);
  4. const createAbortError = () => {
  5. //创建一个Error用于告知delay给中止了
  6. const error = new Error('Delay aborted');
  7. error.name = 'AbortError';
  8. return error;
  9. };
  10. //该方法返回一个函数,该函数返回一个Promise
  11. //该方法接收自定义定时器相关操作、返回成功还是拒绝的标记
  12. //返回的函数接收延迟时间、返回的value、是否可能需要取消
  13. const createDelay = ({clearTimeout: defaultClear, setTimeout: set, willResolve}) => (ms, {value, signal} = {}) => {
  14. if (signal && signal.aborted) { //如果存在signal,并且其中止标记aborted为true
  15. return Promise.reject(createAbortError());//直接返回
  16. }
  17. let timeoutId; //定时器id
  18. let settle; //延迟时间结束后的回调函数
  19. let rejectFn; //拒绝的回调
  20. const clear = defaultClear || clearTimeout;
  21. const signalListener = () => { //监听 abort 事件的回调
  22. clear(timeoutId); // 清空原先的定时器
  23. rejectFn(createAbortError()); //直接执行错误时的回调,参数为createAbortError返回的error
  24. };
  25. const cleanup = () => {
  26. if (signal) {
  27. //移除监听 abort 事件
  28. signal.removeEventListener('abort', signalListener);
  29. }
  30. };
  31. //返回这个Promise
  32. const delayPromise = new Promise((resolve, reject) => {
  33. settle = () => {
  34. cleanup(); // **执行的时候** 就可以移除监听 abort 事件了
  35. //判断返回成功还是失败
  36. if (willResolve) {
  37. resolve(value);
  38. } else {
  39. reject(value);
  40. }
  41. };
  42. rejectFn = reject;
  43. timeoutId = (set || setTimeout)(settle, ms);
  44. });
  45. if (signal) { //有监听标志的话就要监听abort事件
  46. signal.addEventListener('abort', signalListener, {once: true});
  47. }
  48. //给返回的Promise对象加上清除定时器立即触发的方法
  49. delayPromise.clear = () => {
  50. clear(timeoutId);//清除前面的定时器
  51. timeoutId = null;
  52. settle();//直接触发回调函数
  53. };
  54. return delayPromise;
  55. };
  56. //接收自定义的定时器相关的两个操作
  57. const createWithTimers = clearAndSet => {
  58. const delay = createDelay({...clearAndSet, willResolve: true});
  59. delay.reject = createDelay({...clearAndSet, willResolve: false});
  60. delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
  61. return delay;//返回添加了reject、range功能delay方法
  62. };
  63. const delay = createWithTimers();
  64. delay.createWithTimers = createWithTimers; //再将其创造函数绑回自己身上,具体作用我也不知道是啥,欢迎评论区一起讨论~
  65. //导出
  66. module.exports = delay;
  67. module.exports.default = delay;

5. 学习资源