JavaScript异步编程:(回调函数、Promise、async/await、Generator)

文章目录

  • 前言
  • 1. 回调函数
    • 1.1. 回调函数的基本概念和使用方法
    • 1.2. 回调函数的优缺点和注意事项
    • 1.3. 回调地狱和如何避免
  • 2. Promise
    • 2.1. Promise 的基本概念和使用方法
    • 2.2. Promise 的状态和状态转换
    • 2.3. Promise 的链式调用和错误处理
    • 2.4. Promise.all 和 Promise.race 的使用方法
  • 3. async/await
    • 3.1. async/await 的基本概念和使用方法
    • 3.2. async/await 和 Promise 的关系
    • 3.3. async/await 的错误处理方法
  • 4. Generator
    • 4.1. Generator 的基本概念和使用方法
    • 4.2. Generator 和异步编程的结合
    • 4.3. Generator 的错误处理方法
  • 5. 其他异步编程技术
    • 5.1. 事件监听器
    • 5.2. setInterval 和 setTimeout
  • 6. 异步编程的最佳实践
    • 6.1. 如何优化异步编程性能

前言

JavaScript 中异步编程的目的是允许代码执行非阻塞操作。这很重要,因为 JavaScript 是一种单线程语言,意味着一次只能执行一个任务。异步编程允许同时执行多个任务,提高性能和响应能力。

JavaScript 中有几种异步编程技术,包括回调函数Promiseasync/await生成器

以下是 JavaScript 异步编程的简单概述:

1. 回调函数

1.1. 回调函数的基本概念和使用方法

JavaScript 中回调函数是指将一个函数作为参数传递给另一个函数,并在另一个函数执行完后调用该函数。回调函数通常用于处理异步操作,比如向服务器发送请求并在请求返回后执行某些操作。

以下是回调函数的基本使用方法:

// 1. 创建一个需要执行的函数。
function doSomething(callback) {
  console.log("doSomething");
  callback();
}

// 2. 创建一个回调函数。
function callback() {
  console.log("callback");
}

// 3. 调用需要执行的函数,并将回调函数作为参数传递进去。
doSomething(callback);

以上代码将依次输出 doSomethingcallback

1.2. 回调函数的优缺点和注意事项

回调函数的优点包括:

  • 可以处理异步操作,允许代码执行非阻塞操作,提高性能和响应能力。
  • 可以实现代码的模块化和可重用性。

回调函数的缺点和注意事项包括:

  • 错误处理:如果回调函数中出现错误,需要对错误进行处理,否则可能会导致程序崩溃。
  • 可能会产生竞态条件(race condition):如果多个回调函数同时修改同一个变量,可能会出现竞态条件,导致程序出现不可预料的结果。
  • 可能会产生回调地狱(callback hell):如果回调函数嵌套过多,代码可读性会变差,难以维护。

例子:模拟一个回调地狱

function step1(callback) {
  setTimeout(function() {
    console.log('Step 1 done');
    callback();
  }, 1000);
}

function step2(callback) {
  setTimeout(function() {
    console.log('Step 2 done');
    callback();
  }, 1000);
}

function step3(callback) {
  setTimeout(function() {
    console.log('Step 3 done');
    callback();
  }, 1000);
}

step1(function() {
  step2(function() {
    step3(function() {
      console.log('All steps done');
    });
  });
});

如果有一百个回调函数,请问阁下如何应对?

1.3. 回调地狱和如何避免

为了避免回调地狱,建议使用 Promiseasync/awaitPromises 允许链接异步操作,而 async/await 则使编写异步代码更容易以同步方式进行。此外,将复杂的回调拆分为较小的函数也可以帮助提高可读性和可维护性。

2. Promise

2.1. Promise 的基本概念和使用方法

PromiseJavaScript 中一种用于处理异步操作的对象。Promise 对象表示一个可能还没有完成的异步操作,并且可以指定在异步操作完成时如何处理结果

以下是 Promise 的基本使用方法:

