1. 前言
大家好,我是若川。为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学。我倾力组织了源码共读活动,每周大家一起学习 200 行左右的源码,共同进步,已进行 4 个月,很多人都表示收获颇丰。
想学源码,极力推荐关注我写的专栏(目前 1.9K 人关注)《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue 3.2 发布、vue-this、create-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 yarncd 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 yarncd delay && yarn i# VSCode 直接打开当前项目# code .
       复制代码
 3. delay
我们从零开始来实现一个比较完善的 delay 函数。
3.1 第一版 简版延迟
要完成这样一个延迟函数。
3.1.1 使用
 (async() => {    await delay1(1000);    console.log('输出这句');})();
       复制代码
 3.1.2 实现
用 Promise 和 setTimeout 结合实现,我们都很容易实现以下代码。
 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 抛出错误,在 adapter 中 request.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.gitcd delay-analysis# 我写的例子都在 examples 这个文件夹中,可以启动服务本地查看调试npx http-server examples# 打开 http://localhost:8080
       复制代码
 
最后可以持续关注我 @若川。欢迎与我交流,参与 源码共读 活动,每周大家一起学习 200 行左右的源码,共同进步。
评论