面试官:请手写一个带取消功能的延迟函数,axios 取消功能的原理是什么

11,006 阅读7分钟

1. 前言

大家好,我是若川为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学。我倾力组织了每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与

想学源码,极力推荐关注我写的专栏(目前1.9K人关注)《学习源码整体架构系列》 包含jQueryunderscorelodashvuexsentryaxiosreduxkoavue-devtoolsvuex4koa-composevue 3.2 发布vue-thiscreate-vue玩具vite等20余篇源码文章。

本文仓库 https://github.com/lxchuan12/delay-analysis.git,求个star^_^

源码共读活动 每周一期,已进行到17期。于是搜寻各种值得我们学习,且代码行数不多的源码。delay 主文件仅70多行,非常值得我们学习。

阅读本文,你将学到:

1. 学会如何实现一个比较完善的 delay 函数
2. 学会使用 AbortController 实现取消功能
3. 学会面试常考 axios 取消功能实现
4. 等等

2. 环境准备

# 推荐克隆我的项目,保证与文章同步
git clone https://github.com/lxchuan12/delay-analysis.git
# npm i -g yarn
cd delay-analysis/delay && yarn i
# VSCode 直接打开当前项目
# code .
# 我写的例子都在 examples 这个文件夹中,可以启动服务本地查看调试
# 在 delay-analysis 目录下
npx http-server examples
# 打开 http://localhost:8080

# 或者克隆官方项目
git clone https://github.com/sindresorhus/delay.git
# npm i -g yarn
cd delay && yarn i
# VSCode 直接打开当前项目
# code .

3. delay

我们从零开始来实现一个比较完善的 delay 函数

3.1 第一版 简版延迟

要完成这样一个延迟函数。

3.1.1 使用

(async() => {
    await delay1(1000);
    console.log('输出这句');
})();

3.1.2 实现

PromisesetTimeout 结合实现,我们都很容易实现以下代码。

const delay1 = (ms) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, ms);
    });
}

我们要传递结果。

3.2 第二版 传递 value 参数作为结果

3.2.1 使用

(async() => {
    const result = await delay2(1000, { value: '我是若川' });
    console.log('输出结果', result);
})();

我们也很容易实现如下代码。传递 value 最后作为结果返回。

3.2.2 实现

因此我们实现也很容易实现如下第二版。

const delay2 = (ms, { value } = {}) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(value);
        }, ms);
    });
}

这样写,Promise 永远是成功。我们也需要失败。这时我们定义个参数 willResolve 来定义。

3.3 第三版 willResolve 参数决定成功还是失败。

3.3.1 使用

(async() => {
    try{
        const result = await delay3(1000, { value: '我是若川', willResolve: false });
        console.log('永远不会输出这句');
    }
    catch(err){
        console.log('输出结果', err);
    }
})();

3.3.2 实现

加个 willResolve 参数决定成功还是失败。于是我们有了如下实现。

const delay3 = (ms, {value, willResolve} = {}) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(willResolve){
                resolve(value);
            }
            else{
                reject(value);
            }
        }, ms);
    });
}

3.4 第四版 一定时间范围内随机获得结果

延时器的毫秒数是写死的。我们希望能够在一定时间范围内随机获取到结果。

3.4.1 使用

(async() => {
    try{
        const result = await delay4.reject(1000, { value: '我是若川', willResolve: false });
        console.log('永远不会输出这句');
    }
    catch(err){
        console.log('输出结果', err);
    }

    const result2 = await delay4.range(10, 20000, { value: '我是若川,range' });
    console.log('输出结果', result2);
})();

3.4.2 实现

我们把成功 delay 和失败 reject 封装成一个函数,随机 range 单独封装成一个函数。

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

const createDelay = ({willResolve}) => (ms, {value} = {}) => {
    return new Promise((relove, reject) => {
        setTimeout(() => {
            if(willResolve){
                relove(value);
            }
            else{
                reject(value);
            }
        }, ms);
    });
}

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 delay4 = createWithTimers();