const promise = new Promise(function(resolve, reject) {
  // 异步操作
  if (/* 异步操作成功 */) {
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // 异步操作成功时的处理
}, function(error) {
  // 异步操作失败时的处理
});

在这个例子中,promise 对象表示一个异步操作。new Promise 构造函数接受一个函数作为参数,这个函数又接受两个函数作为参数:resolvereject。当异步操作成功时,调用 resolve 函数并传递一个值;当异步操作失败时,调用 reject 函数并传递一个错误对象。

promise.then 方法用于指定在异步操作完成时如何处理结果。then 方法接受两个函数作为参数:一个用于处理异步操作成功时的结果,另一个用于处理异步操作失败时的结果。

2.2. Promise 的状态和状态转换

Promise 对象有三种状态:pending(等待中)、fulfilled(已成功)和rejected(已失败)。

Promise 对象被创建时,它的状态是 pending。当异步操作成功时,可以调用 resolve 函数并将结果传递给它,这样 Promise 对象的状态就会从 pending 转变为 fulfilled。当异步操作失败时,可以调用 reject 函数并将错误传递给它,这样 Promise 对象的状态就会从 pending 转变为 rejected

const promise = new Promise(function(resolve, reject) {
  // 异步操作
  if (/* 异步操作成功 */) {
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // 异步操作成功时的处理
}, function(error) {
  // 异步操作失败时的处理
});

2.3. Promise 的链式调用和错误处理

以下是 Promise 的链式调用:

const promise = new Promise(function(resolve, reject) {
  // 异步操作
});

promise.then(function(result1) {
  // 处理结果1
  return result1;
}).then(function(result2) {
  // 处理结果2
  return result2;
}).then(function(result3) {
  // 处理结果3
  return result3;
}).catch(function(error) {
  // 处理错误
  console.log(error);
});

在这个例子中,在 promise 对象上调用 then 方法可以指定在异步操作完成时如何处理结果。如果在 then 方法中返回一个值,该值也可以被下一个 then 方法中的函数使用。如果在 then 方法中抛出一个错误,或者在前面的异步操作中出现了错误,那么错误会被传递到 catch 方法中处理。

Promise 为什么可以链式调用?

  1. 如果then的回调函数返回一个Promise实例对象,那么下一个then会得到它的异步结果
  2. 如果then的回调函数没有任何返回值,那么会默认返回一个Promise实例对象
  3. 如果then的回调函数中返回一个具体的数据(如字符串数字等),那么下一个then可以直接获取该数据(会自动包装成Promise对象)
let p1 = new Promise((resolve, reject) => {
  resolve("hello p1");
});

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("hello p2");
  }, 3000);
});

p1.then((ret) => {
  // 获取异步的正常结果
  console.log(ret); // hello p1
  // 此处的返回值是什么?Promise实例对象
  // 如果这里返回Promise实例对象,那么下一个then会得到该异步任务的结果
  return p2;
})
  .then((ret) => {
    console.log(ret); // hello p2
    // 如果这里返回的是普通数据,那么下一个then会得到该数据
    return "hello 普通数据";
  })
  .then((ret) => {
    console.log(ret); // hello 普通数据
    // 如果这里没有任何返回值,那么会默认返回一个Promise实例对象
  })
  .then((ret) => {
    console.log(ret); // undefined
  })
  .catch((err) => {
    // 获取错误的提示信息
    console.log(err);
  });

上面的例子依次打印出:hello p1hello p2hello 普通数据undefined

JavaScript异步编程:(回调函数、Promise、async/await、Generator)_第1张图片

2.4. Promise.all 和 Promise.race 的使用方法

Promise.all 方法用于并行执行多个异步操作,并在所有异步操作都完成时返回结果;
以下是使用 Promise.all 的示例:

const promises = [
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
];

Promise.all(promises)
  .then(values => console.log(values))
  .catch(error => console.error(error));

在此示例中,我们创建了一个 promise 数组并将其传递给 Promise.all。当所有输入的 promises 都被 resolved 时,结果 promise 的 then 方法将被调用,并带有一个解决值的数组。如果任何一个输入的 promise 被 rejected,结果 promise 的 catch 方法将被调用,并带有错误信息。

JavaScript异步编程:(回调函数、Promise、async/await、Generator)_第2张图片

模拟实际项目中 Promise.all() 方法的使用

import { getList, getDetail, getInfo } from "@/api/list.js";

const p1 = new Promise((resolve, reject) => {
  getList({ pageSize: 10, pageNum: 1 })
    .then((res) => {
      resolve(res.data);
    })
    .catch((err) => {
      reject(err);
    });
});
const p2 = new Promise((resolve, reject) => {
  getDetail({ id: 1 })
    .then((res) => {
      resolve(res.data);
    })
    .catch((err) => {
      reject(err);
    });
});
const p3 = new Promise((resolve, reject) => {
  getInfo()
    .then((res) => {
      resolve(res.data);
    })
    .catch((err) => {
      reject(err);
    });
});

