文章目录
- 前言
- 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 解析为使用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中的一种强大工具,它允许您响应浏览器中发生的特定事件。要添加事件监听器,首先需要选择要侦听事件的元素,例如使用
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 操作的数量和频率。