帮助我们写好Js异步代码的14条Linting规则

1,181 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

在js中调试异步代码的感觉有点像在雷区中艰难前行, 因为你不知道什么时候什么地方控制台中会打印出信息,当然也很难知道代码是如何执行的。
很难写出完全按照我们意愿执行的异步代码,如果在写异步代码的时候,能够获得一些提示,那岂不是美滋滋?

幸运的是我们可以使用linter帮助我们在把代码部署到生产环境之前发现bug,以下就是帮助我们在js和node.js中更好地编写异步代码的linting规则。

ESLint的异步代码规则

以下规则是ESLint默认提供的,我们可以通过将他们添加到.eslintrc配置文件来启用。

1.no-async-promise-executor

此规则不允许将async函数传递给Promise构造函数。

// ❌
new Promise(async (resolve, reject) => {});

// ✅
new Promise((resolve, reject) => {});

虽然将async函数传递给Promise构造函数在技术上是有效的,但这是错的,因为以下两个原因:第一, 如果async函数抛出异常,异常将被丢失,而不会被promise rejected。第二,如果在构造函数中使用await, 那么再用promise封装一次是没有必要的。

2. no-await-in-loop

此规则不允许在循环中使用wait。 当在循环中使用await并等待异步任务时,这通常表明该段代码没有充分利用js的事件驱动架构。我们可以通过并行执行任务来大幅提高代码的执行效率。

// ❌
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);

当然如果有的时候,我们确实需要在不考虑效率的情况下按顺序运行代码,我们可以使用内联注释:// eslint-disable-line no-await-in-loop

3.no-promise-executor-return

此规则不允许在promise构造函数中使用return。

// ❌
new Promise((resolve, reject) => {
  return result;
});

// ✅
new Promise((resolve, reject) => {
  resolve(result);
});

在promise中return一个值,无法使用这个值也不会以任何方式来影响promise,应该将值传递给resolve ,或者如果发生了错误,则调用 reject

4 require-atomic-updates

这条规则不允许赋值与await相结合使用。 思考下面的例子,你认为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);

没错答案不是8,而是5或者3,问题出在读取和使用totalPosts存在时间差,当在separate 函数中更新一个变量时,更新值不会在当前函数作用域内生效。因此,这两个函数都将其结果更改到totalPosts的初始值0中。

为了避免这种情况,我们应该确保变量在更新的同时被读取。

// ✅
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; // 变量的更新和读取时同时的
}

await Promise.all([addPosts(1), addPosts(2)]);
console.log('Post count:', totalPosts);

5.max-nested-callbacks

此规则限制回调的最大嵌套深度。换句话说,这条规则阻止回调地狱!

