实现一个带取消功能的延迟函数,取消的原理是什么?

168 阅读5分钟

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

这是源码共读的第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官方中文文档 最新的Axios0.22版本开始支持了 AbortController API,我们可以通过和 Fetch请求例子中一样操作进行请求取消

const controller = new AbortController(); 
axios.get('/foo/bar', { signal: controller.signal })
    .then(function(response) { //... }); 
// 取消请求 
controller.abort()

总结

  1. 新的API AbortController 接口,用于取消Web请求
  2. Delay函数 循序渐进的 实现理解, 异步下的延迟取消,做到快速手撸一个延迟函数
  3. 高阶函数的封装理解,多种调用形式的实现