前言
浏览器内核(浏览器渲染进程)多线程
- GUI渲染线程:负责渲染浏览器界面,解析HTML、CSS,构建DOM树,布局和绘制。当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- JS引擎线程:负责解析Javascript脚本,运行代码。
- 事件触发线程:当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。
- 定时触发器线程:setTimeout、setInterval所在线程。浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确),因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 异步http请求线程:负责执行异步请求,检测到请求状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理
GUI渲染线程与JS引擎线程互斥,当执行JS引擎线程时,GUI渲染会被挂起,当任务队列空闲时,主线程才会去执行GUI渲染,当js脚本执行时间过长,将导致页面渲染的阻塞。
JavaScript是一门单线程的非阻塞的脚本语言。
单线程:JavaScript代码在执行时,都只有一个主线程来处理所有的任务
单线程why?
JavaScript最早设计初衷:运行在浏览器端的脚本语言,为了实现页面上的动态交互,实现页面交互的核心就是操作dom,假如是多线程模式就会出现线程同步问题:多个线程一起工作,一个修改dom,一个删除dom,浏览器不知道先执行哪一个, 避免这种线程同步问题,JavaScript只能是单线程。
优点:安全 简单
缺点:耗时任务可能造成假死现象
于是JavaScript将任务的执行模式分成了两种:同步模式、异步模式
非阻塞:代码需要进行异步任务时,主线程会挂起这个任务,在异步任务返回结果的时候在去执行相应的回调
web workers在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是UI线程)不会因此被阻塞/放慢。所以并没有改变js是单线程的本质,可以说webworkers是js开得外挂
浏览器环境
调用栈、事件队列
内存堆:内存分配发生的地方。
调用栈:代码执行时的地方。
JS代码首次运行,都会先创建一个全局执行上下文并压入到调用栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;当该函数执行结束时,执行上下文从栈中弹出,进入上一个函数的执行环境。这个过程反复进行,直到执行栈中的代码全部执行完毕。
console.log("A")
function bar(){
console.log('bar task')
}
function foo(){
var a =1;
console.log('foo task')
bar()
console.log('foo end')
}
foo()
console.log("B")
事件队列:
js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列—事件队列。
被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这个过程被称为“事件循环(Event Loop)”
练习:
console.log("A")
setTimeout(function(){
console.log("定时器异步回调")
},1000)
fetch('./data.js')
.then(function(response) {
console.log("http请求异步回调")
})
console.log("B")
setTimeout(()=>{
console.log(1)
},20)
console.log(2)
setTimeout(()=>{
console.log(3)
},10)
console.log(4)
// console.time("AA")
// for(let i = 0; i < 90000000; i++){ //大概80ms
// //do something
// }
// console.timeEnd("AA")
setTimeout(()=>{
console.log(6)
},20)
console.log(7)
setTimeout(()=>{
console.log(8)
},10)
微任务、宏任务
异步任务被分为两类:
宏任务(macro task):
主线程代码(script中的代码)、
setInterval()、
setTimeout()、
setImmediate()、
requestAnimationFrame 、//执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画
UI render、
I/O 流、
Ajax请求
微任务(micro task):
process.nextTick 、
new Promise().then()、
async await 、
new MutaionObserver() //监视对DOM树的更改
不同的异步任务 执行优先级也有区别。
在事件循环中会根据异步事件的类型,把这个事件放到对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回调加入当前执行栈;如果存在,则会依次执行微任务队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。
我们只需记住当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
练习:
1.
console.log(1)
setTimeout(()=>{
console.log(2)
},20)
new Promise(function (resolve){
console.log(3)
resolve()
}).then(function (){
console.log(4)
})
console.log(5)
- async await 是promise语法糖,所以可以转为promise 比较容易看出来执行顺序
async function async1(){
console.log("A")
await async2()
console.log("B")
}
async function async2(){
console.log("C")
}
console.log("D")
// setTimeout(function (){
// console.log("E")
// },0)
async1()
new Promise(function (resolve){
console.log("F")
resolve()
}).then(function (){
console.log("G")
})
console.log("H")
原则:先同后异 先微后宏
Node环境
每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。
timers定时器:执行已经到期的 setTimeout() 和 setInterval() 的回调函数。
pending callbacks待定回调:执行延迟到下一个循环迭代的 I/O 回调。
idle, prepare:仅系统内部使用,可以不必理会。
**poll 轮询:**执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调,直到队列已用尽,或者达到了与系统相关的硬性限制。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,受到代码运行的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有回调被添加到队列中,然后立即执行。
check 检测:执行setImmediate()方法的回调,当poll阶段进入空闲状态,并且setImmediate queue中有callback时,事件循环进入这个阶段。
close callbacks 关闭的回调函数:执行一些准备关闭的回调函数,如:socket.on(‘close’, …)。
setTimeout setImmediate
执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时器将受进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。
例如,如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
如果两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用
const fs = require('fs');
fs.readFile('./data.js', () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
process.nextTick
process.nextTick,其不属于事件循环的任何一个阶段,Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。
尽管没有提及,但是实际上node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
process.nextTick(() => {
console.log('nextTick 2');
});
});
process.nextTick(() => {
console.log('nextTick 1');
});
});
版本变化:
Node 11之后事件循环的原理发生了变化,和浏览器执行顺序趋于一致。
在Node v11以前,微任务和宏任务在Node的执行顺序:
1.执行完一个阶段的所有任务
2.执行完nextTick队列里面的内容
3.然后执行完微任务队列的内容
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
for(let i=0;i<900000000;i++){
}
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout0')
}, 0)
setTimeout(function () {
console.log('setTimeout2')
}, 300)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick1'));
async1();
process.nextTick(() => console.log('nextTick2'));
new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})
console.log('script end')