原文连接:blog.bitsrc.io/understandi…
做者主页:blog.bitsrc.io/@Sukhjinderjavascript
JavaScript是单线程编程语言,这意味着同一时间只能发生一件事情。也就是说,JavaScript引擎只能在一个线程的同一时间里处理一个语句。java
单线程语言简化了咱们的编程,由于你不用担忧并发问题,但这也意味着在执行像网络请求这样耗时的操做的时候,会堵塞主进程的进行。node
想象着从一个API接口请求一些数据,在某些状况下服务器会花费一些时间处理请求,迟迟没有给出响应,这样就阻塞了主进程,让网页变得迟钝。web
这就是异步JavaScript发挥做用的地方。用异步的JavaScript(好比回调函数,promise或者async/await),你能够在不阻塞主进程的状况下执行长网络请求。编程
虽然你没必学习全部的概念去成为一名优秀的JavaScript工程师,但了解这些概念仍是颇有帮助的。promise
那么废话很少说了,让咱们进入正题吧!浏览器
在咱们深刻了解异步JavaScript以前,让咱们先理解同步JavaScript在JavaScript引擎里是怎么执行的,举个例子:bash
const second = () => {
console.log('Hello there!');
}
const first = () => {
console.log('Hi there!');
second();
console.log('The End');
}
first();
复制代码
为了理解上述代码是怎么在JavaScript引擎里执行的,咱们必须了解执行上下文和调用栈(也被称为执行栈)的概念。服务器
执行上下文是JavaScript代码在一个环境中编译和执行的抽象概念。在JavaScript里运行的任何代码,都会在执行上下文中执行。 函数里的代码在函数执行上下文中执行,全局代码在全局执行上下文中执。每个函数都有它本身的执行上下文。网络
执行栈,顾名思义,就是一个后进先出的栈结构,用来在代码运行的时候存储全部被建立的执行上下文。
JavaScript有一个单独的执行栈,由于它是单线程编程语言。执行栈是后进先出的数据结构就意味着元素只能出栈顶被增长或者移除。
让咱们回到刚才提到的代码段,试着去理解一下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上面。
假设咱们正在用同步的方式进行图像处理或网络请求:
const processImage = (image) => {
/**
* doing some operations on image
**/
console.log('Image processed');
}
const networkRequest = (url) => {
/**
* requesting network resource
**/
return someData;
}
const greeting = () => {
console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();
复制代码
进行图像处理和网络请求须要花费时间。因此当调用processImage()这个函数的时候,花费多少时间取决于要处理图片的大小。
当processImage()函数执行完成以后,它从执行栈里弹出。以后networkRequest()函数被调用,压入执行栈。一样的,须要花费一些时间去结束这个函数的执行。
因此你看,咱们必须得等到函数(好比processImage()或networkRequest())执行完了,才能进行下一步动做。这也意味着这些函数会阻塞执行栈或者主进程,咱们没法在执行这些函数的同时进行一些其余的操做,这是很不科学的。
最简单的解决办法就是使用异步回调了。咱们使用异步的回调函数让代码再也不被阻塞,好比:
const networkRequest = () => {
setTimeout(() => {
console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
复制代码
这里我用一个定时器方法去模仿网络请求。请牢记,setTimeout定时器并非JavaScript引擎的一部分,而是web APIs(在浏览器中)和C/C++ APIs(在node.js中)的一部分。
为了理解这段代码是如何执行的,咱们还得理解一些别的概念,好比事件循环(Event Loop)和回调队列(Message Quene,也被称为任务队列或消息队列)。
const networkRequest = () => {
setTimeout(() => {
console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');
复制代码
输出:
Hello World
The End
Async Code
复制代码
事件循环的工做就是去查看执行栈是否是空的。若是执行栈是空的,事件循环就去消息队列里查看是否有等待被执行的回调函数。
在本例里,消息队列包含一个等待被执行的回调函数,执行栈是空的。事件循环就会把回调函数压到执行栈栈顶去。
在console.log('Async Code')这段代码被压入执行栈栈顶,执行完毕而后从栈顶弹出以后。以后这个回调函数从栈顶弹出。到这时,这段程序才算真正地结束了。
消息队列也包含来自DOM事件的回调函数,好比点击事件和键盘事件,例如:
document.querySelector('.btn').addEventListener('click',(event) => {
console.log('Button Clicked');
});
复制代码
在DOM事件里,事件监听器在web APIs环境里等待一个具体事件发生(在这个例子里是点击事件),当那个事件发生后,回调函数就被放到消息队列里等待被执行。
一样的,事件循环检查执行栈是否是空的,若是是空的,就把回调函数压入执行栈里,让回调函数执行。
如今,咱们已经了解到怎么执行异步的回调函数和DOM事件了,显然,他们是被消息队列存起来而后再等待被执行的。
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的返回是存储在微任务队列里的,因此它的执行优先级比消息队列高。 让咱们看另外一个例子,这个例子里有两个promise和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工程师,但了解这些概念仍是对你有帮助的。 :)