JS浏览器事件循环机制
进程和线程的关系
- 进程是系统分配的独立资源,是CPU资源分配的基本单位,进程是有一个或者多个线程组成的。
- 线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程资源的。
- 做个简单的比喻:进程=火车,线程=车厢线程在进程下行进(单纯的车厢无法运行)一个进程可以包含多个线程(一辆火车可以有多个车厢)不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
链接:https://www.zhihu.com/question/25532384/answer/411179772
来源:知乎
浏览器内核
-
浏览器是多进程的,浏览器的每一个tab标签都代表一个独立的进程,(也不一定,因为多个空白tab标签会合成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。
-
浏览器内核有多种线程在工作
- GUI渲染线程:
- 负责渲染页面,解析HTML,CSS构成DOM树等,当页面重绘或者由于某种操作引起回流都会调起该线程。
- 和JS引擎线程是互斥的,当js引擎线程在工作时,GUI渲染线程会被挂起,GUI更新被放入在JS任务队列中,等待JS引擎线程空闲的时候继续执行。
- JS引擎线程:
- 单线程工作,负责解析运行javascript脚本
- 和GUI渲染线程互斥,js运行耗时过长就会导致页面阻塞
- 事件触发线程:
- 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待JS引擎处理。
- 定时器触发线程:
- 浏览器定时计数器并不是由JS引擎计数的,阻塞会导致计时不准确。
- 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待js引擎处理
- http请求线程:
- http请求的时候会开启一条请求线程。
- 请求完成有结构了之后,将请求的回调函数添加到任务队列中,等待js引擎处理。
- GUI渲染线程:
JavaScript
JavaScript引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。
HTML5中提出了Web-Worker API,主要是为了解决页面阻塞问题,但是并没有改变JavaScript是单线程的本质。了解Web-Worker网址:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API
JavaScript事件循环机制
Java事件循环机制氛围浏览器和Node事件循环机制,两者的实现技术不一样,浏览器Event Loop是HTML中定义的规范,Node Event Loop是由libuv库实现。这里主要讲的是浏览器部分。
javascript有一个main thread主线程和call-stack调用栈(执行栈),所有的任务都会被放到调用栈中等待被主线程调用执行。
-
JS调用栈
JS调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从站顶部移出该函数,直到栈内被清空。
-
同步任务、异步任务
JavaScript单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
-
Event Loop
调用栈中的同步任务都执行完毕,栈被被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入栈中执行。每次栈内被清空,都会去读取任务队列中有没有任务,有就读取执行,一直循环读取-执行操作,就形成了事件循环。
-
定时器
定时器会开启一条定时器触发线程来触发计时,定时在会在等待了指定的时间后将事件放如任务队列中等待读取到主线程执行。
制定其制定的延迟毫秒数其实并不准确,因为定时器只是在到了指定的时间将事件放入到任务队列中,必须要等到同步的任务和现有的任务队列中的事件全部执行完成之后,才回去读定时器的事件并到主线程执行,中间可能会存在耗时较久的任务,那么就不可能保证在指定的时间执行。
-
宏任务、微任务
除了广义的同步、异步任务,javascript单线程中的任务可以细分为宏任务和微任务。
宏任务包括:script(整体代码),setTimeout,setInterval,setImmediate,I/O,UI rendering。
微任务包括:process.nextTick,Promises,Object.observe,MutationObserver。
//setTimeout和Primise被称为任务源,来自不同的任务元注册的回调函数会被放入不同的任务队列中。
console.log(1);
setTimeout(function() {
console.log(2);
})
var promise = new Promise(function(resolve, reject) {
console.log(3);
resolve();
})
promise.then(function() {
console.log(4);
})
console.log(5);
第一次事件循环中,javascript引擎会把整个script代码当成一个宏任务执行,执行完成之后,再检测本次循环中是否寻在微任务,存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取宏任务的任务队列中的任务执行,再执行所有的微任务,如此循环。JS 的执行顺序就是每次事件循环中的宏任务-微任务。
- 上面的示例中,第一次事件循环,整段代码作为宏任务进入主线程执行。
- 遇到了 setTimeout ,就会等到过了指定的时间后将回调函数放入到宏任务的任务队列中。
- 遇到 Promise,将 then 函数放入到微任务的任务队列中。
- 整个事件循环完成之后,会去检测微任务的任务队列中是否存在任务,存在就执行。
- 第一次的循环结果打印为: 1,3,5,4。
- 接着再到宏任务的任务队列中按顺序取出一个宏任务到栈中让主线程执行,那么在这次循环中的宏任务就是 setTimeout 注册的回调函数,执行完这个回调函数,发现在这次循环中并不存在微任务,就准备进行下一次事件循环。
- 检测到宏任务队列中已经没有了要执行的任务,那么就结束事件循环。
- 最终的结果就是 1,3,5,4,2。
行完这个回调函数,发现在这次循环中并不存在微任务,就准备进行下一次事件循环。
- 检测到宏任务队列中已经没有了要执行的任务,那么就结束事件循环。
- 最终的结果就是 1,3,5,4,2。