[JS] 14个规则让你在JavaScript中编写异步代码更加规范

在JavaScript异步代码编常见的问题

在JavaScript的异步编程中,我们经常面临各种挑战。这包括处理竞态条件、避免回调地狱、处理错误,以及确保资源不泄漏。异步函数的不确定性和事件竞争也是常见问题。解决这些问题需要仔细考虑和正确的工具,以确保代码的可读性和可维护性。希望能在这篇文章中你能收获一些新内容,不断学习实践成长。

常见情景问题

传入异步executor

js 复制代码
// ❌ 
new Promise(async (resolve, reject) => {}); 
// ✅ 
new Promise((resolve, reject) => {});
  1. 错误处理问题:如果异步函数内部抛出一个错误,它将不会被新构造的Promise reject,而是会被视为未捕获的异常。这是因为Promise构造函数在异步函数内没有适当的错误处理机制。这可能会导致错误的处理问题,因为在一般情况下,我们期望Promise能够reject并传递错误信息。

  2. 不必要的包装问题 :如果在构造函数内部使用await,那么将Promise包装在异步函数内可能是不必要的。await本身已经会返回一个Promise,并且如果内部Promise没有额外的异步操作,包装它可能会导致多余的Promise嵌套。这增加了代码的复杂性,而不提供额外的好处。

为了解决这些问题,通常更好的做法是在异步函数内部创建一个Promise并正确处理错误,然后将这个Promise返回。这将确保错误能够正常地被捕获并传递,而不会引入不必要的Promise包装。

示例:

javascript 复制代码
function someAsyncFunction() {
  return new Promise(async (resolve, reject) => {
    try {
      // 异步操作
      const result = await someOtherAsyncFunction();
      resolve(result);
    } catch (error) {
      reject(error);
    }
  });
}

这种方式可以更好地处理异步操作和错误,并避免不必要的Promise包装。

循环中的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);

当对可迭代对象的每个元素执行操作并等待异步任务时,这通常表明程序没有充分利用 JavaScript 的事件驱动架构。通过并行执行任务,可以大大提高代码的效率。

当然这只是一般情况,如果对于异步的执行需要顺序上的控制还是可以使用这种方法。

executor方法存在return

js 复制代码
// ❌
new Promise((resolve, reject) => {
  return result;
});

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

在Promise构造函数中,你不能使用return语句来返回一个值并期望它会影响Promise的状态。这是因为Promise构造函数的参数是一个executor函数,它在Promise对象被创建时会立即执行,而return语句在这个上下文中并不会影响Promise的行为。你可以在executor函数内部使用resolvereject函数来控制Promise的状态,而不是使用return语句。

竞态条件 (Race Conditions)

这是一个非常有意思的问题,先让我们看一个示例

js 复制代码
// ❌
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,但是实际上这里打印的内容是3或者5. 问题在于 读取和更新 totalPosts 之间存在时间间隔 .这会导致竞态条件,以便在单独的函数调用中更新值时,更新不会反映在当前函数作用域中。因此,这两个函数都将它们的结果与 totalPosts 的初始值 0 相加。

为了避免这种争用条件,你应该确保在更新变量的同时读取变量。

ts 复制代码
let totalPosts = 0;
const sleep = async (delay) => {
  return new Promise((r) => {
    setTimeout(r, delay);
  });
};
await(async () => {
  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 post = await getPosts(userId);
    totalPosts += post;
    console.log(totalPosts);
  }

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

回调地狱

js 复制代码
// ❌
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和async/await语法的清晰可读的代码。深层嵌套的回调结构会使代码难以理解和维护,而将其改写为Promise链和async/await语法可以显著提高代码的可读性和可维护性。

在第一个示例中,多个异步操作被嵌套在回调函数内部,这导致了代码的缩进深度和复杂性的增加。而在第二个示例中,使用async/await和Promise,每个异步操作都被清晰地表达,并且代码结构更加扁平,易于理解。

这种重构的好处包括:

  1. 可读性提高:使用async/await和Promise可以让代码更具可读性,更接近自然语言的表达方式,易于理解代码的执行流程。

  2. 错误处理:使用async/await使错误处理更加简单,你可以使用try-catch块来捕获和处理异步操作中的错误。

  3. 扁平结构:代码变得更加扁平,没有深层嵌套的回调,使代码更易维护。

  4. 可维护性:改写为Promise和async/await的代码更容易维护,因为你可以更容易地跟踪和调试问题。

总之,将回调地狱重构为Promise链和async/await语法是一种良好的实践,可以提高代码质量和可维护性。这使得异步代码更容易编写和理解,减少了潜在的错误和问题。

异步方法中 return await

js 复制代码
// ❌
async () => {
  return await getUser(userId);
};

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

两种写法都可以工作,但第一种写法在async函数中使用了await,然后返回一个Promise。然而,它引入了不必要的Promise包装,因为async函数本身会隐式地返回一个Promise。

async函数中返回的值会自动包装成一个Promise,所以在大多数情况下,直接返回异步操作的结果即可,而不需要额外的await和Promise包装。

rejectError结果

在创建一个Promise并拒绝(reject)它时,通常更好的做法是使用new Error来创建一个Error对象,以便提供有关错误的更多信息,以及允许捕获和处理这个错误。因此,第二种写法是更推荐的。

❌ 不推荐的写法:

javascript 复制代码
Promise.reject('An error occurred');

在这种写法中,你只传递了一个简单的字符串作为拒绝的理由。这可能会使错误处理和调试更加困难,因为它没有提供关于错误的详细信息。

✅ 推荐的写法:

javascript 复制代码
Promise.reject(new Error('An error occurred'));

在这种写法中,你使用new Error创建了一个Error对象,将有关错误的信息包装在其中。这样做可以提供更多的上下文和信息,使错误处理更容易,也使错误消息更具描述性。当你在代码中捕获这个错误时,可以访问错误对象的属性,如message,以了解发生了什么错误。

总之,使用new Error来创建Error对象并拒绝Promise是更好的做法,以提供更多的错误信息和更好的错误处理能力。

node runtime下的异步编程问题

回调忽略错误处理

js 复制代码
// ❌
function callback(err, data) {
  console.log(data);
}

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

  console.log(data);
}

