帮助编写异步代码的ESLint规则
調(diào)試 JavaScript 中的異步代碼有時就像在雷區(qū)中穿梭。你不知道 console.log 會在何時何地打印出來,也不知道代碼是如何執(zhí)行的。
你很難正確構(gòu)造異步代碼,使其按照你的意圖以正確的順序執(zhí)行。
如果在編寫異步代碼時能得到一些指導(dǎo),并在即將出錯時收到一條有用的信息,那豈不更好?
幸運(yùn)的是,在將錯誤推向生產(chǎn)環(huán)境之前,我們有一些規(guī)則來捕捉這些錯誤。以下是一份經(jīng)過編譯的linting規(guī)則列表,可為你在 JavaScript 和 Node.js 中編寫異步代碼提供具體幫助。
即使你最終沒有在項(xiàng)目中使用這些規(guī)則,閱讀它們的說明也會讓你更好地理解異步代碼,并提高你的開發(fā)技能。
ESLint異步代碼規(guī)則
ESLint 默認(rèn)提供以下規(guī)則。將它們添加到 .eslintrc 配置文件中即可啟用。
no-async-promise-executor
該規(guī)則不允許將async函數(shù)傳遞給new Promise構(gòu)造函數(shù)。
// ?
new Promise(async (resolve, reject) => {});
// ?
new Promise((resolve, reject) => {});
雖然從技術(shù)上講,向 Promise 構(gòu)造函數(shù)傳遞異步函數(shù)是有效的,但出于以下兩個原因,這樣做通常是錯誤的。首先,如果異步函數(shù)拋出錯誤,錯誤將丟失,不會被新構(gòu)造的 Promise 拒絕。其次,如果在構(gòu)造函數(shù)內(nèi)部使用了 await,那么外層的 Promise 可能就沒有必要了,可以將其刪除。
no-await-in-loop
該規(guī)則不允許在循環(huán)內(nèi)使用await。
在對可迭代對象的每個元素進(jìn)行操作并等待異步任務(wù)時,往往表明程序沒有充分利用 JavaScript 的事件驅(qū)動架構(gòu)。通過并行執(zhí)行任務(wù),可以大大提高代碼的效率。
// ?
for (const url of urls) {
const response = await fetch(url);
}
// ?
const responses = [];
for (const url of urls) {
const response = fetch(url);
responses.push(response);
}
await Promise.all(responses);
如果你想按順序運(yùn)行任務(wù),我建議你使用行內(nèi)注釋暫時禁用該規(guī)則:// eslint-disable-line no-await-in-loop。
no-promise-executor-return
該規(guī)則不允許在 Promise 構(gòu)造函數(shù)中返回值。
// ?
new Promise((resolve, reject) => {
return result;
});
// ?
new Promise((resolve, reject) => {
resolve(result);
});
在 Promise 構(gòu)造函數(shù)中返回的值不能使用,也不會對 promise 產(chǎn)生任何影響。應(yīng)將該值傳遞給resolve,如果發(fā)生錯誤,則調(diào)用 reject 并告知錯誤信息。
該規(guī)則不會阻止你在 Promise 構(gòu)造函數(shù)中的嵌套回調(diào)內(nèi)返回值。請務(wù)必使用
resolve或reject來結(jié)束promise。
require-atomic-updates
該規(guī)則不允許將賦值與 await 結(jié)合使用,否則會導(dǎo)致競賽條件。
請看下面的示例,你認(rèn)為 totalPosts 的最終值會是多少?
// ?
let totalPosts = 0;
async function getPosts(userId) {
const users = [{ id: 1, posts: 5 }, { id: 2, posts: 3 }];
await sleep(Math.random() * 1000);
return users.find((user) => user.id === userId).posts;
}
async function addPosts(userId) {
totalPosts += await getPosts(userId);
}
await Promise.all([addPosts(1), addPosts(2)]);
console.log('Post count:', totalPosts);
也許你已經(jīng)感覺到這是一個騙人的問題,答案不是 8。沒錯,totalPosts 打印的是 5 或 3。自己在瀏覽器中試試吧。
問題在于讀取和更新 totalPosts 之間存在時間差。這就造成了一個競賽條件,當(dāng)值在單獨(dú)的函數(shù)調(diào)用中更新時,更新不會反映在當(dāng)前函數(shù)的作用域中。因此,這兩個函數(shù)都將其結(jié)果添加到 totalPosts 的初始值 0 中。
要避免這種競賽條件,應(yīng)確保在更新變量的同時讀取變量。
// ?
let totalPosts = 0;
async function getPosts(userId) {
const users = [{ id: 1, posts: 5 }, { id: 2, posts: 3 }];
await sleep(Math.random() * 1000);
return users.find((user) => user.id === userId).posts;
}
async function addPosts(userId) {
const posts = await getPosts(userId);
totalPosts += posts; // variable is read and immediately updated
}
await Promise.all([addPosts(1), addPosts(2)]);
console.log('Post count:', totalPosts);
max-nested-callbacks
該規(guī)則強(qiáng)制限制回調(diào)的最大嵌套深度。換句話說,該規(guī)則可防止回調(diào)地獄!
/* eslint max-nested-callbacks: ["error", 3] */
// ?
async1((err, result1) => {
async2(result1, (err, result2) => {
async3(result2, (err, result3) => {
async4(result3, (err, result4) => {
console.log(result4);
});
});
});
});
// ?
const result1 = await asyncPromise1();
const result2 = await asyncPromise2(result1);
const result3 = await asyncPromise3(result2);
const result4 = await asyncPromise4(result3);
console.log(result4);
深度嵌套會使代碼難以閱讀,更難以維護(hù)。在編寫 JavaScript 異步代碼時,將回調(diào)重構(gòu)為promise,并使用現(xiàn)代的 async/await 語法。
no-return-await
該規(guī)則不允許不必要的return await。
// ?
async () => {
return await getUser(userId);
}
// ?
async () => {
return getUser(userId);
}
由于async函數(shù)返回的所有值都已封裝在 promise 中,因此等待 promise 并立即返回是不必要的。因此,你可以直接返回 promise。
當(dāng)周圍有 try...catch 語句時,這條規(guī)則會出現(xiàn)例外。移除 await 關(guān)鍵字會導(dǎo)致不捕獲拒絕的promise。在這種情況下,我建議你將結(jié)果賦值給另一行的變量,以明確意圖。
// ??
async () => {
try {
return await getUser(userId);
} catch (error) {
// Handle getUser error
}
}
// ??
async () => {
try {
const user = await getUser(userId);
return user;
} catch (error) {
// Handle getUser error
}
}
prefer-promise-reject-errors
該規(guī)則強(qiáng)制要求在拒絕 Promise 時使用 Error 對象。
// ?
Promise.reject('An error occurred');
// ?
Promise.reject(new Error('An error occurred'));
最佳做法是始終使用 Error 對象來拒絕Promise。因?yàn)殄e誤對象會存儲堆棧跟蹤,所以這樣做可以更容易地跟蹤錯誤的來源。
Node.js 特定規(guī)則
以下規(guī)則是 esLint-plugin-node 插件為 Node.js 提供的附加 ESLint 規(guī)則。要使用這些規(guī)則,需要安裝該插件并將其添加到 .eslintrc 配置文件的 plugins 數(shù)組中。
node/handle-callback-err
該規(guī)則強(qiáng)制在回調(diào)中處理錯誤。
// ?
function callback(err, data) {
console.log(data);
}
// ?
function callback(err, data) {
if (err) {
console.log(err);
return;
}
console.log(data);
}
在 Node.js 中,將錯誤作為第一個參數(shù)傳遞給回調(diào)函數(shù)是很常見的。忘記處理錯誤會導(dǎo)致應(yīng)用程序行為異常。
當(dāng)函數(shù)的第一個參數(shù)名為 err 時,就會觸發(fā)該規(guī)則。在大型項(xiàng)目中,經(jīng)常會發(fā)現(xiàn)不同的錯誤命名方式,如 e 或 error。你可以通過在 .eslintrc 文件中為規(guī)則提供第二個參數(shù)來更改默認(rèn)配置:node/handle-callback-err: ["error", "^(e|err|error)$"]。
node/no-callback-literal
該規(guī)則強(qiáng)制要求在調(diào)用回調(diào)函數(shù)時將 Error 對象作為第一個參數(shù)。如果沒有錯誤,也接受 null 或 undefined。
// ?
cb('An error!');
callback(result);
// ?
cb(new Error('An error!'));
callback(null, result);
該規(guī)則可確保你不會意外調(diào)用第一個參數(shù)為非錯誤的回調(diào)函數(shù)。根據(jù)錯誤優(yōu)先的回調(diào)約定,回調(diào)函數(shù)的第一個參數(shù)應(yīng)該是錯誤,如果沒有錯誤,則應(yīng)該是 null 或 undefined 。
只有當(dāng)函數(shù)名為 cb 或 callback 時,才會觸發(fā)該規(guī)則。
node/no-sync
如果 Node.js 核心 API 中存在異步替代方法,則該規(guī)則不允許使用同步方法。
// ?
const file = fs.readFileSync(path);
// ?
const file = await fs.readFile(path);
在 Node.js 中使用同步方法進(jìn)行 I/O 操作會阻止事件循環(huán)。在大多數(shù)網(wǎng)絡(luò)應(yīng)用程序中,進(jìn)行 I/O 操作時需要使用異步方法。
在 CLI 實(shí)用程序或腳本等某些應(yīng)用程序中,使用同步方法也是可以的。你可以使用 /* eslint-disable node/no-sync */ 在文件頂部禁用這一規(guī)則。
針對 TypeScript 用戶的附加規(guī)則
如果你的項(xiàng)目使用的是 TypeScript,那么你可能已經(jīng)熟悉了 TypeScript ESLint(以前的 TSLint)。以下規(guī)則僅適用于 TypeScript 項(xiàng)目,因?yàn)樗鼈儠念愋托畔⒅型茢喑鲱~外的上下文。
@typescript-eslint/await-thenable
該規(guī)則不允許等待非 Promise 的函數(shù)或值。
// ?
function getValue() {
return someValue;
}
await getValue();
// ?
async function getValue() {
return someValue;
}
await getValue();
雖然等待一個非 Promise 的值是有效的 JavaScript(它會立即解析),但這往往表明程序員出錯了,比如在調(diào)用一個返回 Promise 的函數(shù)時忘記加上括號。
@typescript-eslint/no-floating-promises
此規(guī)則強(qiáng)制 Promise 必須附加錯誤處理程序。
// ?
myPromise()
.then(() => {});
// ?
myPromise()
.then(() => {})
.catch(() => {});
此規(guī)則可防止代碼庫中出現(xiàn)浮動 Promise。浮動 Promise 是指沒有任何代碼來處理潛在錯誤的 Promise。
請務(wù)必處理 Promise 拒絕,否則你的 Node.js 服務(wù)器將會崩潰。
@typescript-eslint/no-misused-promises
該規(guī)則禁止將 Promise 傳遞到非處理 Promise 的地方,如 if 條件語句。
// ?
if (getUserFromDB()) {}
// ? ??
if (await getUserFromDB()) {}
// ? ??
const user = await getUserFromDB();
if (user) {}
該規(guī)則可防止你在容易遺漏的地方忘記 await 異步函數(shù)。
雖然該規(guī)則允許在 if 條件語句中等待,但我建議將結(jié)果賦值給一個變量,然后在條件中使用該變量,以提高可讀性。
@typescript-eslint/promise-function-async
該規(guī)則強(qiáng)制 Promise 返回函數(shù)必須是 async 。
// ?
function doSomething() {
return somePromise;
}
// ?
async function doSomething() {
return somePromise;
}
返回promise的非同步函數(shù)可能會有問題,因?yàn)樗赡軙伋鲆粋€ Error 對象并返回一個被拒絕的promise。代碼通常不會同時處理這兩種情況。本規(guī)則可確保函數(shù)返回被拒絕的promise或拋出 Error,但絕不會同時返回兩種情況。
此外,如果知道所有返回 Promise 的函數(shù)都被標(biāo)記為 async ,那么瀏覽代碼庫就容易多了。
啟用這些規(guī)則
我發(fā)布了一個 ESLint 配置包,你可以輕松將其添加到你的項(xiàng)目中。它分別導(dǎo)出了基本規(guī)則、Node.js 特定規(guī)則和 TypeScript 特定規(guī)則。
針對非TypeScript用戶
npm install --save-dev eslint eslint-config-async eslint-plugin-node
然后在你的 .eslintrc 配置文件中添加下列配置:
{
"plugins": [
"eslint-plugin-node"
],
"extends": [
"async",
"async/node"
]
}
針對TypeScript用戶
安裝包及其依賴:
npm install --save-dev eslint eslint-config-async eslint-plugin-node typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin
然后在你的 .eslintrc 配置文件中添加下列配置:
"plugins": [
"eslint-plugin-node",
"@typescript-eslint"
],
"extends": [
"async",
"async/node",
"async/typescript"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"tsconfigRootDir": "__dirname",
"project": ["./tsconfig.json"],
};
就是這樣!將這些異步代碼的校驗(yàn)規(guī)則添加到你的項(xiàng)目中,并修復(fù)出現(xiàn)的任何問題。你可能會發(fā)現(xiàn)一兩個 bug!?? ??
以上就是本文的全部內(nèi)容,如果對你有所幫助,歡迎點(diǎn)贊、收藏、轉(zhuǎn)發(fā)~
總結(jié)
以上是生活随笔為你收集整理的帮助编写异步代码的ESLint规则的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大模型应用(1) 搭建本地知识库
- 下一篇: 比Nginx更好用的Gateway!