几乎在每一本JS相关的书籍中,都会说JS是单线程的,JS是经过事件队列(Event Loop)的方式来实现异步回调的。 对不少初学JS的人来讲,根本搞不清楚单线程的JS为何拥有异步的能力,因此,我试图从进程、线程的角度来解释这个问题。javascript
说到CPU和进程、线程,对计算机操做系统有过学习和了解的同窗应该比较熟悉。前端
计算机的核心是CPU
,它承担了全部的计算任务。java
它就像一座工厂,时刻在运行。ajax
假定工厂的电力有限,一次只能供给一个车间使用。 也就是说,一个车间开工的时候,其余车间都必须停工。 背后的含义就是,单个CPU一次只能运行一个任务。算法
进程
就比如工厂的车间,它表明CPU所能处理的单个任务。 进程
之间相互独立,任一时刻,CPU老是运行一个进程
,其余进程
处于非运行状态。 CPU使用时间片轮转进度算法来实现同时运行多个进程
。编程
从上文咱们已经简单了解了CPU、进程、线程,简单汇总一下。segmentfault
进程
是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)线程
是cpu调度的最小单位(线程是创建在进程的基础上的一次程序运行单位,一个进程中能够有多个线程)进程
之间也能够通讯,不过代价较大单线程
与多线程
,都是指在一个进程
内的单和多咱们已经知道了CPU
、进程
、线程
之间的关系,对于计算机来讲,每个应用程序都是一个进程
, 而每个应用程序都会分别有不少的功能模块,这些功能模块其实是经过子进程
来实现的。 对于这种子进程
的扩展方式,咱们能够称这个应用程序是多进程
的。windows
而对于浏览器来讲,浏览器就是多进程的,我在Chrome浏览器中打开了多个tab,而后打开windows控制管理器:promise
如上图,咱们能够看到一个Chrome浏览器启动了好多个进程。
总结一下:浏览器
主进程
第三方插件进程
GPU进程
渲染进程
,就是咱们说的浏览器内核
那么浏览器中包含了这么多的进程,那么对于普通的前端操做来讲,最重要的是什么呢?
答案是渲染进程
,也就是咱们常说的浏览器内核
从前文咱们得知,进程和线程是一对多的关系,也就是说一个进程包含了多条线程。
而对于渲染进程
来讲,它固然也是多线程的了,接下来咱们来看一下渲染进程包含哪些线程。
GUI渲染线程
JS引擎线程
事件触发线程
定时触发器线程
异步http请求线程
当咱们了解了渲染进程包含的这些线程后,咱们思考两个问题:
首先是历史缘由,在建立 javascript 这门语言时,多进程多线程的架构并不流行,硬件支持并很差。
其次是由于多线程的复杂性,多线程操做须要加锁,编码的复杂性会增高。
并且,若是同时操做 DOM ,在多线程不加锁的状况下,最终会致使 DOM 渲染的结果不可预期。
这是因为 JS 是能够操做 DOM 的,若是同时修改元素属性并同时渲染界面(即 JS线程
和UI线程
同时运行), 那么渲染线程先后得到的元素就可能不一致了。
所以,为了防止渲染出现不可预期的结果,浏览器设定 GUI渲染线程
和JS引擎线程
为互斥关系, 当JS引擎线程
执行时GUI渲染线程
会被挂起,GUI更新则会被保存在一个队列中等待JS引擎线程
空闲时当即被执行。
到了这里,终于要进入咱们的主题,什么是 Event Loop
先理解一些概念:
执行栈
任务队列
,异步任务触发条件达成,将回调事件放到任务队列
中执行栈
中全部同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列
,将可运行的异步任务回调事件添加到执行栈
中,开始执行在前端开发中咱们会经过setTimeout/setInterval
来指定定时任务,会经过XHR/fetch
发送网络请求, 接下来简述一下setTimeout/setInterval
和XHR/fetch
到底作了什么事
咱们知道,不论是setTimeout/setInterval
和XHR/fetch
代码,在这些代码执行时, 自己是同步任务,而其中的回调函数才是异步任务。
当代码执行到setTimeout/setInterval
时,其实是JS引擎线程
通知定时触发器线程
,间隔一个时间后,会触发一个回调事件, 而定时触发器线程
在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程
所管理的事件队列
中。
当代码执行到XHR/fetch
时,其实是JS引擎线程
通知异步http请求线程
,发送一个网络请求,并制定请求完成后的回调事件, 而异步http请求线程
在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程
所管理的事件队列
中。
当咱们的同步任务执行完,JS引擎线程
会询问事件触发线程
,在事件队列
中是否有待执行的回调函数,若是有就会加入到执行栈中交给JS引擎线程
执行
用一张图来解释:
再用代码来解释一下:
let timerCallback = function() { console.log('wait one second'); }; let httpCallback = function() { console.log('get server data success'); } // 同步任务 console.log('hello'); // 同步任务 // 通知定时器线程 1s 后将 timerCallback 交由事件触发线程处理 // 1s 后事件触发线程将 timerCallback 加入到事件队列中 setTimeout(timerCallback,1000); // 同步任务 // 通知异步http请求线程发送网络请求,请求成功后将 httpCallback 交由事件触发线程处理 // 请求成功后事件触发线程将 httpCallback 加入到事件队列中 $.get('www.xxxx.com',httpCallback); // 同步任务 console.log('world'); //... // 全部同步任务执行完后 // 询问事件触发线程在事件事件队列中是否有须要执行的回调函数 // 若是没有,一直询问,直到有为止 // 若是有,将回调事件加入执行栈中,开始执行回调代码
总结一下:
当咱们基本了解了什么是执行栈,什么是事件队列以后,咱们深刻了解一下事件循环中宏任务、微任务
咱们能够将每次执行栈执行的代码当作是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每个宏任务会从头至尾执行完毕,不会执行其余。
咱们前文提到过JS引擎线程
和GUI渲染线程
是互斥的关系,浏览器为了可以使宏任务
和DOM任务
有序的进行,会在一个宏任务
执行结果后,在下一个宏任务
执行前,GUI渲染线程
开始工做,对页面进行渲染。
// 宏任务-->渲染-->宏任务-->渲染-->渲染...
主代码块,setTimeout,setInterval等,都属于宏任务
第一个例子:
document.body.style = 'background:black'; document.body.style = 'background:red'; document.body.style = 'background:blue'; document.body.style = 'background:grey';
咱们能够将这段代码放到浏览器的控制台执行如下,看一下效果:
咱们会看到的结果是,页面背景会在瞬间变成灰色,以上代码属于同一次宏任务,因此所有执行完才触发页面渲染,渲染时GUI线程会将全部UI改动优化合并,因此视觉效果上,只会看到页面变成灰色。
第二个例子:
document.body.style = 'background:blue'; setTimeout(function(){ document.body.style = 'background:black' },0)
执行一下,再看效果:
我会看到,页面先显示成蓝色背景,而后瞬间变成了黑色背景,这是由于以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,而后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色。
咱们已经知道宏任务
结束后,会执行渲染,而后执行下一个宏任务
, 而微任务能够理解成在当前宏任务
执行后当即执行的任务。
也就是说,当宏任务
执行完,会在渲染前,将执行期间所产生的全部微任务
都执行完。
Promise,process.nextTick等,属于微任务
。
第一个例子:
document.body.style = 'background:blue' console.log(1); Promise.resolve().then(()=>{ console.log(2); document.body.style = 'background:black' }); console.log(3);
执行一下,再看效果:
控制台输出 1 3 2 , 是由于 promise 对象的 then 方法的回调函数是异步执行,因此 2 最后输出
页面的背景色直接变成黑色,没有通过蓝色的阶段,是由于,咱们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了黑色,而后才执行的渲染。
第二个例子:
setTimeout(() => { console.log(1) Promise.resolve(3).then(data => console.log(data)) }, 0) setTimeout(() => { console.log(2) }, 0) // print : 1 3 2
上面代码共包含两个 setTimeout ,也就是说除主代码块外,共有两个宏任务
, 其中第一个宏任务
执行中,输出 1 ,而且建立了微任务队列
,因此在下一个宏任务
队列执行前, 先执行微任务
,在微任务
执行中,输出 3 ,微任务执行后,执行下一次宏任务
,执行中输出 2
宏任务
(栈中没有就从事件队列
中获取)微任务
,就将它添加到微任务
的任务队列中宏任务
执行完毕后,当即执行当前微任务队列
中的全部微任务
(依次执行)宏任务
执行完毕,开始检查渲染,而后GUI线程
接管渲染JS线程
继续接管,开始下一个宏任务
(从事件队列中获取本文转载至掘金,做者云中君,感谢做者分享。
http://www.javashuo.com/article/p-slsqhrtr-y.html
文章本身反复阅读了好几遍,以为仍是写得很是不错,由浅入深,通俗易懂,我想就算没有必定计算机操做系统理论基础的同窗也能够看得懂了。
这实际上是一个js很是基础同时很重要的知识点,js事件执行机制的学习对js的异步编程的理解、浏览器性能优化都颇有帮助。
最后再次感谢做者!
推荐阅读:
【专题:JavaScript进阶之路】
JavaScript之深刻理解闭包
ES6 尾调用和尾递归
Git经常使用命令小结
JavaScript之call()理解
JavaScript之对象属性
我是Cloudy,年轻的前端攻城狮一枚,爱专研,爱技术,爱分享。
我的笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎你们指出,也欢迎你们一块儿交流前端各类问题!