面試官:請手寫一個帶取消功能的延遲函數(shù),axios 取消功能的原理是什么
本文倉庫 https://github.com/lxchuan12/delay-analysis.git,求個star^_^[1]
源碼共讀活動 每周一期,已進(jìn)行到17期。于是搜尋各種值得我們學(xué)習(xí),且代碼行數(shù)不多的源碼。delay 主文件僅70多行[2],非常值得我們學(xué)習(xí)。
閱讀本文,你將學(xué)到:
1.?學(xué)會如何實現(xiàn)一個比較完善的?delay?函數(shù)
2.?學(xué)會使用?AbortController?實現(xiàn)取消功能
3.?學(xué)會面試常考?axios?取消功能實現(xiàn)
4.?等等
2. 環(huán)境準(zhǔn)備
#?推薦克隆我的項目,保證與文章同步
git?clone?https://github.com/lxchuan12/delay-analysis.git
#?npm?i?-g?yarn
cd?delay-analysis/delay?&&?yarn?i
#?VSCode?直接打開當(dāng)前項目
#?code?.
#?我寫的例子都在?examples?這個文件夾中,可以啟動服務(wù)本地查看調(diào)試
#?在?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?直接打開當(dāng)前項目
#?code?.
3. delay
我們從零開始來實現(xiàn)一個比較完善的 delay 函數(shù)[3]。
3.1 第一版 簡版延遲
要完成這樣一個延遲函數(shù)。
3.1.1 使用
(async()?=>?{
????await?delay1(1000);
????console.log('輸出這句');
})();
3.1.2 實現(xiàn)
用 Promise 和 setTimeout 結(jié)合實現(xiàn),我們都很容易實現(xiàn)以下代碼。
const?delay1?=?(ms)?=>?{
????return?new?Promise((resolve,?reject)?=>?{
????????setTimeout(()?=>?{
????????????resolve();
????????},?ms);
????});
}
我們要傳遞結(jié)果。
3.2 第二版 傳遞 value 參數(shù)作為結(jié)果
3.2.1 使用
(async()?=>?{
????const?result?=?await?delay2(1000,?{?value:?'我是若川'?});
????console.log('輸出結(jié)果',?result);
})();
我們也很容易實現(xiàn)如下代碼。傳遞 value 最后作為結(jié)果返回。
3.2.2 實現(xiàn)
因此我們實現(xiàn)也很容易實現(xiàn)如下第二版。
const?delay2?=?(ms,?{?value?}?=?{})?=>?{
????return?new?Promise((resolve,?reject)?=>?{
????????setTimeout(()?=>?{
????????????resolve(value);
????????},?ms);
????});
}
這樣寫,Promise 永遠(yuǎn)是成功。我們也需要失敗。這時我們定義個參數(shù) willResolve 來定義。
3.3 第三版 willResolve 參數(shù)決定成功還是失敗。
3.3.1 使用
(async()?=>?{
????try{
????????const?result?=?await?delay3(1000,?{?value:?'我是若川',?willResolve:?false?});
????????console.log('永遠(yuǎn)不會輸出這句');
????}
????catch(err){
????????console.log('輸出結(jié)果',?err);
????}
})();
3.3.2 實現(xiàn)
加個 willResolve 參數(shù)決定成功還是失敗。于是我們有了如下實現(xiàn)。
const?delay3?=?(ms,?{value,?willResolve}?=?{})?=>?{
????return?new?Promise((resolve,?reject)?=>?{
????????setTimeout(()?=>?{
????????????if(willResolve){
????????????????resolve(value);
????????????}
????????????else{
????????????????reject(value);
????????????}
????????},?ms);
????});
}
3.4 第四版 一定時間范圍內(nèi)隨機(jī)獲得結(jié)果
延時器的毫秒數(shù)是寫死的。我們希望能夠在一定時間范圍內(nèi)隨機(jī)獲取到結(jié)果。
3.4.1 使用
(async()?=>?{
????try{
????????const?result?=?await?delay4.reject(1000,?{?value:?'我是若川',?willResolve:?false?});
????????console.log('永遠(yuǎn)不會輸出這句');
????}
????catch(err){
????????console.log('輸出結(jié)果',?err);
????}
????const?result2?=?await?delay4.range(10,?20000,?{?value:?'我是若川,range'?});
????console.log('輸出結(jié)果',?result2);
})();
3.4.2 實現(xiàn)
我們把成功 delay 和失敗 reject 封裝成一個函數(shù),隨機(jī) range 單獨封裝成一個函數(shù)。
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();
實現(xiàn)到這里,相對比較完善了。但我們可能有需要提前結(jié)束。
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 實現(xiàn)
聲明 settle變量,封裝 settle 函數(shù),在調(diào)用 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 可以實現(xiàn)取消功能。
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 實現(xiàn)
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 函數(shù)
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 實現(xiàn)
傳遞 clearTimeout, setTimeout 兩個參數(shù)替代上一版本的clearTimeout,setTimeout。于是有了第七版。這也就是delay的最終實現(xiàn)。
????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 鏈?zhǔn)秸{(diào)用的 dispatchRequest 拋出錯誤,在 adapter 中 request.abort() 取消請求,使 promise 走向 rejected,被用戶捕獲取消信息。
更多查看我的 axios 源碼文章取消模塊 學(xué)習(xí) axios 源碼整體架構(gòu),取消模塊(可點擊)
5. 總結(jié)
我們從零開始實現(xiàn)了一個帶取消功能比較完善的延遲函數(shù)。也就是 delay 70多行源碼[11]的實現(xiàn)。
包含支持隨機(jī)時間結(jié)束、提前清除、取消、自定義 clearTimeout、setTimeout等功能。
取消使用了 mdn AbortController[12] ,由于兼容性不太好,社區(qū)也有了相應(yīng)的 npm abort-controller[13] 實現(xiàn) polyfill。
yet-another-abortcontroller-polyfill[14]
建議克隆項目啟動服務(wù)調(diào)試?yán)樱∠髸由羁獭?/p>
#?推薦克隆我的項目,保證與文章同步
git?clone?https://github.com/lxchuan12/delay-analysis.git
cd?delay-analysis
#?我寫的例子都在?examples?這個文件夾中,可以啟動服務(wù)本地查看調(diào)試
npx?http-server?examples
#?打開?http://localhost:8080
參考資料
本文倉庫 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
