面試官:請手寫一個帶取消功能的延遲函數,axios 取消功能的原理是什麼

大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以點此加我微信 ruochuan12 參與,每週大家一起學習 200 行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含 20 餘篇源碼文章。

本文倉庫 https://github.com/lxchuan12/delay-analysis.git,求個 star^_^[1]

源碼共讀活動 每週一期,已進行到 17 期。於是搜尋各種值得我們學習,且代碼行數不多的源碼。delay 主文件僅 70 多行 [2],非常值得我們學習。

閱讀本文,你將學到:

1. 學會如何實現一個比較完善的 delay 函數
2. 學會使用 AbortController 實現取消功能
3. 學會面試常考 axios 取消功能實現
4. 等等
  1. 環境準備

# 推薦克隆我的項目,保證與文章同步
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 .
  1. delay

我們從零開始來實現一個比較完善的 delay 函數 [3]。

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[4]

npm abort-controller[5]

mdn AbortController[6]

fetch-abort[7]

fetch#aborting-requests[8]

yet-another-abortcontroller-polyfill[9]

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;
  1. axios 取消請求

axios取消原理是:通過傳遞 config 配置 cancelToken 的形式,來取消的。判斷有傳cancelToken,在 promise 鏈式調用的 dispatchRequest 拋出錯誤,在 adapterrequest.abort() 取消請求,使 promise 走向 rejected,被用戶捕獲取消信息。

更多查看我的 axios 源碼文章取消模塊 學習 axios 源碼整體架構,取消模塊 (可點擊)

  1. 總結

我們從零開始實現了一個帶取消功能比較完善的延遲函數。也就是 delay 70 多行源碼 [11] 的實現。

包含支持隨機時間結束、提前清除、取消、自定義 clearTimeout、setTimeout等功能。

取消使用了 mdn AbortController[12] ,由於兼容性不太好,社區也有了相應的 npm abort-controller[13] 實現 polyfill

yet-another-abortcontroller-polyfill[14]

建議克隆項目啓動服務調試例子,印象會更加深刻。

# 推薦克隆我的項目,保證與文章同步
git clone https://github.com/lxchuan12/delay-analysis.git
cd delay-analysis
# 我寫的例子都在 examples 這個文件夾中,可以啓動服務本地查看調試
npx http-server examples
# 打開 http://localhost:8080

最後可以持續關注我 @若川。歡迎加我微信 ruochuan12 交流,參與 源碼共讀 活動,每週大家一起學習 200 行左右的源碼,共同進步。

參考資料

[1]

本文倉庫 https://github.com/lxchuan12/delay-analysis.git,求個 star^_^: https://github.com/lxchuan12/delay-analysis.git

[2]

delay 主文件僅 70 多行: https://github.com/sindresorhus/delay/blob/main/index.js

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/XOtYBC9FHyn0_tE3AgT0iA