我们首先要考虑下js作为浏览器脚本语言,主要用途就是和用户互动和操作DOM。比如js同时有两个线程,两个线程同时操作同一个DOM,比如一个给DOM添加内容,一个移除DOM,那到底该听谁的呢?所以这就决定了它只能是单线程,否则就会出现一些奇怪的问题。
我们每打开一个tab页就会产生一个进程
浏览器进程
第三方插件进程
每种类型的插件对应一个进程,当使用该插件时才创建。因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响
GPU进程
该进程只有一个,用于3D绘制等
渲染进程
网络进程
主要负责页面的网络资源加载。
如果浏览器是单进程,那么当一个tab页面崩溃了,就会影响到整个浏览器。同时如果插件崩溃了也会影响整个浏览器。浏览器进程有很多,每个进程又有很多的线程,都会占用内存。进程之间的内容相互隔离,这是为了保护操作系统中进行互不干扰的技术,每一个进程只能访问自己占有的数据,也就避免了进程A写入数据到进程B的情况。因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。
页面的渲染、js的执行、事件的循环、都在渲染进程中执行,所以重点看下渲染进程。渲染进程是多线程的,下面看下比较常用的几个线程
GUI线程
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
当修改了一些元素的颜色或者背景色,页面就会重绘(Repaint)
当修改元素的尺寸,页面就是重排也叫回流(Reflow)
当页面需要重绘和重排的时候GUI线程执行,绘制页面
重绘和重排的成本比较高,尽量避免重绘和重排
GUI线程和JS引擎线程是互斥的
JS引擎线程
JS引擎线程就是JS内核,负责处理JavaScript脚本程序(例如V8引擎)
JS引擎线程负责解析JavaScript脚本,运行代码
JS引擎一直等待任务队列的到来,然后加以处理
GUI线程和JS引擎是互斥的,JS引擎线程会阻塞GUI渲染线程
如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
事件触发线程
定时触发器线程
异步HTTP请求线程
下面就来谈谈我们的重头戏
let setTimeoutCallBack = function() { console.log('我是定时器回调'); }; let httpCallback = function() { console.log('我是http请求回调'); } // 同步任务 console.log('我是同步任务1'); // 异步定时任务 setTimeout(setTimeoutCallBack,1000); // 异步http请求任务 ajax.get('/info',httpCallback); // 同步任务 console.log('我是同步任务2');
我们来看下这段代码。解析一下执行过程
console.log('我是同步任务1');
setTimeout
时候,会交给定时器线程,并告诉定时器线程在1s后将setTimeoutCallBack
回调交给事件触发线程,1s后事件触发线程把这个回调添加到了任务队列中等待执行ajax
,会交给异步HTTP请求线程发送网络请求,请求成功后,将回调httpCallback
交给事件触发线程并放入任务队列中等待执行。console.log('我是同步任务2');
浏览器上的所有线程是的行为都很单一。
了解了事件循环下面看下宏任务和微任务
为了协调这些任务能够有序的在主线程上执行,页面进行引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,主线程不断的从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务
常见的宏任务:
微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束后之前。
异步回调有两种方式
我们知道宏任务结束后,会执行渲染,然后执行下一次宏任务,微任务可以理解为当前宏任务执行后立即执行的任务。
常见的微任务:
当执行完一个宏任务之后,会立即执行期间所产生的所有微任务,然后执行渲染
浏览器首先会执行一个宏任务,然后执行当前执行栈所产生的微任务,然后再渲染页面,然后再执行下一个宏任务
function test() { console.log(1) setTimeout(function () { // timer1 console.log(2) }, 1000) } test(); setTimeout(function () { // timer2 console.log(3) }) new Promise(function (resolve) { console.log(4) setTimeout(function () { // timer3 console.log(5) }, 100) resolve() }).then(function () { setTimeout(function () { // timer4 console.log(6) }, 0) console.log(7) }) console.log(8)
下面我们来分析一下整体的流程
首先应该找到同步任务先执行
console.log(1)
会先执行,打印1。而setTimeout(我们记作timer1)作为宏任务加入宏任务队列console.log(4)
打印4。而setTimeout(我们记作timer3)作为宏任务加入宏任务队列console.log(8)
作为同步任务执行,打印8我们再看异步任务
console.log(7)
打印7console.log(3)
作为同步任务打印3,然后检查有没有微任务和GUI渲染console.log(6)
作为同步任务打印6,然后检查有没有微任务和GUI渲染console.log(5)
作为同步任务打印5,然后检查有没有微任务和GUI渲染console.log(2)
作为同步任务打印2,然后检查有没有微任务和GUI渲染 所以最终结果为:1、4、8、7、3、6、5、2