实现到这里,相对比较完善了。但我们可能有需要提前结束。

3.5 第五版 提前清除

3.5.1 使用

(async () => {
    const delayedPromise = delay5(1000, {value: '我是若川'});

    setTimeout(() => {
        delayedPromise.clear();
    }, 300);

    // 300 milliseconds later
    console.log(await delayedPromise);
    //=> '我是若川'
})();

3.5.2 实现

声明 settle变量,封装 settle 函数,在调用 delayPromise.clear 时清除定时器。于是我们可以得到如下第五版的代码。

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

const createDelay = ({willResolve}) => (ms, {value} = {}) => {
    let timeoutId;
    let settle;
    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;
}

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();

3.6 第六版 取消功能

我们查阅资料可以知道有 AbortController 可以实现取消功能。

caniuse AbortController

npm abort-controller

mdn AbortController

fetch-abort

fetch#aborting-requests

yet-another-abortcontroller-polyfill

3.6.1 使用

(async () => {
    const abortController = new AbortController();

    setTimeout(() => {
        abortController.abort();
    }, 500);

    try {
        await delay6(1000, {signal: abortController.signal});
    } catch (error) {
        // 500 milliseconds later
        console.log(error.name)
        //=> 'AbortError'
    }
})();

3.6.2 实现

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;
};

const createDelay = ({willResolve}) => (ms, {value, signal} = {}) => {
    if (signal && signal.aborted) {
		return Promise.reject(createAbortError());
	}

    let timeoutId;
    let settle;
    let rejectFn;
    const signalListener = () => {
        clearTimeout(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 = setTimeout(settle, ms);
    });
    
    if (signal) {
		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();

3.7 第七版 自定义 clearTimeout 和 setTimeout 函数

3.7.1 使用

const customDelay = delay7.createWithTimers({clearTimeout, setTimeout});

(async() => {
    const result = await customDelay(100, {value: '我是若川'});

    // Executed after 100 milliseconds
    console.log(result);
    //=> '我是若川'
})();

3.7.2 实现

传递 clearTimeout, setTimeout 两个参数替代上一版本的clearTimeout,setTimeout。于是有了第七版。这也就是delay的最终实现。

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;
};

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;
}

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;
}
const delay7 = createWithTimers();
delay7.createWithTimers = createWithTimers;

4. axios 取消请求

axios取消原理是:通过传递 config 配置 cancelToken 的形式,来取消的。判断有传cancelToken,在 promise 链式调用的 dispatchRequest 抛出错误,在 adapterrequest.abort() 取消请求,使 promise 走向 rejected,被用户捕获取消信息。

更多查看我的 axios 源码文章取消模块 学习 axios 源码整体架构,取消模块

5. 总结

我们从零开始实现了一个带取消功能比较完善的延迟函数。也就是 delay 70多行源码的实现。

包含支持随机时间结束、提前清除、取消、自定义 clearTimeout、setTimeout等功能。

取消使用了 mdn AbortController ,由于兼容性不太好,社区也有了相应的 npm abort-controller 实现 polyfill

yet-another-abortcontroller-polyfill

建议克隆项目启动服务调试例子,印象会更加深刻。

# 推荐克隆我的项目,保证与文章同步
git clone https://github.com/lxchuan12/delay-analysis.git
cd delay-analysis
# 我写的例子都在 examples 这个文件夹中,可以启动服务本地查看调试
npx http-server examples
# 打开 http://localhost:8080

最后可以持续关注我@若川。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与

另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(4.2k+人)第一的专栏,写有20余篇源码文章。包含jQueryunderscorelodashvuexsentryaxiosreduxkoavue-devtoolsvuex4koa-composevue 3.2 发布vue-thiscreate-vue玩具vitecreate-vite 等20余篇源码文章。


关于 && 源码共读交流群

作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan02。前端路上 | 所知甚少,唯善学。
关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。
若川的博客
segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~
掘金专栏,欢迎关注~
知乎若川视野专栏,开通了若川视野专栏,欢迎关注~
github blog,求个star^_^~