异步JS
1.同步
在同步编程中,浏览器是按照我们书写代码的顺序一行一行地执行程序的。
浏览器会等待代码的解析和工作,在上一行完成后才会执行下一行。这样做是很有必要的,因为每一行新的代码都是建立在前面代码的基础之上的。
1.1 耗时同步函数的问题
当页面中有一个较为耗时的同步函数时,用户不能输入任何东西,也不能点击任何东西,或做任何其他事情。
这就是耗时的同步函数的基本问题。
因此,在这里我们想要的是一种方法,以让我们的程序可以:
- 通过调用一个函数来启动一个长期运行的操作
- 让函数开始操作并立即返回,这样我们的程序就可以保持对其他事件做出反应的能力
- 当操作最终完成时,通知我们操作的结果。
这就是异步函数为我们提供的能力。
2.JS实现异步的两种传统方式
2.1 事件处理函数
事件处理程序实际上就是异步编程的一种形式:你提供的函数(事件处理程序)将在事件发生时被调用(而不是立即被调用)。
如果“事件”是“异步操作已经完成”,那么你就可以看到事件如何被用来通知调用者异步函数调用的结果的。
一些早期的异步 API 正是以这种方式来使用事件的。
XMLHttpRequest
API 可以让你用 JavaScript 向远程服务器发起 HTTP 请求。由于这样的操作可能需要很长的时间,所以它被设计成异步 API,你可以通过给 XMLHttpRequest
对象附加事件监听器来让程序在请求进展和最终完成时获得通知。
例:
html
<button id="xhr">点击发起请求</button>
<button id="reload">重载</button>
<pre readonly class="event-log"></pre>
js
const log = document.querySelector('.event-log');
document.querySelector('#xhr').addEventListener('click', () => {
log.textContent = '';
const xhr = new XMLHttpRequest();
xhr.addEventListener('loadend', () => {
log.textContent = `${log.textContent}完成!状态码:${xhr.status}`;
});
xhr.open('GET', 'https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json');
xhr.send();
log.textContent = `${log.textContent}请求已发起\n`;});
document.querySelector('#reload').addEventListener('click', () => {
log.textContent = '';
document.location.reload();
});
我们在添加了事件监听器后发送请求。注意,在这之后,我们仍然可以在控制台中输出“请求已发起”,也就是说,我们的程序可以在请求进行的同时继续运行,而我们的事件处理程序将在请求完成时被调用。
2.2 回调函数
回调函数 — 一个被传递到另一个函数中的会在适当的时候被调用的函数
实际上,事件处理函数就是一种特殊类型的回调函数。
而且,回调函数曾经是 JS 中实现异步函数的主要方式。
然而,当回调函数本身需要调用其他同样接受回调函数的函数时,基于回调的代码会变得难以理解。这种情况在需要执行一些分解成一系列异步函数的操作时十分常见。
例如下面这个例子:
function doStep1(init) {
return init + 1;
}
function doStep2(init) {
return init + 2;
}
function doStep3(init) {
return init + 3;
}
function doOperation() {
let result = 0;
result = doStep1(result);
result = doStep2(result);
result = doStep3(result);
console.log(`结果:${result}`);
}
doOperation();
现在我们有一个被分成三步的操作,每一步都依赖于上一步。在这个例子中,第一步给输入的数据加 1,第二步加 2,第三步加 3。从输入 0 开始,最终结果是 6(0+1+2+3)。作为同步代码,这很容易理解。但是如果我们用回调来实现这些步骤呢?
function doStep1(init, callback) {
const result = init + 1;
callback(result);
}
function doStep2(init, callback) {
const result = init + 2;
callback(result);
}
function doStep3(init, callback) {
const result = init + 3;
callback(result);
}
//回调地狱
function doOperation() {
doStep1(0, result1 => {
doStep2(result1, result2 => {
doStep3(result2, result3 => {
console.log(`结果:${result3}`);
});
});
});
}
doOperation();
因为必须在回调函数中调用回调函数,我们就得到了这个深度嵌套的 doOperation()
函数,这就更难阅读和调试了。
即所谓的回调地狱。
而面对这样的嵌套回调,处理错误也会变得非常困难:你必须在“金字塔”的每一级处理错误,而不是在最高一级一次完成错误处理。
由于以上这些原因,大多数现代异步 API 都不使用回调。事实上,JavaScript 中异步编程的基础是 Promise
。
3.Promise
Promise 是现代 JavaScript 中异步编程的基础,是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。
在 Promise 返回给调用者的时候,操作往往还没有完成,但 Promise 对象可以让我们操作最终完成时对其进行处理(无论成功还是失败)。
简单的来说,在基于 Promise 的 API 中,异步函数会启动操作并返回 Promise
对象。然后,你可以将处理函数附加到 Promise 对象上,当操作完成时(成功或失败),这些处理函数将被执行。
详细的 Promise 学习笔记见…