你知道的越多,你不知道的越多
点赞
再看,手留余香,与有荣焉javascript
几乎在每一本JS相关的书籍中,都会说JS是单线程
的,JS是经过事件队列(Event Loop)
的方式来实现异步回调的。 对不少初学JS的人来讲,根本搞不清楚单线程的JS为何拥有异步
的能力,因此,我试图从进程
、线程
的角度来解释这个问题。html
计算机的核心是CPU
,它承担了全部的计算任务。前端
它就像一座工厂,时刻在运行。java
假定工厂的电力有限,一次只能供给一个车间使用。 也就是说,一个车间开工的时候,其余车间都必须停工。 背后的含义就是,单个CPU一次只能运行一个任务。ajax
进程
就比如工厂的车间,它表明CPU所能处理的单个任务。 进程
之间相互独立,任一时刻,CPU老是运行一个进程
,其余进程
处于非运行状态。 CPU使用时间片轮转进度算法来实现同时运行多个进程
。算法
一个车间里,能够有不少工人,共享车间全部的资源,他们协同完成一个任务。windows
线程
就比如车间里的工人,一个进程
能够包括多个线程
,多个线程
共享进程
资源。数组
从上文咱们已经简单了解了CPU、进程、线程,简单汇总一下。promise
进程
是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)线程
是cpu调度的最小单位(线程是创建在进程的基础上的一次程序运行单位,一个进程中能够有多个线程)进程
之间也能够通讯,不过代价较大单线程
与多线程
,都是指在一个进程
内的单和多咱们已经知道了CPU
、进程
、线程
之间的关系,对于计算机来讲,每个应用程序都是一个进程
, 而每个应用程序都会分别有不少的功能模块,这些功能模块其实是经过子进程
来实现的。 对于这种子进程
的扩展方式,咱们能够称这个应用程序是多进程
的。浏览器
而对于浏览器来讲,浏览器就是多进程的,我在Chrome浏览器中打开了多个tab,而后打开windows控制管理器:
如上图,咱们能够看到一个Chrome浏览器启动了好多个进程。
总结一下:
渲染进程
,就是咱们说的浏览器内核
那么浏览器中包含了这么多的进程,那么对于普通的前端操做来讲,最重要的是什么呢?
答案是渲染进程
,也就是咱们常说的浏览器内核
从前文咱们得知,进程和线程是一对多的关系,也就是说一个进程包含了多条线程。
而对于渲染进程
来讲,它固然也是多线程的了,接下来咱们来看一下渲染进程包含哪些线程。
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线程
继续接管,开始下一个宏任务
(从事件队列中获取)