/* 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);

深度的嵌套使代码更难阅读,也更难维护。使用promise重构回调,并在编写异步JavaScript代码时使用现代的异步/等待语法会更好。

6. no-return-await

这条规则不允许使用return await

// ❌
async () => {
  return await getUser(userId);
}

// ✅
async () => {
  return getUser(userId);
}

await一个promise并返回是多此一举的, 因为从async函数返回的值都自动的封装在一个promise中, 因此再使用await就显得多此一举了。

这条规则一个例外情况是使用try...catch 语句, 这种情况移除 await关键词回导致promise rejection无法捕捉到。在这种情况下,建议将结果赋值给一个变量。

/ 👎
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
  }
}

7.prefer-promise-reject-errors

此规则强制在reject promise时使用Error 对象。

// ❌
Promise.reject('An error occurred');

// ✅
Promise.reject(new Error('An error occurred'));

最好的做法是总是使用 Error对象来reject 一个 Promise,这样将更容易跟踪错误的来源,因为Error对象存储了堆栈跟踪。

Node.js特定规则

以下规则是Node.js附加ESLint规则,由eslint插件eslint-plugin-node提供。如果要使用它们,我们需要安装插件并将其添加到.eslintrc配置文件中的plugins数组中。

8. node/handle-callback-err

此规则强制在回调中执行错误处理。

// ❌
function callback(err, data) {
  console.log(data);
}

// ✅
function callback(err, data) {
  if (err) {
    console.log(err);
    return;
  }

  console.log(data);
}

在Node.js通常将错误作为第一个参数传递给回调函数。忘记处理错误可能会导致应用程序异常。 当函数的第一个参数名为err时,会触发此规则。在大型项目中,可能会经常出现以别的名称命名的错误情况,如e 或 error,我们可以在.eslintrc配置文件中,配置node/handle-callback-err: ["error", "^(e|err|error)$"]

9. node/no-callback-literal

此规则强制以错误对象作为第一个参数调用回调函数。如果没有发生错误,就传递null或undefined。

/ ❌
cb('An error!');
callback(result);

// ✅
cb(new Error('An error!'));
callback(null, result);

此规则确保我们不会非错误变量作为第一个参数传给回调函数,根据error-first回调约定,回调函数的第一个参数应该是error,如果没有错误,则为null或undefined。只有当函数名为cb或callback时,才会触发该规则。

10. node/no-sync

此规则不允许在Node.js有异步替代方案的api上使用同步方法。

// ❌
const file = fs.readFileSync(path);

// ✅
const file = await fs.readFile(path);

对Node.js中的I/O操作使用同步方法会阻止事件循环。在大多数web应用程序中,在执行I/O操作时都期望使用异步方法。
当然在一些程序(如CLI实用程序或脚本)使用同步方法是可以的。我们可以在文件的顶部使用/* eslint-disable node/no-sync */来禁用此规则。

TypeScript的附加规则

如果项目使用的是TypeScript,那么你可能熟悉TypeScript ESLint(以前的TSLint)。以下规则仅适用于TypeScript项目。

11.@typescript eslint/wait thenable

该规则不允许await 不是Promise值或函数。

// ❌
function getValue() {
  return someValue;
}

await getValue();

// ✅
async function getValue() {
  return someValue;
}

await getValue();

虽然等待一个非Promise值(它将马上resolve )是可以运行的JavaScript代码,但这通常意味着错误, 例如忘记添加括号来调用返回promise的函数。

12.@typescript-eslint/no-floating-promises

此规则强制promise附加错误处理程序。

// ❌
myPromise()
  .then(() => {});

// ✅
myPromise()
  .then(() => {})
  .catch(() => {});

此规则防止在代码库中浮动 Promise。浮动Promise是指没有任何代码来处理潜在错误的 Promise。 始终确保处理reject Promise的情况。

13.@typescript-eslint/no-misused-promises

这条规则不允许将Promise传递给那些不处理它们的地方,比如if条件句。

// ❌
if (getUserFromDB()) {}

// ✅ 👎
if (await getUserFromDB()) {}

// ✅ 👍
const user = await getUserFromDB();
if (user) {}

这个规则可以防止你忘记在一个容易忘记await的地方,去await异步函数。 虽然该规则允许在if条件中await,但建议将结果分配给一个变量,并在条件中使用该变量,以提高可读性。

14. @typescript-eslint/promise-function-async

此规则强制返回Promise的函数必须是async的。

// ❌
function doSomething() {
  return somePromise;
}

// ✅
async function doSomething() {
  return somePromise;
}

返回promise的非异步函数可能会有问题,因为它可能抛出错误对象并返回被reject的promise。此规则确保函数返回被reject的promise或抛出错误,但决不能同时返回两个。 此外,当我们知道所有返回promise并因此异步的函数都被标记为async时,在代码库中导航就容易多了。

在项目中启用这些规则

我们可以通过以下方式安装ESLint配置包,它启用了上述所有规则(除了TypeScript规则)。

npm install --save-dev eslint eslint-config-async eslint-plugin-node

然后在你的.eslintrc配置文件添加以下内容:

{
  "plugins": [
    "eslint-plugin-node"
  ],
  "extends": [
    "eslint-config-async"
  ]
}

译自:maximorlov.com/linting-rul…