本文参加了由公众号@若川视野 ****发起的每周源码共读活动, 点击了解详情一起参与。
有时我们会遇到延迟一段时间再执行的场景,你一定能想到最简单的解决方案:setTimeout。但是如果在延迟的基础上增加像“一定范围内随机时间”,“提前触发”,“可以失败”,“可以取消”等需求时,就不是单单setTimeout就能搞定的。本文分析了delay的源码,带你一步一步的实现了一个功能完善的延迟函数。
1.学习准备工作
代码:
git clone https://github.com/lxchuan12/delay-analysis.git
cd delay-analysis/delay
yarn
文章:
文章: 面试官:请手写一个带取消功能的延迟函数,axios 取消功能的原理是什么
地址: https://juejin.cn/post/7042461373904715812#heading-24
文章:学习 axios 源码整体架构,打造属于自己的请求库
地址: https://juejin.cn/post/6844904019987529735#heading-26
源码共读群友(前舟小哥)文章:https://juejin.cn/post/7092588620007079949
2.学习一步一步实现delay
2.1 delay1-3
代码路径参见川哥代码examples\delay1-3
delay1-3的使用:
<body>
<script src="./index.js">
</script>
<script>
(async() => {
await delay1(1000);
console.log('输出这句');
})();
</script>
<script>
(async() => {
const result = await delay2(1000, { value: '我是若川' });
console.log('输出结果', result);
})();
</script>
<script>
(async() => {
try{
const result = await delay3(1000, { value: '我是若川', willResolve: false });
console.log('永远不会输出这句', result);
}
catch(err){
console.log('输出结果', err);
}
})();
</script>
</body>
delay1-3的实现:
const delay1 = (ms) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
}
const delay2 = (ms, { value } = {}) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(value);
}, ms);
});
}
const delay3 = (ms, {value, willResolve} = {}) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if(willResolve){
resolve(value);
}
else{
reject(value);
}
}, ms);
});
}
delay1接收参数ms,经过ms毫秒值后resolve;
delay2接收参数ms, 还要一个对象,这个对象的value属性是最后resolve的值;
delay3在第二个对象形式的参数中又加了willResolve参数,此参数传值并且为真值时resolve,否则reject。
运行效果如下图所示:
2.2 delay4
代码路径参见川哥代码 examples\delay4
delay4的使用:
<body>
<script src="./index.js">
</script>
<script>
(async() => {
try{
const result = await delay4.reject(1000, { value: '我是若川', willResolve: true });
console.log('永远不会输出这句');
}
catch(err){
console.log('输出结果', err);
}
const result2 = await delay4.range(10, 20000, { value: '我是range' });
console.log(result2);
})();
</script>
</body>
delay4的实现:
// 随机生成一个整数时间
const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);
// createDelay是一个箭头函数接受参数willResolve,返回一个函数
// 这个被返回的函数接受ms毫秒值和一个对象,对象value属性时要resolve的值 (这叫高阶函数)
const createDelay = ({willResolve}) => (ms, {value} = {}) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if(willResolve){
resolve(value);
}
else{
reject(value);
}
}, ms);
});
}
const createWithTimers = () => {
// delay是一个函数,在js中除了基本类型一切皆对象
const delay = createDelay({willResolve: true});
// 定义delay的reject方法,willResolve固定传值false
delay.reject = createDelay({willResolve: false});
// 定义range方法,此函数接受三个参数,前两个参数minimum, maximum传给randomInteger用于生成一个整型时间,
// 第三个参数是配置对象,包含value属性
delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
return delay;
}
const delay4 = createWithTimers();
delay4有了range方法可以在一定时间范围内随机获取到结果。运行结果如图:
过一段时间:
2.3 delay5
代码路径参见川哥代码 examples\delay5
delay5使用:
<body>
<script src="./index.js">
</script>
<script>
(async () => {
const delayedPromise = delay5(1000, {value: '我是若川'});
setTimeout(() => {
delayedPromise.clear();
}, 300);
// 300 milliseconds later
console.log(await delayedPromise);
//=> '我是若川'
})();
</script>
</body>
delay5的定义:
const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);
const createDelay = ({willResolve}) => (ms, {value} = {}) => {
// 保存setTimeout的id用于清除
let timeoutId;
// 持有判断应该resolve还是reject核心逻辑的函数引用
let settle;
const delayPromise = new Promise((resolve, reject) => {
//settle函数赋值
settle = () => {
if(willResolve){
resolve(value);
}
else{
reject(value);
}
}
// 记录setTimeout的id用于清除
timeoutId = setTimeout(settle, ms);
});
// Promise对象上定义clear方法
delayPromise.clear = () => {
// 清除定义
clearTimeout(timeoutId);
timeoutId = null;
// 调用核心逻辑
settle();
};
return delayPromise;
}
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 delay5 = createWithTimers();
delay5通过clear方法,可以取消之前定义的延时时间,让promise提前resolve或者reject。运行效果如图:
不到1秒就立刻输出了~
2.4 delay6
代码路径参见川哥代码 examples\delay6
delay6的使用:
<body>
<script src="./index.js">
</script>
<script>
(async () => {
// AbortController 参见 https://developer.mozilla.org/en-US/docs/Web/API/AbortController
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, 500);
try {
// 将abortController的signal属性传给delay6
await delay6(1000, {signal: abortController.signal});
} catch (error) {
// 500 milliseconds later
console.log(error.name)
//=> 'AbortError'
}
})();
</script>
</body>
delay6的定义:
// 和之前一样生成整型随机时间
const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);
// 创建错误对象的方法
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) {
return Promise.reject(createAbortError());
}
let timeoutId;
let settle;
// rejectFn持有对reject回调方法的引用
let rejectFn;
const signalListener = () => {
clearTimeout(timeoutId);
rejectFn(createAbortError());
}
// 事件清除函数
const cleanup = () => {
if (signal) {
// 不在监听signal的abort事件
signal.removeEventListener('abort', signalListener);
}
};
const delayPromise = new Promise((resolve, reject) => {
// settle方法中有变化
settle = () => {
cleanup();
if (willResolve) {
resolve(value);
} else {
reject(value);
}
};
// 持有reject函数的引用
rejectFn = reject;
timeoutId = setTimeout(settle, ms);
});
if (signal) {
// 如果传signal方法了则监听abort事件,并且只监听一次
signal.addEventListener('abort', signalListener, {
once: true
});
}
delayPromise.clear = () => {
clearTimeout(timeoutId);
timeoutId = null;
settle();
};
return delayPromise;
}
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 delay6 = createWithTimers();
delay6主要使用了AbortContoler, AbortContoler实例上会有一个signal属性,当AbortContoler实例调用abort方法时,会触发signal的abort事件;把signal传给delay函数,并在其中监听abort事件是否被触发,如果被触发了,则定义一个错误对象,并reject这个错误对象
代码运行效果如下图:
2.5 delay7
代码路径参见川哥代码 examples\delay7
delay7使用:(在川哥原来代码基础上稍有改动)
<body>
<script src="./index.js">
</script>
<script>
const mySetTimeout = (settle, ms) => {
console.log('自定义setTimeout')
setTimeout(settle, ms)
}
// 注意createWithTimers方法接收一个对象
const customDelay = delay7.createWithTimers({clearTimeout, setTimeout: mySetTimeout});
(async() => {
const result = await customDelay(100, {value: '我是若川'});
// Executed after 100 milliseconds
console.log(result);
//=> '我是若川'
})();
</script>
</body>
delay7的定义:
const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);
const createAbortError = () => {
const error = new Error('Delay aborted');
error.name = 'AbortError';
return error;
};
// createDelay 接收参数时候使用了解构,并为参数定义了别名 (defaultClear, set)
const createDelay = ({clearTimeout: defaultClear, setTimeout: set, willResolve}) => (ms, {value, signal} = {}) => {
if (signal && signal.aborted) {
return Promise.reject(createAbortError());
}
let timeoutId;
let settle;
let rejectFn;
// 如果传了defaultClear则使用,否则使用原生的
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;
// 如果传了set则使用,否则使用原生的
timeoutId = (set || setTimeout)(settle, ms);
});
if (signal) {
signal.addEventListener('abort', signalListener, {once: true});
}
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;
}
// 创建delay7
const delay7 = createWithTimers();
// 棒一个方法,可以接收自定义的setTimeout和clearTimeout
delay7.createWithTimers = createWithTimers;
deay7可以传递 clearTimeout, setTimeout ,这就是delay7的最终实现。运行效果如下图所示:
3.学习收获
1.了解delay的实现思路
2.了解AbortController基本知识
3.了解高阶函数在源码中的用法
4.了解解构赋值并起别名在源码中的用法
5.了解axios取消请求的实现原理
学完本期源码,您可以思考如下问题:
1.如何使Promise延迟执行?
2.如何随机生成一定范围内的整数?
3.你在项目中如何使用高阶函数和闭包的?
4.是否了解AbortController?
5.如何取消axios请求?
6.谈谈axios取消请求的原理?
7.简述delay的实现思路?