Promise.all([p1, p2, p3])
  .then((res) => {
    // 此处的res就是p1, p2, p3传递过来的数组,是一个数组
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

Promise.race 方法用于并行执行多个异步操作,接受一个 promise 数组并返回一个新的 promise,并在其中任何一个异步操作完成时返回结果。
以下是使用 Promise.race 的示例:

const promises = [
  new Promise(resolve => setTimeout(resolve, 1000, 'one')),
  new Promise(resolve => setTimeout(resolve, 2000, 'two')),
  new Promise(resolve => setTimeout(resolve, 3000, 'three'))
];

Promise.race(promises)
  .then(value => console.log(value))
  .catch(error => console.error(error));

在此示例中,我们创建了一个 resolve 速度不同的 promise 数组。当任何一个输入的 promise 被 resolved 时,结果 promise 的 then 方法将被调用,并带有第一个被 resolved 的 promise 的值。如果任何一个输入的 promise 被 rejected,结果 promise 的 catch 方法将被调用,并带有错误信息。

JavaScript异步编程:(回调函数、Promise、async/await、Generator)_第3张图片

3. async/await

3.1. async/await 的基本概念和使用方法

async/await 是JavaScript处理异步操作的较新语法。它允许您编写异步代码,看起来和行为类似于同步代码,使其更易于阅读和维护。

以下是使用async/await的示例:

async function getInfo() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}

getInfo().then(data => console.log(data));

await关键字必须出现在async函数中
async函数中多个await执行顺序是串行的
await后面跟的是什么?一般是Promise实例对象,也可以是普通数据
async函数返回值是Promise实例对象
async函数中依然可以调用async函数

3.2. async/await 和 Promise 的关系

function showInfo () {
  return 'hello'
}

function queryData () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('nihao')
    }, 2000)
  })
}

// async函数的返回值是什么?Promise实例对象
async function getResult () {
  // 异步任务执行结束后可以得到结果
  // await后面跟的是什么?Promise实例对象
  // await接收的值是异步的结果
  let ret = await queryData()
  // await后面如果是普通数据也是可以的
  // let ret1 = await showInfo()
  // console.log(ret)
  // console.log(ret1)
  return ret
  // return new Promise((resolve, reject) => {
  //   resolve(ret)
  // })
}

// let result = getResult()
// result.then(ret => {
//   console.log(ret)
// })


// async函数中依然可以调用async函数
async function testData () {
  
  getResult().then(res => {
    console.log(res)
  })

  // let info = await getResult()
  // console.log(info)
}

testData()

3.3. async/await 的错误处理方法

Async/await 也可以与 try/catch 块一起使用来处理错误。这是一个例子:

async function getInfo() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
  }
}

getInfo();

4. Generator

4.1. Generator 的基本概念和使用方法

function*()是ES6中引入的一种新的函数类型,也被称为生成器函数。它是一种特殊的函数,可以在执行过程中暂停和恢复。在生成器函数中,可以使用yield关键字来暂停函数执行,并返回一个值。然后,可以再次调用生成器函数以恢复函数执行,并继续执行yield之后的代码。

以下是一个简单的生成器函数的示例:

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = generateSequence();

console.log(generator.next().value); // 输出 1
console.log(generator.next().value); // 输出 2
console.log(generator.next().value); // 输出 3

在这个例子中,generateSequence函数是一个生成器函数,它使用yield关键字来暂停函数执行。在generateSequence函数中,我们依次返回1、2和3。然后,我们创建一个生成器对象generator,并使用next()方法来执行生成器函数的下一个步骤。

在第一次调用generator.next()时,函数执行到第一个yield关键字,暂停函数执行,并返回值1。在第二次调用generator.next()时,函数从上次暂停的地方继续执行,并执行到第二个yield关键字,暂停函数执行,并返回值2。在第三次调用generator.next()时,函数从上次暂停的地方继续执行,并执行到最后一个yield关键字,暂停函数执行,并返回值3。

生成器函数可以接受参数,并根据参数来控制函数执行。以下是一个带参数的生成器函数的示例:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

const generator = generateSequence(1, 3);

