【若川视野 x 源码共读】 手写 delay,还要有这些功能哦

1,151 阅读7分钟

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

歌德说过:读一本好书,就是在和高尚的人谈话。
同理,读优秀的开源项目的源码,就是在和优秀的大佬交流,是站在巨人的肩膀上学习 —— 那么今天如何通过读源码来完成这道 手写 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能满足这种场景

(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)创造的,将delayrejectrange分别单独地封装为一个函数,并且给他加到这个对象中。
我们

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 返回一个 promiseJavaScript 通常并没有“中止” 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 方法

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

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. 学习资源

🌊如果有所帮助,欢迎点赞关注,一起进步⛵