摘要:当函数结束,将会被从调用栈移出。事件循环事件循环的责任就是查看调用栈并确定调用栈是否为空。事件循环会再次检查调用栈是否为空,如果为空的话,它会把事件回调压入栈中,然后回调函数则被执行。
写在文章前
这篇文章是翻译自Sukhjinder Arora的
Understanding Asynchronous JavaScript。这篇文章描述了异步和同步JavaScript是如何在运行环境中,使用调用栈,消息队列,作业队列,以及事件循环来工作的。文章如有翻译不好的地方还望多多包涵。
众所周知,JavaScript 是单线程的编程语言,那就意味着在同一个时间只能有一件事发生。通俗的讲,JavaScript引擎每一个线程一次只能处理一个声明。
虽然单线程语言可以简化写代码的过程,因为你不用担心并发的问题,但这样同时也意味着你无法在不锁住主线程的情况下,执行像网络访问这种长时间的操作。
想象一下,从API请求数据的这个情况。服务器可能需要一些时间来处理请求,同时阻塞主线程使网页无响应。
这就是异步Javascript可以发挥作用的地方了。使用异步JavaScript(例如像回调,promises,和async/await),你就可以在不锁住主线程的情况下执行长时间的网络请求。
你没有必要学习所有这些概念来成为一个出色JavaScript工程师,这些只是对你很有帮助而已:)
所以废话不多说,我们开始吧。
同步JavaScript是怎么工作的呢?在我们深入了解异步JavaScript之前,让我们先来了解一下同步的JavaScript代码是如何在引擎内部执行的。举个例子:
const second = () => { console.log("hello there"); } const first = () => { console.log("hi,there"); second(); console.log("The End"); } first();
在我们想要理解上面代码是如何在JavaScript引擎执行的之前,我们需要先要理解执行上下文和调用栈的概念(也叫执行栈)。
执行上下文执行上下文是JavaScript代码被评估和执行的地方的抽象概念。每当任何js代码执行的时候,他们就运行在执行上下文内部。
函数执行在函数的执行上下文内,全局代码执行在全局的执行上下文内。每个函数都有自己的执行上下文。
调用栈调用栈就像他名字里展示的那样,他是一个具有后进先出的栈结构,它用于存储代码执行期间创建的所有执行上下文。
JavaScript是拥有单一调用栈的,因为它是单线程的语言。调用栈的LIFO(后进先出结构)决定了东西只能从栈的顶部添加或者删除。
让我们回到上面的代码片段,然后尝试理解一下上面的代码片段是怎么在JavaScript引擎内部执行的。
const second = () => { console.log("hello there"); } const first = () => { console.log("hi,there"); second(); console.log("The End"); } first();
上面代码的调用栈:
)
当代码执行的时候,一个全局的执行上下文就被创建了(表示为main())然后将他压入调用栈的顶部。当first()被调用的时候,first()又被压入调用栈的顶部。
接下来,console.log("hi,there")又被压入栈的顶部,当它执行结束,他就从栈中弹出了。之后,我们调用了second(),所以second()函数就被压入栈顶。
console.log("Hello there!")被压入栈顶,并且当它执行结束就被弹出。 此时,second()函数执行结束,所以从栈中弹出。
console.log("The End")被压入栈顶然后再结束的时候被移出。然后,first()函数执行结束,被移出调用栈。
此时,整个程序结束调用,所以全局执行上下文(main())从栈中弹出。
异步JavaScript到底是怎么运行的呢?现在我们已经对调用栈有个大致了解了,也知道了同步的JavaScript是怎么工作的,现在我们回到异步JavaScript这个话题。
什么是锁?我们想象一下我们正在使用同步的方式进行图像处理或者网络请求。比如:
const processImage = (image) => { //对图像进行处理 console.log("Image Processed"); } const netWorkRequest = (url) => { //网络资源请求 return someData; } const greeting = () => { console.log("Hello World"); } processImage(logo.jpg); networkRequest("www.somerandomurl.com"); greeting();
图像的处理和网络请求很花时间。所以当processImage()函数被调用的时候,花费的时间将取决于图像的大小。
当processImage()函数结束,将会被从调用栈移出。之后networkRequest()函数被调用并且被压入栈中。所以又要花费一些时间来结束调用。
最后当networkRequest()函数结束,greeting()函数被调用,因为他只包含一个console.log声明,而且console.log声明执行的非常地块,所以greeting()函数很快的就结束调用了。
如你所见,我们必须要等,等到函数(就像processImage()和networkRequest())结束执行。这就意味着这些函数被锁在调用栈或者主线程里。 所以在上述代码执行期间我们不能执行任何其他的操作,这不绝不是我们想要的。
所以怎么解决?最简单的解决办法就是异步回调。我们使用异步回调让我们的代码不被锁住。举个栗子:
const networkRequest = () => { setTimeout(() => { console.log("Async Code"); },2000); }; console.log("Hello World"); networkRequest();
在这里我使用了setTimeout方法来模拟网络请求。请注意setTimeout不是Javascript引擎的一部分,它是Web Api(浏览器中)和 C/C++ (在node.js)中的一部分。
为了理解这段代码是如何执行的,我们需要理解更多的概念,比如像事件循环和回调队列(也叫做任务队列或者消息队列)。
事件循环,WEB API, 消息队列/任务队列不是JavaScript引擎的一部分,他们是浏览器的JavaScript运行时环境或者Node.js JavaScript 运行环境的一部分。 在Nodejs中,网络接口被C/C++ API 取代.
现在,让我们回到上面的代码,然后看一看他们是怎么以异步的方式执行的。
const networkRequest = () => { setTimeout(() => { console.log("Async Code"); }, 2000); }; console.log("Hello World"); networkRequest(); console.log("The End");
当上面的代码在浏览器加载的时候,console.log("Hello World")入栈并且当调用结束的出栈。接下来,调用的是networkRequest(),所以它被推入栈顶。
接下来setTimeout()方法被调用,所以被压入栈顶。setTimeout函数有2个参数:1) 回调函数 2)以ms为单位的时间。
setTimeout在Web API环境中开始了一个为时2s的计时器。此时,setTimeout已经结束了,所以被弹出栈,接着,console.log("The End")被压入栈,执行然后在结束后从栈中移出。
与此同时,计时器到时间了,现在回调被推入到信息队列,但回调并没有被立即执行,而是被放到了事件循环开始的地方。
事件循环事件循环的责任就是查看调用栈并确定调用栈是否为空。如果调用栈为空,他就会查看消息队列来确定是否有任何挂起的回调函数等待被执行。
在这个例子中消息队列中包括一个回调函数,并且此时调用栈为空。因此事件循环把回调函数压入栈顶。
在那之后,console.log(‘Async Code‘)这条语句被压入栈顶,执行,然后从栈中弹出。此时回调函数结束了,所以它被从栈中弹出,然后整个程序结束执行。
DOM 事件消息队列中也包括DOM事件中的回调函数比如点击事件和键盘事件,例如:
document.querySelector(".btn").addEventListener("click",(event) => { console.log("Button Clicked"); })
在DOM事件里,事件监听器位于Web API 环境中等待某个事件发生(在这个例子中是点击事件),并且当该事件发生的时候,回调函数则被放置在消息队列中等待被执行。
事件循环会再次检查调用栈是否为空,如果为空的话,它会把事件回调压入栈中,然后回调函数则被执行。
我们已经学习了异步回调和DOM 事件是如何执行的,他们使用消息队列来存储所有等待被执行的回调。
ES6 作业队列/ 微任务队列ES6介绍了一种被JavaScript 中Promises使用的叫做作业队列/微任务队列的概念。消息队列和作业队列的区别就在于作业队列会比消息队列拥有更高的优先级,也就是说作业队列/微任务队列中的Promise的任务会比消息队列中的回调函数先执行。
例如:
console.log("Script start"); setTimeout(() => { console.log("setTimeout"); },0); new Promise((resolve,reject) => { resolve("Promise resolved"); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log("Script End");
输出:
Script start Script End Promise resolved setTimeout
我们可以看到promise是在setTimeout之前被执行的,因为promise的返回是存储在微任务队列中的,它比消息队列拥有更高的优先级。
让我们看下一个例子,这次有两个Promises和两个setTimeout。
console.log("Script start"); setTimeout(() => { console.log("setTimeout 1"); },0); setTimeout(() => { console.log("setTimeout 2"); },0); new Promise((resolve,reject) => { resolve("Promise 1 resolved"); }).then(res => console.log(res)) .catch(err => console.log(err)); new Promise((resolve,reject) => { resolve("Promise 2 resolved"); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log("Script End");
这一次输出:
Script start Script End Promise 1 resolved Promise 2 resolved setTimeout 1 setTimeout 2
我们可以看到两个promise都是在setTimeout回调的前面执行的,因为事件循环机制中,微任务队列中的任务要优先于消息队列/任务队列中的任务。
当事件循环正在执行微任务队列中的任务时,如果另一个promise处于resolved的状态的话,他会被添加到同一个微任务队列的尾部,并且他会比消息队列中的回调先执行,不管回调函数已经等待执行了多久了。(优先级高果然就是能为所欲为= =)。
举个例子:
console.log("Script start"); setTimeout(() => { console.log("setTimeout"); }, 0); new Promise((resolve, reject) => { resolve("Promise 1 resolved"); }).then(res => console.log(res)); new Promise((resolve, reject) => { resolve("Promise 2 resolved"); }).then(res => { console.log(res); return new Promise((resolve, reject) => { resolve("Promise 3 resolved"); }) }).then(res => console.log(res)); console.log("Script End");
这次的输出:
Script start Script End Promise 1 resolved Promise 2 resolved Promise 3 resolved setTimeout
所以所有在微任务队列中的任务都将在消息队列中的任务之前执行。也就是说,事件循环将会在执行任何消息队列的回调之前,首先清空微任务队列中的任务。
总结我们已经学习了异步JavaScript是如何工作的,以及一些其他的概念比如说调用栈,事件循环,消息/任务队列以及工作/微任务队列,他们在一起构成了JavaScript的运行环境。再重申一下,虽然您没有必要将这些所有的概念都学习,来成为一个出色的JavaScript开发人员,但了解这些概念会很有帮助:)
今天的文章就这样啦,如果你觉得这篇文章对你很有帮助,请点击旁边的鼓掌按钮,你也可以在Medium和Twitter上面follow我。如果你有任何的疑问,欢迎在下面留言,我会很开心的帮助你的:)
译者结语如果你对我的翻译或者内容有什么意见或者建议欢迎在下面留言告诉我,喜欢文章就给个赞吧,非常感谢您的阅读,Hava a nice day:)
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/99861.html
摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...
摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...
摘要:调用栈被清空,消息队列中并无任务,线程停止,事件循环结束。不确定的时间点请求返回,将设定好的回调函数放入消息队列。调用栈执行完毕执行消息队列任务。请求并发回调函数执行顺序无法确定。 异步编程 JavaScript中异步编程问题可以说是基础中的重点,也是比较难理解的地方。首先要弄懂的是什么叫异步? 我们的代码在执行的时候是从上到下按顺序执行,一段代码执行了之后才会执行下一段代码,这种方式...
摘要:从异步过程的角度看,函数就是异步过程的发起函数,事件监听函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。 1.为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。JavaScrip...
摘要:异步本质上应该就是多线程语言的产物。如果是多线程的异步,假死的应该是运行方法的线程,而方法仍然会按预期打印出。当然了,按我个人的理解,应该说是是的回调函数。 引子 每个故事都有由来。前两天在看 gulp 的时候,看到了它有个 promise 的玩意儿,然后的然后,这两天就掉进了 javascript 的异步和回调的坑里面去了。 其间搜索了 javascript promise,看到了...
阅读 1063·2021-10-08 10:04
阅读 3504·2021-08-05 10:01
阅读 2258·2019-08-30 11:04
阅读 1763·2019-08-29 15:29
阅读 770·2019-08-29 15:12
阅读 1653·2019-08-26 12:11
阅读 3095·2019-08-26 11:33
阅读 1149·2019-08-26 10:23