console.log(generator.next().value); // 输出 1
console.log(generator.next().value); // 输出 2
console.log(generator.next().value); // 输出 3

在这个例子中,generateSequence函数接受两个参数startend,并使用for循环生成从startend的数字序列。然后,我们创建一个生成器对象generator,并使用next()方法来执行生成器函数的下一个步骤。

在第一次调用generator.next()时,函数执行到for循环的第一个步骤,生成1并暂停函数执行。在第二次调用generator.next()时,函数从上次暂停的地方继续执行,生成2并暂停函数执行。在第三次调用generator.next()时,函数从上次暂停的地方继续执行,生成3并暂停函数执行。

生成器函数可以与其他函数结合使用,以创建简单的迭代器。以下是一个使用生成器函数实现的迭代器的示例:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

function iterateSequence(sequence) {
  const iterator = sequence[Symbol.iterator]();
  console.log(iterator, "iterator");

  while (true) {
    const result = iterator.next();
    console.log(result); 
    // result: { value: 1, done: false } { value: 2, done: false } { value: 3, done: false } { value: undefined, done: true }
    if (result.done) {
      break;
    }

    console.log(result.value);
  }
}

// 执行生成器函数
const sequence = generateSequence(1, 3);

// 执行迭代器函数
iterateSequence(sequence);

// 最终输出 1  2  3

在这个例子中,我们定义了一个iterateSequence函数,它接受一个可迭代序列,并使用while循环遍历序列中的所有元素。在iterateSequence函数中,我们使用sequence[Symbol.iterator]()获取序列的迭代器,并使用iterator.next()来获取序列中的下一个元素。如果result.donetrue,则表示序列中没有更多的元素可供遍历,我们就可以退出循环了。

最后,我们创建了一个生成器对象sequence,并将其传递给iterateSequence函数,以遍历序列中的所有元素。

生成器函数是一种很有用的函数类型,在处理迭代器和异步操作时非常有用。

4.2. Generator 和异步编程的结合

生成器可以与异步编程结合使用,以简化和优化异步代码。通过使用生成器来控制异步操作的流程,您可以编写看起来和表现得更像同步代码的异步代码。

以下是使用生成器来控制异步操作流程的示例:

function fetchData(url) {
  return fetch(url)
    .then(response => response.json())
}

function* fetchMultipleData() {
  // 模拟接口调用
  const data1 = yield fetchData('https://api.example.com/data1');
  const data2 = yield fetchData('https://api.example.com/data2');
  const data3 = yield fetchData('https://api.example.com/data3');
  // 返回接口拿回来的数据
  return [data1, data2, data3];
}

function run() {
  const generator = fetchMultipleData();
  let promise = generator.next().value;
  
  // done为true的时候退出迭代
  while (!generator.next(promise).done) {
    promise = promise.then(data => generator.next(data).value);
  }

  promise.then(result => console.log(result));
}

run();

在此示例中,fetchData是一个函数,它返回一个 Promise,该 Promise 解析为使用 fetch API 从 URL 检索的 JSON 数据。fetchMultipleData是一个生成器函数,它使用 yield 暂停执行并等待每个异步操作完成后继续。当所有异步操作完成时,fetchMultipleData 返回一个结果数组。

run 函数负责运行生成器。它创建生成器的新实例,使用 generator.next().value 获取第一个 Promise,然后进入一个循环,直到生成器完成。在循环的每次迭代中,它使用 generator.next(data).value 获取下一个 Promise,其中 data 是上一个异步操作的结果。然后继续下一次循环迭代,将新的 Promise 传递给 generator.next(promise).done。最后,它将已完成的异步操作的结果记录在控制台中。

4.3. Generator 的错误处理方法

使用生成器时,可以在生成器函数内部使用try/catch块来处理错误。以下是一个示例:

function* myGenerator() {
  try {
    yield 1;
    yield 2;
    throw new Error('出现问题了!');
    yield 3;
  } catch (error) {
    console.error(error);
  }
}

const generator = myGenerator();

console.log(generator.next().value); // 输出:1
console.log(generator.next().value); // 输出:2
console.log(generator.next().value); // 输出:日志中记录的错误
console.log(generator.next().value); // 输出:undefined

在这个示例中,myGenerator函数定义了一个生成器,它产生了三个值,然后抛出一个错误。在生成器函数内部,我们将yield语句包装在try块中,并使用catch块捕获任何错误。当抛出错误时,catch块将错误记录到控制台。