在 Node.js 中,通常将错误作为第一个参数传递给回调函数。忘记处理错误可能会导致应用程序行为异常。

在这种写法中,首先检查了err参数,如果存在错误,会进行适当的处理,例如打印错误信息。然后,只有在没有错误时才会处理数据。这使得代码更具健壮性,能够更好地应对潜在的问题。

正确处理错误是编写可靠的代码的关键部分。在回调函数中,要确保检查并适当地处理错误,以防止错误被忽略,从而提高代码的稳定性和可维护性。

回调第一个参数不是error

js 复制代码
// ❌
cb('An error!');
callback(result);

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

在调用回调函数时,通常更好的做法是遵循一种约定,使错误作为第一个参数传递,以便在调用方能够更容易地检查和处理错误。

在回调函数中,建议使用一种明确的约定,将错误作为第一个参数传递,以确保错误能够被正确处理和检查。这有助于减少混淆,提高代码的可读性。

调用api中存在异步替代方法的同步方法

js 复制代码
// ❌
const file = fs.readFileSync(path);

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

在Node.js中,通常不建议使用同步的文件读取方法(如fs.readFileSync)来读取文件,因为它会阻塞Node.js的事件循环,可能导致性能问题,特别是在处理大量文件或需要高并发的情况下。而使用异步的文件读取方法(如fs.promises.readFile)是更好的选择,因为它不会阻塞事件循环,允许Node.js处理多个请求而不被阻塞。

同样的,在commonJs规范中通过require 去导入其他module也会有相同问题。

await 同步方法或值

js 复制代码
// ❌
function getValue() {
  return someValue;
}

await getValue();

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

await getValue();

在使用await关键字的上下文中,只有异步函数(返回Promise的函数)才能够被等待, 虽然第一种写法并不会产生特定的错误(方法会同步resolve),但是确保你等待的函数是一个异步函数(返回Promise的函数),以便能够正确地使用await来等待其完成。

promise忽略catch

js 复制代码
// ❌
myPromise()
  .then(() => {});

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

在Promise链中,通常需要正确处理错误,以确保能够捕获和处理在Promise链中发生的任何拒绝(reject)情况。因此,第一种写法是不推荐的,因为它没有提供错误处理机制,而第二种写法是更好的做法,因为它包括了.catch()用于捕获可能发生的错误。

特别是服务端场景对于Promise异常的处理是必要的,同时需要确定的是使用try catch的范围。

混合同步和异步代码

js 复制代码
// ❌
if (getUserFromDB()) {}

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

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

使用await关键字等待异步操作的结果是一种良好的实践,可以确保在继续执行后续代码之前等待异步操作完成。然而,将await直接用在条件语句中,如if,可能会引发一些问题和混淆,因此建议将await用在适当的地方以提高可读性。

使用await关键字等待异步操作的结果是重要的,但在条件语句中使用await应该小心谨慎,以避免混淆和提高可读性。将await的结果分配给一个变量通常是更好的做法。

异步函数未使用 async 关键字

js 复制代码
// ❌
function doSomething() {
  return somePromise;
}

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

在JavaScript中,一个函数可以返回一个Promise对象,但是通常,如果函数是异步的,最好将其标记为async以明确表明其异步性质,并使其返回值自动包装在Promise中。这有助于代码的清晰性和可读性。

此外,标记函数为async还有助于确保错误处理,因为async函数内部可以使用try...catch块来捕获和处理可能发生的异常,以及返回被拒绝的Promise。

总之,通常最好将异步函数标记为async以提高代码的可读性,并确保明确表示函数的异步性质。这有助于降低混淆和错误的风险。

使用eslint统一规范和风格

这里推荐使用eslint-config-async eslint-plugin-node 来完成异步代码编写的规范,同时在协作开发时能起到非常大的作用。

参考引用:[maximorlov]: maximorlov.com/linting-rul... "14 Linting Rules To Help You Write Asynchronous Code in JavaScript - Maxim Orlov"

相关推荐
Sun_light4 分钟前
深入理解JavaScript中的「this」:从概念到实战
前端·javascript
水冗水孚30 分钟前
🚀四种方案解决浏览器地址栏预览txt文本乱码问题🚀Content-Type: text/plain;没有charset=utf-8
javascript·nginx·node.js
绅士玖35 分钟前
JavaScript 中的 arguments、柯里化和展开运算符详解
前端·javascript·ecmascript 6
每天都想着怎么摸鱼的前端菜鸟37 分钟前
【uniapp】uniapp热更新WGT资源,简单的多环境WGT打包脚本
javascript·uni-app
我是小七呦38 分钟前
😄我再也不用付费使用PDF工具了,我在Web上实现了一个PDF预览/编辑工具
前端·javascript·面试
G等你下课40 分钟前
JavaScript 中的 argument:函数参数的幕后英雄
前端·javascript
bilibilibiu灬44 分钟前
实现一个web视频动效播放器video-alpha-player
前端·javascript
十盒半价1 小时前
深入探索 JavaScript:从作用域到闭包的奇妙之旅
前端·javascript·trae
骆驼Lara2 小时前
前端跨域解决方案(1):什么是跨域?
前端·javascript
onebyte8bits2 小时前
CSS Houdini 解锁前端动画的下一个时代!
前端·javascript·css·html·houdini