你们好,我是wmingren,小伙伴们都知道JavaScript是单线程的语言,所谓的单线程呢就是指若是有多个任务就必须去排队,前面任务执行完成后,后面任务再执行。到这里咱们就产生了一个疑问,既然是单线程的,又怎么会有异步操做呢?首先了解一下同步和异步的概念吧。
若是在函数返回结果的时候,调用者可以拿到预期的结果(就是函数计算的结果),那么这个函数就是同步的.
console.log('hello');//执行后,得到了返回结果
若是函数是同步的,即便调用函数执行任务比较耗时,也会一致等待直到获得执行结果。以下面的代码:面试
function wait(){ var time = (new Date()).getTime();//获取当前的unix时间戳 while((new Date()).getTime() - time > 5000){} console.log('5秒过去了'); } wait(); console.log('慢死了');
上面代码中,函数wait是一个耗时程序,持续5秒,在它执行的这漫长的5秒中,下面的console.log()函数只能等待,这就是同步。ajax
若是在函数返回的时候,调用者还不能购获得预期结果,而是未来经过必定的手段获得(例如回调函数),这就是异步。例如ajax操做。
若是函数是异步的,发出调用以后,立刻返回,可是不会立刻返回预期结果。调用者没必要主动等待,当被调用者获得结果以后会经过回调函数主动通知调用者。
了解完同步和异步以后,咱们再来看看咱们的问题:单线程又怎么会有异步呢?
JavaScript其实就是一门语言,说是单线程仍是多线程得结合具体运行环境。众所周知,js的运行环境就是浏览器,具体由js引擎取解析和执行。下面咱们来了解下浏览器。
一个浏览器一般由如下几个常驻的线程:浏览器
要注意的是渲染引擎和js引擎线程是不能同时进行的。渲染线程在执行任务的时候,js引擎线程会被挂起。由于如果在渲染页面的时候,js处理了DOM,浏览器就不知道该听谁的了
一般讲到浏览器的时候,咱们会说到两个引擎:渲染引擎和JS引擎。
一、 渲染引擎:Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trdent引擎,FireFox用的是Gecko引擎。不一样的引擎对同一个样式的实现不一致,就致使浏览器的兼容性问题。
二、 JS引擎:js引擎能够说是js虚拟机,负责解析js代码的解析和执行。一般有如下步骤:
不一样浏览器的js引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。之因此说js是单线程就是由于浏览器运行时只开启一个js解释器,缘由是如有两个线程操做DOM,浏览器就又晕了。多线程
JavaScript是单线程的,可是浏览器不是单线程的。一些I/O操做,定时器的计时和事件监听是由其余线程完成的。异步
由上面浏览器一篇的介绍能够知道,浏览器中多个线程的合做完成了异步的操做,那么异步的回调函数又是怎样完成执行的呢?
![]()
这就须要了解消息队列和事件循环了。ide
如上图所示,左边的栈存储的是同步任务,就是那些能当即执行、不耗时的任务,如变量和函数的初始化、事件的绑定等等那些不须要回调函数的操做均可归为这一类。右边的堆用来存储声明的变量、对象。下面的队列就是消息队列,一旦某个异步任务有了响应就会被推入队列中。如用户的点击事件、浏览器收到服务的响应和setTimeout中待执行的事件,每一个异步任务都和回调函数相关联。函数
JS引擎线程用来执行栈中的同步任务,当全部同步任务执行完毕后,栈被清空,而后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。spa
JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,若是没有新的任务,就会等待,直到有新的任务,这就叫事件循环。
线程
上图以AJAX异步请求为例,发起异步任务后,由AJAX线程执行耗时的异步操做,而JS引擎线程继续执行堆中的其余同步任务,直到堆中的全部异步任务执行完毕。而后,从消息队列中依次按照顺序取出消息做为一个同步任务在JS引擎线程中执行,那么AJAX的回调函数就会在某一时刻被调用执行。unix
最后来一个经典的面试题来帮助你们理解js的同步和异步。
代码以下:
//执行下面这段代码,执行后,在 5s 内点击两下,过一段时间(>5s)后,再点击两下,整个过程的输出结果是什么? setTimeout(function(){ for(var i = 0; i < 100000000; i++){} console.log('timer a'); }, 0) for(var j = 0; j < 5; j++){ console.log(j); } setTimeout(function(){ console.log('timer b'); }, 0) function waitFiveSeconds(){ var now = (new Date()).getTime(); while(((new Date()).getTime() - now) < 5000){} console.log('finished waiting'); } document.addEventListener('click', function(){ console.log('click'); }) console.log('click begin'); waitFiveSeconds();
要想了解上述代码的输出结果,首先介绍下定时器。
setTimeout 的做用是在间隔必定的时间后,将回调函数插入消息队列中,等栈中的同步任务都执行完毕后,再执行。由于栈中的同步任务也会耗时, 因此间隔的时间通常会大于等于指定的时间 。setTimeout(fn, 0) 的意思是,将回调函数fn马上插入消息队列,等待执行,而不是当即执行。看一个例子:
setTimeout(function() { console.log("a") }, 0) for(let i=0; i<10000; i++) {} console.log("b")
//打印结果,说明回调函数没有当即执行,而是等待同步任务执行完成后才执行的 b a
下面来解释一下面试题吧。先执行同步任务,for循环,而后是console.log('click begin') 最后是waitFiveSeconds函数
在同步任务执行的期间,‘timera’,‘timerb’对应的回调和click事件的回调前后入队列。
同步任务结束后,js引擎线程空闲后会线查看是否有事件可执行,接着在处理其余异步任务,所以会有下面的输出:
0 1 2 3 4 click begin finished waiting 2 click //5s中两次点击 timer a timer b 2 click //5s后两次点击