本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第18期 | delay 带取消功能的延迟函数
前言
本文将通过剖析delay函数,明白延迟函数的实现原理,实现一个完善的delay函数,并理解取消功能的原理
环境
# 推荐克隆若川的项目,保证与文章同步
git clone https://github.com/lxchuan12/delay-analysis.git
# npm i -g yarn
cd delay-analysis/delay && yarn i
# 或者克隆官方项目
git clone https://github.com/sindresorhus/delay.git
# npm i -g yarn
cd delay && yarn i
使用
基本使用
//import delay from 'delay'
const delay = require('delay');
(async () => {
const res = await delay(1000, { value: 'test' });
console.log(res)
})();
失败请求
(async () => {
const res = await delay.reject(1000, { value: 'test' });
console.log(res)
})();
随机时间
(async () => {
try {
const res = await delay.reject(1000, { value: 'test', signal });
console.log('不会被触发')
} catch (e) {
console.log('被取消')
}
})();
提前取消
import delay from 'delay'
const abortController = new AbortController()
const signal = abortController.signal;
(async () => {
//提前取消
setTimeout(() => {
abortController.abort()
}, 500)
try {
const res = await delay(1000, { value: 'test', signal });
console.log('不会被触发')
} catch (e) {
console.log('被提前取消')
}
})();
实现
由浅入深去理解,挨个去实现之前我们使用到的功能,直至完善整个函数的功能
实现延迟
const delay = (ms) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('OK')
}, ms)
})
}
(async () => {
console.log(await delay(1000))
})();
实现传参和控制Promise的结果
增加参数的传递, 并根据参数判断结果
const delay = (ms, { value, willResolve }) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
willResolve ? resolve(value) : reject(value)
}, ms)
})
}
(async () => {
try {
const res = await delay(1000, { value: 'OK', willResolve: false })
console.log(res)
} catch (e) {
console.log('执行失败')
}
})();
实现随机触发
const randomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
const delay = (min, max, { value, willResolve }) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
willResolve ? resolve(value) : reject(value)
}, randomNumber(min, max))
})
}
(async () => {
const res = await delay(100, 2000, { value: 'OK', willResolve: true })
console.log(res)
})();
封装随机触发以及失败触发
通过高阶函数的调用形式封装为 delay.reject 以及 delay.range的形式调用, 本质还是通过函数对delay函数的再次调用
const randomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
const delay = ({ willResolve }) => (ms, { value }) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
willResolve ? resolve(value) : reject(value)
}, ms)
})
}
const createDelay = () => {
const instance = delay({ willResolve: true })
instance.reject = delay({ willResolve: false })
instance.range = (min, max, options) => instance(randomNumber(min, max), options)
return instance
}
const delayRun = createDelay();
(async () => {
try {
const res1 = await delayRun.reject(100, { value: 'OK' })
//const res1 = await delayRun.range(100,2000, { value: 'OK' })
console.log('不会被执行')
} catch (e) {
console.log(e)
}
})();
提前清除并执行
通过增加变量的形式,使得定时器能够被进行清除, 同时还需要理解宏任务中的 执行顺序,明白提前清除的核心
const randomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
const delay = ({ willResolve }) => (ms, { value }) => {
let timeoutId
let settled
const delayPromise = new Promise((resolve, reject) => {
settled = () => {
willResolve ? resolve(value) : reject(value)
}
timeoutId = setTimeout(settled, ms)
})
delayPromise.clear = () => {
clearTimeout(timeoutId)
timeoutId = null
settled()
}
return delayPromise
}
const createDelay = () => {
const instance = delay({ willResolve: true })
instance.reject = delay({ willResolve: false })
instance.range = (min, max, options) => instance(randomNumber(min, max), options)
return instance
}
const delayRun = createDelay();
(async () => {
//res1 为 Promise 且内部执行后 会在 宏任务中增加一个 1000ms 的任务
const res1 = delayRun(1000, { value: 'OK' })
//此时 这里的setTimeout也会给宏任务增加一个 100ms的任务,宏任务的优先级是根据时间以及先后顺序决定的
//此时clear会被先执行,所以达到提前的效果
setTimeout(() => {
res1.clear()
}, 100)
console.log(await res1)
})();
AbortController接口
AbortController接口 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求
它是实现取消请求的核心原理,使得Fetch(Promise) 进行提前终止达到需求
它在原型上具有signal 属性,它是一个AbortSignal对象实例, 可以用来监听 请求取消, 还具有一个abort() 方法, 用于取消请求,所以具体用法
在Fetch下进行请求取消:
//MDN官方例子
const controller = new AbortController();
const signal = controller.signal;
const url = "video.mp4";
const downloadBtn = document.querySelector(".download");
const abortBtn = document.querySelector(".abort");
downloadBtn.addEventListener("click", fetchVideo);
abortBtn.addEventListener("click", () => {
controller.abort();
console.log("Download aborted");
});
function fetchVideo() {
fetch(url, { signal })
.then((response) => {
console.log("Download complete", response);
})
.catch((err) => {
console.error(`Download error: ${err.message}`);
});
}
基于AbortController实现请求取消
Promise中 我们需要依赖于AbortController 中的 signal 属性,它继承了EventTarget接口,也意味着它可以进行 事件的监听 取消等操作,我们可以用它去监听 AbortController.abort()的操作
const abortController = new AbortController()
const signal = abortController.signal
const randomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
const delay = ({ willResolve }) => (ms, { value, signal }) => {
let settled
let timeoutId
let rejectFn
if (signal && signal.aborted) return Promise.reject('aborted is true') //意味着请求已经被终止了 直接返回
//监听触发事件 清空定时器
const signalListener = () => {
clearTimeout(timeoutId)
rejectFn(new Error('delay abort'))
}
//cleanSignal 用于初始化 signal监听
const cleanSignal = () => {
if (signal) {
signal.removeEventListener('abort', signalListener)
}
}
const delayPromise = new Promise((resolve, reject) => {
settled = () => {
cleanSignal()
willResolve ? resolve(value) : reject(value)
}
rejectFn = reject //接收rejct 用于改变promise的结果
timeoutId = setTimeout(settled, ms)
})
//用于增加 signal实例的 取消请求监听
if (signal) {
signal.addEventListener('abort', signalListener)
}
delayPromise.clear = () => {
clearTimeout(timeoutId)
timeoutId = null
settled()
}
return delayPromise
}
const createDelay = () => {
const instance = delay({ willResolve: true })
instance.reject = delay({ willResolve: false })
instance.range = (min, max, options) => instance(randomNumber(min, max), options)
return instance
}
const delayRun = createDelay();
(async () => {
try {
setTimeout(() => {
abortController.abort()
}, 500)
const res1 = await delayRun(1000, { value: 'OK', signal })
console.log('不会被执行')
} catch (e) {
console.log(e)
}
})();
完善函数实现
至此函数的实现基本完整了,最后增加支持用户对于定时器和清除定时器时的自定义操作,即 delay.createWithTimers的形式调用,这里和 reject 以及 range的封装差不多,只是将参数的传入放在不同位置
const abortController = new AbortController()
const signal = abortController.signal
const randomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
const delay = ({ setFn, clearFn, willResolve }) => (ms, { value, signal }) => {
let settled
let timeoutId
let rejectFn
//默认值
clearFn = clearFn || clearTimeout
setFn = setFn || setTimeout
if (signal && signal.aborted) return Promise.reject('aborted is true') //意味着请求已经被终止了 直接返回
//监听事件 触发时 清空定时器的触发
const signalListener = () => {
clearFn(timeoutId)
rejectFn(new Error('delay abort'))
}
//cleanSignal 用于初始化 signal状态
const cleanSignal = () => {
if (signal) {
signal.removeEventListener('abort', signalListener)
}
}
const delayPromise = new Promise((resolve, reject) => {
settled = () => {
cleanSignal()
willResolve ? resolve(value) : reject(value)
}
rejectFn = reject
timeoutId = setFn(settled, ms)
})
//用于增加 signal实例的 取消请求监听
if (signal) {
signal.addEventListener('abort', signalListener)
}
delayPromise.clear = () => {
clearFn(timeoutId)
timeoutId = null
settled()
}
return delayPromise
}
const createDelay = (setAndClear) => {
const instance = delay({ ...setAndClear, willResolve: true })
instance.reject = delay({ ...setAndClear, willResolve: false })
instance.range = (min, max, options) => instance(randomNumber(min, max), options)
return instance
}
const delayRun = createDelay();
delayRun.createWithTimers = createDelay; //不进行调用 此时还只是一个函数
(async () => {
const setFn = (settled, ms) => {
setTimeout(settled, ms)
console.log('自定义执行定时器时需要干的其他事情')
}
//返回Promise,再进行调用
const delayInstance = delayRun.createWithTimers({ setFn })
const res = await delayInstance(1000, { value: 'OK' })
console.log(res)
})();
Axios中的取消请求
Axios官方中文文档
最新的Axios 从 0.22版本开始支持了 AbortController API,我们可以通过和 Fetch请求例子中一样操作进行请求取消
const controller = new AbortController();
axios.get('/foo/bar', { signal: controller.signal })
.then(function(response) { //... });
// 取消请求
controller.abort()
总结
- 新的API
AbortController接口,用于取消Web请求 Delay函数 循序渐进的 实现理解, 异步下的延迟取消,做到快速手撸一个延迟函数- 高阶函数的封装理解,多种调用形式的实现