同步js
要理解什么是异步js,我们应该从确切理解同步js 开始。下面我们来一个例子。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Simple synchronous JavaScript example</title> </head> <body> <button>Click me</button> <script> const btn = document.querySelector('button') btn.addEventListener('click', () => { alert('You clicked me!') let pElem = document.createElement('p') pElem.textContent = 'This is a newly-added paragraph.' document.body.appendChild(pElem) }) </script> </body> </html>
这段代码, 一行一行的顺序执行:
每一个操作在执行的时候,其他任何事情都没有发生 — 网页的渲染暂停。js 传统上是单线程的。即使有多个内核,也只能在单一线程上运行多个任务,此线程称为主线程,任何时候只能做一件事情, 其他的事情都阻塞了,直到前面的操作完成。
所以上面的例子,点击了按钮以后,段落不会创建,直到在alert消息框中点击ok,段落才会出现。
异步js
为什么使用异步代码这么难?看一个例子,当你从服务器获取一个图像,通常你不可能立马就得到,这需要时间,虽然现在的网络很快。这意味着下面的伪代码可能不能正常工作:
var response = fetch('myImage.png'); var blob = response.blob(); // display your image blob in the UI somehow
因为你不知道下载图片会多久,所以第二行代码执行的时候可能报错(可能间歇的,也可能每次)因为图像还没有就绪。取代的方法就是,代码必须等到 response
返回才能继续往下执行。
在js代码中,你经常会遇到两种异步编程风格:老派callbacks,新派promise。下面就来分别介绍。
异步callbacks
异步callbacks 其实就是函数,只不过是作为参数传递给那些在后台执行的其他函数. 当那些后台运行的代码结束,就调用callbacks函数,通知你工作已经完成,或者其他有趣的事情发生了。使用callbacks 有一点老套,在一些老派但经常使用的API里面,你会经常看到这种风格。
当我们把回调函数作为一个参数传递给另一个函数时,仅仅是把回调函数定义作为参数传递过去 — 回调函数并没有立刻执行,回调函数会在包含它的函数的某个地方异步执行,包含函数负责在合适的时候执行回调函数。
你可以自己写一个容易的,包含回调函数的函数。来看另外一个例子,用 XMLHttpRequest
API 加载资源:
function loadAsset(url, type, callback) { let xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = type; xhr.onload = function() { callback(xhr.response); }; xhr.send(); } function displayImage(blob) { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } loadAsset('coffee.jpg', 'blob', displayImage);
创建 displayImage()
函数,简单的把blob传递给它,生成objectURL,然后再生成一个image元素,把objectURL作为image的源地址,最后显示这张图片。 然后,我们创建 loadAsset()
函数,把URL,type,和回调函数同时都作为参数。函数用 XMLHttpRequest
(通常缩写 “XHR”) 获取给定URL的资源,在获得资源响应后再把响应作为参数传递给回调函数去处理。
回调函数用途广泛 — 他们不仅仅可以用来控制函数的执行顺序和函数之间的数据传递,还可以根据环境的不同,将数据传递给不同的函数,所以对下载好的资源,你可以采用不同的操作来处理,譬如 processJSON()
, displayText()
, 等等。
Promise
Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。它让你能够把异步操作最终的成功返回值或者失败原因和相关的处理程序关联起来,这样使得异步方法并不会立刻返回最终的值,而是会返回一个promise,以便在未来某个时候把值交给使用者。
一个Promise必然处于几种状态之一:
- 创建promise时,它既不是成功也不是失败状态。这个状态叫作pending(待定)。
- 当promise返回时,称为 resolved(已解决).
- 一个成功resolved的promise称为fullfilled(实现)。它返回一个值,可以通过将
.then()
块链接到promise链的末尾来访问该值。.then()
块中的执行程序函数将包含promise的返回值。 - 一个不成功resolved的promise被称为rejected(拒绝)了。它返回一个原因(reason),一条错误消息,说明为什么拒绝promise。可以通过将
.catch()
块链接到promise链的末尾来访问此原因。
- 一个成功resolved的promise称为fullfilled(实现)。它返回一个值,可以通过将
因为Promise.prototype.then和Promise.prototype.catch方法返回的事promise,所以它们可以被链式调用

