文章目录
- 前言
- 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 中有几种异步编程技术,包括回调函数、Promise、async/await 和生成器。
以下是 JavaScript 异步编程的简单概述:
1. 回调函数
1.1. 回调函数的基本概念和使用方法
JavaScript 中回调函数是指将一个函数作为参数传递给另一个函数,并在另一个函数执行完后调用该函数。回调函数通常用于处理异步操作,比如向服务器发送请求并在请求返回后执行某些操作。
以下是回调函数的基本使用方法:
// 1. 创建一个需要执行的函数。
function doSomething(callback) {
  console.log("doSomething");
  callback();
}
// 2. 创建一个回调函数。
function callback() {
  console.log("callback");
}
// 3. 调用需要执行的函数,并将回调函数作为参数传递进去。
doSomething(callback);
以上代码将依次输出 doSomething 和 callback。
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. 回调地狱和如何避免
为了避免回调地狱,建议使用 Promise 或 async/await。Promises 允许链接异步操作,而 async/await 则使编写异步代码更容易以同步方式进行。此外,将复杂的回调拆分为较小的函数也可以帮助提高可读性和可维护性。
2. Promise
2.1. Promise 的基本概念和使用方法
Promise是JavaScript中一种用于处理异步操作的对象。Promise对象表示一个可能还没有完成的异步操作,并且可以指定在异步操作完成时如何处理结果。
以下是 Promise 的基本使用方法:
const promise = new Promise(function(resolve, reject) {
  // 异步操作
  if (/* 异步操作成功 */) {
    resolve(value);
  } else {
    reject(error);
  }
});
promise.then(function(value) {
  // 异步操作成功时的处理
}, function(error) {
  // 异步操作失败时的处理
});
在这个例子中,
promise对象表示一个异步操作。new Promise构造函数接受一个函数作为参数,这个函数又接受两个函数作为参数:resolve和reject。当异步操作成功时,调用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 为什么可以链式调用?
- 如果then的回调函数返回一个Promise实例对象,那么下一个then会得到它的异步结果
- 如果then的回调函数没有任何返回值,那么会默认返回一个Promise实例对象
- 如果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 p1、hello p2、hello 普通数据、undefined

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方法将被调用,并带有错误信息。

模拟实际项目中 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方法将被调用,并带有错误信息。

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函数接受两个参数start和end,并使用for循环生成从start到end的数字序列。然后,我们创建一个生成器对象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.done为true,则表示序列中没有更多的元素可供遍历,我们就可以退出循环了。
最后,我们创建了一个生成器对象
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 解析为使用fetchAPI 从 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中的一种强大工具,它允许您响应浏览器中发生的特定事件。要添加事件监听器,首先需要选择要侦听事件的元素,例如使用
getElementById、querySelector或querySelectorAll等方法。一旦您获得了元素的引用,就可以调用addEventListener方法,传递你想要侦听的事件类型(例如click、mouseover或submit),以及在事件发生时要调用的函数。
以下是向按钮元素添加单击事件监听器的示例:
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);
setTimeout和setInterval都是在JavaScript应用程序中创建基于时间的功能的有用工具。 但是,重要的是要小心使用它们,并避免在用户设备上创建过多的负载。
6. 异步编程的最佳实践
6.1. 如何优化异步编程性能
优化异步编程性能的几种方法:
- 避免回调地狱:回调地狱是指多层嵌套的回调函数,会使代码难以阅读和维护。可以使用 Promise或async/await来避免回调地狱。
- 合并请求:在发送多个异步请求时,可以将它们合并为一个请求,以减少网络流量和请求次数。
- 缓存数据:如果可能的话,可以缓存已检索的数据,以避免重复检索。
- 控制并发:如果多个异步操作需要同时进行,可以使用 Promise.all或其他类似的方法来控制并发数量,以避免过多的负载。
- 使用 Web Workers:Web Workers允许在后台线程中运行JavaScript代码,以避免阻塞UI线程。
- 优化 I/O 操作:使用流和缓冲区来优化 I/O 操作,以减少内存使用和提高性能。
- 减少 DOM 操作:DOM 操作通常很慢,可以使用虚拟 DOM或其他技术来减少 DOM 操作的数量和频率。



















