异步JS(Asynchronous JavaScript)
MDN原文
通用异步编程概念
产生阻塞的代码
异步技术非常有用,特别是在web编程。当浏览器里面的一个web应用进行密集运算还没有把控制权返回给浏览器的时候,整个浏览器就像冻僵了一样,这叫做阻塞;这时候浏览器无法继续处理用户的输入并执行其他任务,直到web应用交回处理器的控制。
线程
一个线程是一个基本的处理过程,程序用它来完成任务。每个线程一次只能执行一个任务:
Task A --> Task B --> Task C
正如我们之前所说,现在的计算机大都有多个内核(core),因此可以同时执行多个任务。支持多线程的编程语言可以使用计算机的多个内核,同时完成多个任务:
Thread 1: Task A --> Task B Thread 2: Task C --> Task D
JavaScript 是单线程的
JavaScript 传统上是单线程的。即使有多个内核,也只能在单一线程上运行多个任务,此线程称为主线程(main thread)。我们上面的例子运行如下:
Main thread: Render circles to canvas --> Display alert()
同步JavaScript
异步JavaScript
在JavaScript代码中,你经常会遇到两种异步编程风格:老派callbacks,新派promise。下面就来分别介绍。
异步callbacks
异步callbacks 其实就是函数,只不过是作为参数传递给那些在后台执行的其他函数. 当那些后台运行的代码结束,就调用callbacks函数,通知你工作已经完成,或者其他有趣的事情发生了。使用callbacks 有一点老套,在一些老派但经常使用的API里面,你会经常看到这种风格。
- 回调函数不会立刻执行,回调函数会在包含它的函数的某个地方异步执行,包含函数负责在合适的时候执行回调函数。
- 回调函数用途广泛 — 他们不仅仅可以用来控制函数的执行顺序和函数之间的数据传递,还可以根据环境的不同,将数据传递给不同的函数
- 请注意,不是所有的回调函数都是异步的 — 有一些是同步的。一个例子就是使用
Array.prototype.forEach()
来遍历数组
Promises
Promises 是新派的异步代码,现代的web APIs经常用到。
fetch()
API就是一个很好的例子, 它基本上就是一个现代版的,更高效的XMLHttpRequest
。1 fetch('products.json').then(function(response) { 2 return response.json(); 3 }).then(function(json) { 4 products = json; 5 initialize(); 6 }).catch(function(err) { 7 console.log('Fetch problem: ' + err.message); 8 });这里
fetch
()
只需要一个参数— 资源的网络 URL — 返回一个 promise. promise 是表示异步操作完成或失败的对象。可以说,它代表了一种中间状态。 本质上,这是浏览器说“我保证尽快给您答复”的方式,因此得名“promise”。这个概念需要练习来适应;它感觉有点像运行中的薛定谔猫。这两种可能的结果都还没有发生,因此fetch操作目前正在等待浏览器试图在将来某个时候完成该操作的结果。然后我们有三个代码块链接到fetch()的末尾:
- 两个
then()
块。两者都包含一个回调函数,如果前一个操作成功,该函数将运行,并且每个回调都接收前一个成功操作的结果作为输入,因此您可以继续对它执行其他操作。每个.then()
块返回另一个promise,这意味着可以将多个.then()
块链接到另一个块上,这样就可以依次执行多个异步操作。- 如果其中任何一个
then()
块失败,则在末尾运行catch()
块——与同步try...catch
类似,catch()
提供了一个错误对象,可用来报告发生的错误类型。但是请注意,同步try...catch
不能与promise一起工作,尽管它可以与async/await一起工作,稍后您将了解到这一点。
通常在旧式API中找到,涉及将函数作为参数传递给另一个函数,然后在异步操作完成时调用该函数,以便回调可以依次对结果执行某些操作。这是promise的先导;它不那么高效或灵活。仅在必要时使用。回调函数的缺陷:
- 嵌套回调可能很麻烦且难以阅读(即“回调地狱”)
- 每层嵌套都需要调用一次失败回调,而使用promises,您只需使用一个
.catch()
代码块来处理整个链的错误。- 异步回调不是很优雅。
- Promise回调总是按照它们放在事件队列中的严格顺序调用;异步回调不是。
事件队列
像promise这样的异步操作被放入事件队列中,事件队列在主线程完成处理后运行,这样它们就不会阻止后续JavaScript代码的运行。排队操作将尽快完成,然后将结果返回到JavaScript环境。
Promises 对比 callbacks
promises与旧式callbacks有一些相似之处。它们本质上是一个返回的对象,您可以将回调函数附加到该对象上,而不必将回调作为参数传递给另一个函数。
然而,
Promise
是专门为异步操作而设计的,与旧式回调相比具有许多优点:
- 您可以使用多个then()操作将多个异步操作链接在一起,并将其中一个操作的结果作为输入传递给下一个操作。这种链接方式对回调来说要难得多,会使回调以混乱的“末日金字塔”告终。 (也称为回调地狱)。
Promise
总是严格按照它们放置在事件队列中的顺序调用。- 错误处理要好得多——所有的错误都由块末尾的一个.catch()块处理,而不是在“金字塔”的每一层单独处理。
小结
在最基本的形式中,JavaScript是一种同步的、阻塞的、单线程的语言,在这种语言中,一次只能执行一个操作。但web浏览器定义了函数和API,允许我们当某些事件发生时不是按照同步方式,而是异步地调用函数(比如,时间的推移,用户通过鼠标的交互,或者获取网络数据)。这意味着您的代码可以同时做几件事情,而不需要停止或阻塞主线程。
异步还是同步执行代码,取决于我们要做什么。
有些时候,我们希望事情能够立即加载并发生。例如,当将一些用户定义的样式应用到一个页面时,您希望这些样式能够尽快被应用。
但是,如果我们正在运行一个需要时间的操作,比如查询数据库并使用结果填充模板,那么最好将该操作从主线程中移开使用异步完成任务。随着时间的推移,您将了解何时选择异步技术比选择同步技术更有意义。
合作异步JavaScript: 超时和间隔
介绍
很长一段时间以来,web平台为JavaScript程序员提供了许多函数,这些函数允许您在一段时间间隔过后异步执行代码,或者重复异步执行代码块,直到您告诉它停止为止。这些都是:
setTimeout()
在指定的时间后执行一段代码.
setInterval()
以固定的时间间隔,重复运行一段代码.
requestAnimationFrame()
setInterval()的现代版本;在浏览器下一次重新绘制显示之前执行指定的代码块,从而允许动画在适当的帧率下运行,而不管它在什么环境中运行.这些函数设置的异步代码实际上在主线程上运行,但是您可以在迭代之间运行其他代码,运行效率或高或低,这取决于这些操作的处理器密集程度。无论如何,这些函数用于在web站点或应用程序上运行常量动画和其他后台处理。
setTimeout()
在指定的时间后执行一段代码. 它需要如下参数:
- 要运行的函数,或者函数引用。
- 表示在执行代码之前等待的时间间隔(以毫秒为单位,所以1000等于1秒)的数字。如果指定值为0(或完全省略该值),函数将立即运行。稍后详述这样做的原因.
- 更多的参数:在指定函数运行时,希望传递给函数的值.
Note: 因为超时回调是协同执行的,所以不能保证在指定的确切时间之后调用它们。相反,它们将在至少经过那么多时间之后被调用。超时处理程序在主线程到达执行点之前无法运行,在执行点上,它将浏览这些处理程序,选择需要运行的那个来运行。
在下面的示例中,浏览器将在执行匿名函数之前等待两秒钟,然后显示alert消息
1 let myGreeting = setTimeout(function() { 2 alert('Hello, Mr. Universe!'); 3 }, 2000)我们指定的函数不必是匿名的。我们可以给函数一个名称,甚至可以在其他地方定义它,并将函数引用传递给setTimeout()。
例如,如果我们有一个函数既需要从超时调用,也需要响应某个事件,那么这将非常有用。此外它也可以帮助保持代码整洁,特别是当超时回调超过几行代码时
1 // With a named function 2 let myGreeting = setTimeout(function sayHi() { 3 alert('Hello, Mr. Universe!'); 4 }, 2000) 5 6 // With a function defined separately 7 function sayHi() { 8 alert('Hello Mr. Universe!'); 9 } 10 11 let myGreeting = setTimeout(sayHi, 2000);
setTimeout()
返回一个标志符变量用来引用这个间隔,可以稍后用来取消这个超时任务,下面就会学到 Clearing timeouts 。传递参数给setTimeout()
我们希望传递给
setTimeout()
中运行的函数的任何参数,都必须作为列表末尾的附加参数传递给它。例如,我们可以重构之前的函数,这样无论传递给它的人的名字是什么,它都会向它打招呼:1 function sayHi(who) { 2 alert('Hello ' + who + '!'); 3 }人名可以通过第三个参数传进
setTimeout()
:1 let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');
清除超时
最后,如果创建了超时,您可以通过调用
clearTimeout()
,将setTimeout()
调用的标识符作为参数传递给它,从而在超时运行之前取消。要取消上面的超时,你需要这样做:1 clearTimeout(myGreeting);
setInterval()
与
setTimeout()
的工作方式非常相似,只是作为第一个参数传递给它的函数,重复执行的时间不少于第二个参数给出的毫秒数,而不是一次执行。您还可以将正在执行的函数所需的任何参数作为setInterval()调用的后续参数传递。让我们看一个例子。下面的函数创建一个新的
Date()
对象,使用toLocaleTimeString()
从中提取一个时间字符串,然后在UI中显示它。然后,我们使用setInterval()
每秒运行该函数一次,创建一个每秒更新一次的数字时钟的效果。1 function displayTime() { 2 let date = new Date(); 3 let time = date.toLocaleTimeString(); 4 document.getElementById('demo').textContent = time; 5 } 6 7 const createClock = setInterval(displayTime, 1000);
像setTimeout()一样
,setInterval()
返回一个确定的值,稍后你可以用它来取消间隔任务。清除intervals
setInterval
()永远保持运行任务,除非我们做点什么——我们可能会想阻止这样的任务,否则当浏览器无法完成任何进一步的任务时我们可能得到错误, 或者动画处理已经完成了。我们可以用与停止超时相同的方法来实现这一点——通过将setInterval
()调用返回的标识符传递给clearInterval
()函数:
1 const myInterval = setInterval(myFunction, 2000); 2 clearInterval(myInterval);
关于 setTimeout() 和 setInterval() 需要注意的几点
递归的timeouts
还有另一种方法可以使用
setTimeout()
:我们可以递归调用它来重复运行相同的代码,而不是使用setInterval()
。下面的示例使用递归
setTimeout()
每100毫秒运行传递来的函数:1 let i = 1; 2 3 setTimeout(function run() { 4 console.log(i); 5 i++; 6 setTimeout(run, 100); 7 }, 100);将上面的示例与下面的示例进行比较 ––这使用setInterval()来实现相同的效果:
1 let i = 1; 2 3 setInterval(function run() { 4 console.log(i); 5 i++ 6 }, 100);递归setTimeout()和setInterval()有何不同?
上述代码的两个版本之间的差异是微妙的。
- 递归
setTimeout()
保证执行之间的延迟相同,例如在上述情况下为100ms。 代码将运行,然后在它再次运行之前等待100ms,因此无论代码运行多长时间,间隔都是相同的。- 使用
setInterval()
的示例有些不同。 我们选择的间隔包括执行我们想要运行的代码所花费的时间。假设代码需要40毫秒才能运行 - 然后间隔最终只有60毫秒。- 当递归使用
setTimeout()
时,每次迭代都可以在运行下一次迭代之前计算不同的延迟。 换句话说,第二个参数的值可以指定在再次运行代码之前等待的不同时间(以毫秒为单位)。当你的代码有可能比你分配的时间间隔,花费更长时间运行时,最好使用递归的
setTimeout()
- 这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误。立即超时
使用0用作setTimeout()的回调函数会立刻执行,但是在主线程代码运行之后执行。
如果您希望设置一个代码块以便在所有主线程完成运行后立即运行,这将很有用。将其放在异步事件循环中,这样它将随后直接运行。
使用 clearTimeout() or clearInterval()清除
clearTimeout()
和clearInterval()
都使用相同的条目列表进行清除。有趣的是,这意味着你可以使用任一一种方法来清除setTimeout()
和setInterval()。
但为了保持一致性,你应该使用
clearTimeout()
来清除setTimeout()
条目,使用clearInterval()
来清除setInterval()
条目。 这样有助于避免混乱。
requestAnimationFrame()
Promise 是 JavaScript 语言的一个相对较新的功能,允许您推迟进一步的操作,直到上一个操作完成或响应其失败。这对于设置一系列异步操作以正常工作非常有用。本文向您展示了promises如何工作,如何在Web API中使用它们以及如何编写自己的APIPromise 对象本质上表示的是一系列操作的中间状态,或者说是未来某时刻一个操作完成或失败后返回的结果。Promise并不保证操作在何时完成并返回结果,但是保证在当前操作成功后执行您对操作结果的处理代码,或在操作失败后,优雅地处理操作失败的情况。注意: promise中的
.then()/catch()
块基本上是同步代码中try...catch
块的异步等价物。请记住,同步try ... catch
在异步代码中不起作用。
Promise术语
- 创建promise时,它既不是成功也不是失败状态。这个状态叫作pending(待定)。
- 当promise返回时,称为 resolved(已解决).
- 一个成功resolved的promise称为fullfilled(实现)。它返回一个值,可以通过将
.then()
块链接到promise链的末尾来访问该值。.then()
块中的执行程序函数将包含promise的返回值。- 一个不成功resolved的promise被称为rejected(拒绝)了。它返回一个原因(reason),一条错误消息,说明为什么拒绝promise。可以通过将
.catch()
块链接到promise链的末尾来访问此原因。
运行代码以响应多个Promises的实现
Promise.all()静态方法。这将一个promises数组作为输入参数,并返回一个新的Promise对象,只有当数组中的所有promise都满足时才会满足。它看起来像这样:
1 Promise.all([a, b, c]).then(values => { 2 ... 3 });如果它们都实现,那么链接的
.then()
块的执行器函数将被传递一个包含所有这些结果作为参数的数组。如果传递给Promise.all()
的任何promise都拒绝,整个块将拒绝。
在promise fullfill/reject后运行一些最终代码
在promise完成后,您可能希望运行最后一段代码,无论它是否已实现(fullfilled)或被拒绝(rejected)。此前,您必须在
.then()
和.catch()
回调中包含相同的代码。在最近的现代浏览器中,
.finally()
方法可用,它可以链接到常规promise链的末尾,允许您减少代码重复并更优雅地执行操作。上面的代码现在可以写成如下:1 myPromise 2 .then(response => { 3 doSomething(response); 4 }) 5 .catch(e => { 6 returnError(e); 7 }) 8 .finally(() => { 9 runFinalCode(); 10 });
构建自定义promise
使用Promise()构造函数
可以使用
Promise()
构造函数构建自己的promise。让我们看一个简单的示例来帮助您入门 - 这里我们使用promise包装一个
setTimeout()
调用 - 这会在两秒后运行一个函数,该函数解析了promise(使用传递的resolve()
调用),字符串为“Success!”。1 let timeoutPromise = new Promise((resolve, reject) => { 2 setTimeout(function(){ 3 resolve('Success!'); 4 }, 2000); 5 });因此,当您调用此promise时,可以将
.then()
块链接到它的末尾,并将传递一串“Success!”。在下面的代码中,我们只是提醒该消息:1 timeoutPromise 2 .then((message) => { 3 alert(message); 4 })上面的例子不是很灵活 - promise只能用一个字符串来实现,并且它没有指定任何类型的
reject()
条件(诚然,setTimeout()
实际上没有失败条件,所以它这个简单的例子并不重要)。拒绝一个自定义promise
我们可以创建一个
reject()
方法拒绝promise - 就像resolve()
一样,这需要一个值,但在这种情况下,它是拒绝的原因,即将传递给.catch()
的错误块。让我们扩展前面的例子,使其具有一些
reject()
条件,并允许在成功时传递不同的消息。获取previous example副本,并将现有的
timeoutPromise()
定义替换为:1 function timeoutPromise(message, interval) { 2 return new Promise((resolve, reject) => { 3 if (message === '' || typeof message !== 'string') { 4 reject('Message is empty or not a string'); 5 } else if (interval < 0 || typeof interval !== 'number') { 6 reject('Interval is negative or not a number'); 7 } else { 8 setTimeout(function(){ 9 resolve(message); 10 }, interval); 11 } 12 }); 13 };在这里,我们将两个方法传递给一个自定义函数 - 一个用来做某事的消息,以及在做这件事之前要经过的时间间隔。在函数内部,我们返回一个新的
Promise
对象 - 调用该函数将返回我们想要使用的promise。由于
timeoutPromise()
函数返回一个Promise
,我们可以将.then()
,.catch()
等链接到它上面以利用它的功能:1 timeoutPromise('Hello there!', 1000) 2 .then(message => { 3 alert(message); 4 }) 5 .catch(e => { 6 console.log('Error: ' + e); 7 });
总结
当我们不知道函数的返回值或返回需要多长时间时,Promises是构建异步应用程序的好方法。它们使得在没有深度嵌套回调的情况下更容易表达和推理异步操作序列,并且它们支持类似于同步
try ... catch
语句的错误处理方式。Promise适用于所有现代浏览器的最新版本;promise有兼容问题的唯一情况是Opera Mini和IE11及更早版本。
本文中,我们没有涉及的所有promise的功能,只是最有趣和最有用的功能。当您开始了解有关promise的更多信息时,您会遇到更多功能和技巧。
async和await:让异步编程更简单
async functions和
await
关键字是最近添加的JavaScript语言,这是所谓的ECMAScript 2017 JavaScript版的一部分(参见ECMAScript Next support in Mozilla)。这些功能基本上充当promises的语法糖,使得异步代码更易于编写和后续阅读。
async/await的基本要素
async关键字
首先,我们使用
async
关键字,您可以将它放在函数声明之前,将其转换为async function。异步函数是一个知道怎样预期 await 关键字可用于调用异步代码可能性的函数。let hello = async () => { return "Hello" }; hello().then(console.log)将
async
关键字添加到函数中以告诉它们返回promise而不是直接返回值。此外,这使同步函数可以避免运行支持使用await
时带来的任何潜在开销。通过仅在函数声明为异步时添加必要的处理,JavaScript引擎可以为您优化您的程序。await关键字
将ssync与await关键字结合使用时,异步函数的真正优势就变得明显了。这可以放在任何基于异步声明的函数之前,暂停代码在该行上,直到promise完成,然后返回结果值。与此同时,其他正在等待执行机会的代码就有可能如愿执行了。
您可以在调用任何返回Promise的函数时使用
await
,包括Web API函数。1 async function hello() { 2 return greeting = await Promise.resolve("Hello"); 3 }; 4 5 hello().then(alert);使用async/await重写promise代码
由于
async
关键字将函数转换为promise,因此您可以重构代码以使用promises
和await
的混合方法,将函数的后半部分放入新块中以使其更灵活:1 async function myFetch() { 2 let response = await fetch('coffee.jpg'); 3 return await response.blob(); 4 } 5 6 myFetch().then((blob) => { 7 let objectURL = URL.createObjectURL(blob); 8 let image = document.createElement('img'); 9 image.src = objectURL; 10 document.body.appendChild(image); 11 });