面試官:請手寫一個帶取消功能的延遲函數,axios 取消功能的原理是什麼
大家好,我是若川。最近組織了源碼共讀活動,感興趣的可以點此加我微信 ruochuan12 參與,每週大家一起學習 200 行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含 20 餘篇源碼文章。
本文倉庫 https://github.com/lxchuan12/delay-analysis.git,求個 star^_^[1]
源碼共讀活動 每週一期,已進行到 17 期。於是搜尋各種值得我們學習,且代碼行數不多的源碼。delay 主文件僅 70 多行 [2],非常值得我們學習。
閱讀本文,你將學到:
1. 學會如何實現一個比較完善的 delay 函數
2. 學會使用 AbortController 實現取消功能
3. 學會面試常考 axios 取消功能實現
4. 等等
- 環境準備
# 推薦克隆我的項目,保證與文章同步
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 .
- delay
我們從零開始來實現一個比較完善的 delay 函數 [3]。
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[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;
- axios 取消請求
axios
取消原理是:通過傳遞 config
配置 cancelToken
的形式,來取消的。判斷有傳cancelToken
,在 promise
鏈式調用的 dispatchRequest
拋出錯誤,在 adapter
中 request.abort()
取消請求,使 promise
走向 rejected
,被用戶捕獲取消信息。
更多查看我的 axios
源碼文章取消模塊 學習 axios 源碼整體架構,取消模塊 (可點擊)
- 總結
我們從零開始實現了一個帶取消功能比較完善的延遲函數。也就是 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