下面我们看一段代码
fetch('products.json').then(function(response) {
return response.json();
}).then(function(json) {
products = json;
initialize();
}).catch(function(err) {
console.log('Fetch problem: ' + err.message);
});
这里fetch()
只需要一个参数— 资源的网络 URL — 返回一个 promise. promise 是表示异步操作完成或失败的对象。可以说,它代表了一种中间状态。 本质上,这是浏览器说“我保证尽快给您答复”的方式,因此得名“promise”。
fetch操作目前正在等待浏览器试图在将来某个时候完成该操作的结果。然后我们有三个代码块链接到fetch()的末尾:
- 两个 then()块。两者都包含一个回调函数,如果前一个操作成功,该函数将运行,并且每个回调都接收前一个成功操作的结果作为输入,因此您可以继续对它执行其他操作。每个
.then()
块返回另一个promise,这意味着可以将多个.then()
块链接到另一个块上,这样就可以依次执行多个异步操作。 - 如果其中任何一个
then()
块失败,则在末尾运行catch()
块——与同步try…catch类似,catch()提供了一个错误对象,可用来报告发生的错误类型。但是请注意,同步try…catch不能与promise一起工作,尽管它可以与async/await一起工作。
事件队列
像promise这样的异步操作被放入事件队列中,事件队列在主线程完成处理后运行,这样它们就不会阻止后续js代码的运行。排队操作将尽快完成,然后将结果返回到js环境。
promise vs callback
要完全理解为什么 promise 是一件好事,应该回想之前的回调函数,理解它们造成的困难。
我们来谈谈订购披萨作为类比。为了使你的订单成功,你必须按顺序执行,不按顺序执行或上一步没完成就执行下一步是不会成功的:
- 选择配料。如果你是优柔寡断,这可能需要一段时间,如果你无法下定决心或者决定换咖喱,可能会失败。
- 下订单。返回比萨饼可能需要一段时间,如果餐厅没有烹饪所需的配料,可能会失败。
- 然后你收集你的披萨吃。如果你忘记了自己的钱包,那么这可能会失败,所以无法支付比萨饼的费用!
对于callbacks,上述功能的伪代码表示可能如下所示:
chooseToppings(function(toppings) { placeOrder(toppings, function(order) { collectOrder(order, function(pizza) { eatPizza(pizza); }, failureCallback); }, failureCallback); }, failureCallback);
这很麻烦且难以阅读(通常称为“回调地狱”),需要多次调用failureCallback()
(每个嵌套函数一次),还有其他问题。
使用promise改良
Promises使得上面的情况更容易编写,解析和运行。如果我们使用异步promises代表上面的伪代码,我们最终会得到这样的结果:
chooseToppings()
.then(function(toppings) {
return placeOrder(toppings);
})
.then(function(order) {
return collectOrder(order);
})
.then(function(pizza) {
eatPizza(pizza);
})
.catch(failureCallback);
这要好得多 – 更容易看到发生了什么,我们只需要一个.catch()
块来处理所有错误,它不会阻塞主线程(所以我们可以在等待时继续玩视频游戏为了准备好收集披萨),并保证每个操作在运行之前等待先前的操作完成。我们能够以这种方式一个接一个地链接多个异步操作,因为每个.then()
块返回一个新的promise,当.then()
块运行完毕时它会解析
我们还可以使用 async/await
语法进行进一步的改进。
async/await
async functions 和 await 关键字是最近添加到js语言里面的。它们是ECMAScript 2017 JavaScript版的一部分。简单来说,它们是基于promises的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码。
async关键字
首先,我们使用 async 关键字,把它放在函数声明之前,使其成为 async function。异步函数是一个知道怎样使用 await 关键字调用异步代码的函数。
尝试在浏览器的JS控制台中键入以下行:
function hello() { return 'Hello' } hello()
该函数返回“Hello” —— 没什么特别的,对吧?
如果我们将其变成异步函数呢?请尝试以下方法:
async function hello() { return "Hello" };
hello();
现在调用该函数会返回一个 promise。这是异步函数的特征之一 —— 它保证函数的返回值为 promise。
你也可以创建一个异步函数表达式(参见 async function expression ),如下所示:
let hello = async function() { return "Hello" }; hello();
要实际使用promise完成时返回的值,我们可以使用.then()
块,因为它返回的是 promise:
hello().then((value) => console.log(value))
甚至只是简写如
hello().then(console.log)
将 async
关键字加到函数申明中,可以告诉它们返回的是 promise,而不是直接返回值。此外,它避免了同步函数为支持使用 await 带来的任何潜在开销。在函数声明为 async
时,js引擎会添加必要的处理,以优化你的程序。
await关键字
当 await关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了
您可以在调用任何返回Promise的函数时使用 await,包括Web API函数。
这是一个简单的示例:
async function hello() {
return greeting = await Promise.resolve("Hello");
};
hello().then(alert);
使用 async/await 重写 promise 代码
fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob)
let image = document.createElement('img')
image.src = objectURL
document.body.appendChild(image)
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message)
})
到现在为止,你应该对 promises 及其工作方式有一个较好的理解。让我们将其转换为使用async / await看看它使事情变得简单了多少:
async function myFetch() {
let response = await fetch('coffee.jpg');
let myBlob = await response.blob();
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
}
myFetch()
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
它使代码简单多了,更容易理解 —— 去除了到处都是 .then()
代码块!
由于 async
关键字将函数转换为 promise,您可以重构以上代码 —— 使用 promise 和 await 的混合方式,将函数的后半部分抽取到新代码块中。这样做可以更灵活:
async function myFetch() {
let response = await fetch('coffee.jpg');
return await response.blob();
}
myFetch().then((blob) => {
let objectURL = URL.createObjectURL(blob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
});
它到底是如何工作的?
在myFetch()
函数定义中,您可以看到代码与先前的 promise 版本非常相似,但存在一些差异。不需要附加 .then()
代码块到每个promise-based方法的结尾,你只需要在方法调用前添加 await 关键字,然后把结果赋给变量。await 关键字使js运行时暂停于此行,允许其他代码在此期间执行,直到异步函数调用返回其结果。一旦完成,您的代码将继续从下一行开始执行。
let response = await fetch('coffee.jpg')
解析器会在此行上暂停,直到当服务器返回的响应变得可用时。此时 fetch()
返回的 promise 将会完成(fullfilled),返回的 response 会被赋值给 response
变量。一旦服务器返回的响应可用,解析器就会移动到下一行,从而创建一个Blob
。Blob这行也调用基于异步promise的方法,因此我们也在此处使用await
。当操作结果返回时,我们将它从myFetch()
函数中返回。
async/await的缺陷
了解Async/await
是非常有用的,但还有一些缺点需要考虑。
Async/await
让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。 await
关键字会阻塞其后的代码,直到promise完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但您自己的代码被阻塞。
这意味着您的代码可能会因为大量await
的promises相继发生而变慢。每个await
都会等待前一个完成,而你实际想要的是所有的这些promises同时开始处理(就像我们没有使用async/await
时那样)。
有一种模式可以缓解这个问题——通过将 Promise
对象存储在变量中来同时开始它们,然后等待它们全部执行完毕。
参考资料:
- https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Concepts
- https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Introducing#%E5%90%8C%E6%AD%A5javascript
- https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Promises
- https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Async_await