2019-12-21
js是一种单线程语言,同一时刻,他只能做一件事情,也就是说js引擎在同一时刻单一线程内只能处理一个语句。
浏览器从接口获取数据时,服务器需要花费一些时间去处理这个请求,那么此时基于单线程语言的情况,浏览器的 主线程正在被阻塞,并且导致页面无响应,然而,实际情况并非如此,这时js异步处理。
使用异步的js例如(callbacks,promises,async/await),就可以执行网络请求,而不需要阻塞主线程。
同步的js如何工作?
const second = () => {
console.log('Hello there!');
}
const first = () =>{
console.log('Hi there');
second();
console.log('The End');
}
上述代码如何在引擎内部工作?必须理解执行上下文,调用栈;
执行上下文:
是一个执行环境的抽象,在这个环境中js代码被执行和评估。任何js代码在其内部的执行上下文环境被执行,函数代码执行在内部的函数执行上下文中,全局代码执行在内的全局上下文环境中,每一个函数都有他自己的执行上下文环境。
调用栈 Call Stack
就是一个栈,一种后进先出的数据结构。他常常被用于存储代码执行期间创建的执行上下文环境。js是单线程语言,所以他只有一个栈。调用栈是这种后进先出的数据结构,这意味着栈内元素的删除或增加只能是从栈顶开始。
代码的执行过程发生了什么?
代码执行的时候,全局上下文环境被创建(图中的main()),并且被push到了整个栈顶。当遇到first()的时候,又将first()push到栈顶,接下来console.log(‘Hi there’)被push到栈顶,当他执行完成后,他就会被出栈。之后又调用了second(),于是函数second()被推入栈顶,console.log(‘Hello there’)被推入栈顶,执行完成后被出栈(pop)。这时,second函数执行完成后也被出栈。console.log(‘the end’)被推入栈顶,执行完成后,再次被出栈,接下来first函数也执行完被出栈,这时程序完成了他的执行,所以全局上下文main()也被从栈顶移除。
异步的js是如何工作的?
什么是阻塞?
假设正在处理一张照片,或一个网络请求,以同步的方式,例如
const processImage = (image) => {
console.log('image processed');
}
const networkRequest = (url) => {
return someDate;
}
const greeting = () => {
console.log('hello');
}
processIamge(logo.jpg);
networkRequest('www.***.com');
greeting();
处理图片和发送网络请求都需要事件,于是当processImage()函数被调用的时候,他将要花费的时间取决于图片的大小,当他执行完成时,被从栈中移除。随后networkRequest()被调用,并且push到栈中。同样需要花费时间去完成执行操作。然后greeting被调用,推入栈中。因为他包含了console.log语句,执行非常快,立即执行并且返回。也就是说,前两个请求正在阻塞最后的请求执行,这些函数正在阻塞调用栈或者说主线程。
解决方案
最简单的方法是异步回调。
const networkRequest = () =>{
setTimeout(() => {
console.log('Async Code')
},2000)
};
console.log('hello');
networkRequest();
console.log('The End')
这里有一个setTimeout()方法来模拟网络请求,他是web api中的。这部分代码的执行必须了解一些概念:时间循环机制,回调队列。
事件循环机制,web api还有消息队列 都不属于js引擎,他们是浏览器运行环境的一部分。
上述代码被加载到浏览器时,console.log(‘hello’)最先被push到栈中,完成后出栈。接下来是networkRequest()的调用,被push到栈顶,接下来setTimeout()函数被执行,push到栈中,setTimeout()函数接受两个参数,①callback和②毫秒数,他开启了一个两秒钟的定时器。这时setTimeout()已经完成,出栈。之后console.log(‘The End’)被push到栈中,执行完成后pop。
与此同时定时器已经到期,这时callback被push到消息队列,但是回调并不是立即执行,这里就是时间循环机制发挥作用的地方。
时间循环 Event Loop
时间循环机制的作用就是查看调用栈,并且判断调用栈是否为空,如果调用栈为空,他就回去查看消息队列中是否存在被挂起的回调正在等待被执行。
在这个例子中,消息队列只包含一个回调,于是检测到调用栈是空的时候,就将这个回调push到执行栈中。
在console.log(‘Asyn Code’)被push到栈顶后,执行然后出栈,这时回调已经从栈中移除,并且程序执行结束。
Dom Events
Dom事件的回调也会被放置在消息队列中处理,例如键盘事件或鼠标事件。
document.querySelector('.btn').addEventListener('click',(event)=>{
console.log('Button Clicked');
})
在Dom事件中,位于web apis环境中的事件监听器等待着一个确定的时间发生,比如click,于是当事件被触发时这个回调函数就会被放置到消息队列中去,等待调用栈为空的时候被执行。
当时间循环机制检查到调用栈是空的,这个事件的回调就会被push到栈中被执行。
异步以及DOM事件的回调就是被保存在消息队列中等待空时被执行。
ES6中的微任务队列
在js中微任务队列最常见的应用就是promise,微任务队列和消息队列之间最大的不同之处就在于,微任务队列用于更高的优先级,这就意味着Promise中的回调会被添加至为任务队列中,并且会在消息队列中的回调之前被添加到执行栈中。