然后,我们创建了一个生成器对象generator,并使用它的next()方法迭代生成器产生的值。前两次调用generator.next()返回前两个产生的值,而第三次调用将错误记录到控制台。第四次调用generator.next()返回undefined,因为生成器函数已经执行完毕。

还可以通过将生成器包装在try/catch块中来在生成器函数外部处理错误。以下是一个示例:

function* myGenerator() {
  yield 1;
  yield 2;
  throw new Error("出现问题了!");
  yield 3;
}

try {
  const generator = myGenerator();
  console.log(generator.next().value); // 输出:1
  console.log(generator.next().value); // 输出:2
  console.log(generator.next().value); // 输出:抛出错误
  console.log(generator.next().value); // 不会输出,执行已经结束了
} catch (error) {
  console.error(error);
}

在这个示例中,我们将整个生成器函数包装在try/catch块中。当生成器函数内部抛出错误时,由函数外部的catch块捕获,并将错误记录到控制台。

处理生成器中的错误非常重要,以确保意外错误不会导致程序崩溃。

5. 其他异步编程技术

5.1. 事件监听器

事件监听器是JavaScript中的一种强大工具,它允许您响应浏览器中发生的特定事件。要添加事件监听器,首先需要选择要侦听事件的元素,例如使用getElementByIdquerySelectorquerySelectorAll等方法。一旦您获得了元素的引用,就可以调用addEventListener方法,传递你想要侦听的事件类型(例如clickmouseoversubmit),以及在事件发生时要调用的函数。

以下是向按钮元素添加单击事件监听器的示例:

const myButton = document.getElementById('my-button');

myButton.addEventListener('click', function() {
  console.log('Button clicked!');
});

还可以通过使用querySelectorAll方法选择一组元素,然后循环遍历它们以向每个元素添加事件监听器,从而一次性向多个元素添加事件监听器。

const allButtons = document.querySelectorAll('button');

for (let i = 0; i < allButtons.length; i++) {
  allButtons[i].addEventListener('click', function() {
    console.log('Button clicked!');
  });
}

5.2. setInterval 和 setTimeout

setTimeout允许你在指定的时间后执行一段代码。 setTimeout的语法如下:

setTimeout(function, milliseconds);

function参数是要执行的函数,milliseconds参数是在执行函数之前要等待的时间(以毫秒为单位)。

例如,以下代码将在页面加载完成后3秒将 Hello, world! 记录到控制台:

setTimeout(function() {
  console.log("Hello, world!");
}, 3000);

setInterval类似于setTimeout,但是它将在指定的间隔重复执行代码。

setInterval的语法如下:

setInterval(function, milliseconds);

function参数是要执行的函数,milliseconds参数是每次执行函数之间要等待的时间(以毫秒为单位)。

例如,以下代码将每秒将当前时间记录到控制台:

setInterval(function() {
  console.log(new Date().toLocaleTimeString());
}, 1000);

setTimeoutsetInterval都是在JavaScript应用程序中创建基于时间的功能的有用工具。 但是,重要的是要小心使用它们,并避免在用户设备上创建过多的负载。

6. 异步编程的最佳实践

6.1. 如何优化异步编程性能

优化异步编程性能的几种方法:

  1. 避免回调地狱:回调地狱是指多层嵌套的回调函数,会使代码难以阅读和维护。可以使用 Promiseasync/await 来避免回调地狱。
  2. 合并请求:在发送多个异步请求时,可以将它们合并为一个请求,以减少网络流量和请求次数
  3. 缓存数据:如果可能的话,可以缓存已检索的数据,以避免重复检索。
  4. 控制并发:如果多个异步操作需要同时进行,可以使用 Promise.all 或其他类似的方法来控制并发数量,以避免过多的负载。
  5. 使用 Web WorkersWeb Workers 允许在后台线程中运行 JavaScript 代码,以避免阻塞 UI 线程。
  6. 优化 I/O 操作:使用流和缓冲区来优化 I/O 操作,以减少内存使用和提高性能。
  7. 减少 DOM 操作:DOM 操作通常很慢,可以使用虚拟 DOM 或其他技术来减少 DOM 操作数量和频率

你可能感兴趣的:(javascript,javascript,开发语言,ecmascript)