我们都知道 javascript 是单线程的, Javascript 作为浏览器脚本语言,主要用途是与用户互动,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题。
同步任务 : 指的是在当前主线程(执行栈)中运行的任务, 只有当前一个任务执行完成, 下一个任务才会接着执行, 不管前一个任务执行需要多久
异步任务 : 不进入主线程, 存放在任务队列中, 但主线程的任务清空后,进入到任务队列中的,取出某个任务到 主线程中执行
宏任务 与 微任务
异步任务可分为宏任务与微任务, 微任务会先于宏任务执行
setTimeout 与 setInterval 的区别
堆 与 栈的概念
主线程代码在运行的时候会产生 堆 (heap) 和 栈 (stack)
我们结合上图来剖析一下 javascript 中的事件环
从图中可以看出当 javascript 运行时会在主线程上产生一个执行栈, 同步代码在当前执行栈中从上到到执行,在栈中会调用一些外部的 API ( click, ajax, setTimeout 等 ),将他们的 callback 事件依次加入到 宏任务中 即任务队列(消息队列)中,将 promise.then 等 属于微任务的放入到微任务队列中, 当执行栈中的同步代码执行完成后, 会先取出微任务中的 任务到当前执行栈中执行, 在执行的过程中,如果有调用 webAPI , 将对应的 callback事件依次放入任务队列中, 执行站中的任务执行完之后, 会再到 微任务队列中查看,如果有, 取出到执行栈中执行, 直到微任务被清空之后, 在去任务队列中 依次取出对应的 某个 任务到 执行栈中执行, 执行的过程同上, 如此循环形成了一个 event loop
总结为以下几点
1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
3. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
4. 主线程不断重复上面的第三步。
再来举个栗子
console.log('log1');
new Promise(function(resolve, reject) {
console.log('promise11');
resolve();
}).then(function() {
console.log('then1');
new Promise(function (resolve, reject) {
console.log('promise22');
resolve();
}).then(() => {
console.log('then22');
})
});
setTimeout(function() {
console.log('setTimeout');
}, 0);
console.log('log2');
当代码运行的时候, 会先执行同步的代码依次打印出 log1 promise11 log2 (promiese解释)
同步代码执行完后 会到 微任务队列中查看是否有任务需执行, 因此 取出 promise.then 执行, 这时打印出 then1 promise22, 将第二个 promise 的 then 回调发到微任务中, 当执行栈的代码执行完之后, 再到微任务队列中查看, 这时 取出第二个 promise.then 的回调到执行栈中执行 打印出 then22, 执行栈的代码执行完后, 再到微任务队列中查看, 此时发现微任务队列被清空, 再到 任务队列对取出 setTimeout 的 callback 执行 打印出 setTimeout
综上输出的结果为 log1 => promise11 => log2 => then1 => promise22 => then2 => setTimeout
Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。
先来看一张图, 这张图描述了 nodejs 的运行机制:
1. timers(计时器), 执行setTimeout以及setInterval的回调
2. I/O callbcacks, 执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调
3. idle,prepare node内部使用
4. poll(轮询), 获取新的I/O事件;node会在适当条件下阻塞在这里。
5. check, 处理 setImmediate 回调
6. close callbcaks,处理关闭的回调 如socke.on('close')
timers
一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。
注意:技术上来说,poll 阶段控制 timers 什么时候执行。
注意:这个下限时间有个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。
I/O callbacks
这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED,
类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行。
poll
poll 阶段有两个主要功能:
1. 执行下限时间已经达到的timers的回调
2. 处理 poll 队列里的事件
当event loop进入 poll 阶段,并且 没有设定的 timers(there are no timers scheduled),会发生下面两件事之一:
1. 如果 poll 队列不为空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;
2. 如果 poll 队列为空,则发生以下两件事之一:
- 如果代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里的回调)。
- 如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。但是,当event loop进入 poll 阶段,并且 有设定的 timers,一旦 poll 队列为空(poll 阶段空闲状态):
1. event loop将检查 timers, 如果有1个或多个timers的下限时间已经到达,event loop 将绕回 timers 阶段,并执行 timer 队列。
check
这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。
setImmediate()实际上是一个特殊的 timer,跑在event loop中一个独立的阶段。它使用libuv的API
来设定在 poll 阶段结束后立即执行回调。
通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。但是,只要有被setImmediate()设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。
close callbacks
如果一个 socket 或 handle 被突然关掉(比如 socke.on(‘close’)),close事件将在这个阶段被触发,否则将通过 process.nextTick() 触发。
在 nodejs 中微任务是在当前执行栈的尾部下一次 Event Loop(主线程读取”任务队列”)之前触发,并且每一次任务队列切换的时候都会清空当前一轮中微任务中的事件。
而每次切换到宏任务中的某一任务队列时, 都会清空队列中在本轮循环加入的 callback 函数, 清空之后, 查看微任务中如果有放入新的事件,拿到执行栈中执行, 执行完之后再切换到下一任务队列。
本轮循环指的是: JS 主线程会从任务队列中提取任务到执行栈中执行,每一次执行都可能会再产生一个新的任务, 对于这些新任务来说这次执行到下一次从任务队列中将它们提取到执行栈之前就是本轮循环
setImmediate() 和 setTimeout()是相似的,区别在于什么时候执行回调:
1. setImmediate()被设计在 poll 阶段结束后立即执行回调;
2. setTimeout()被设计在指定下限时间到达后执行回调
setImmediate(function A() {
console.log(1);
});
setTimeout(function B() {
console.log(2);
}, 0);
上面代码中,setImmediate 与setTimeout(fn,0) 各自添加了一个回调函数 A 和 B ,都是在下一次Event Loop 触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1 2,也可能是2 1
再来看一个例子
let fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('1')
}, 0)
setImmediate(() => {
console.log('2')
})
});
// 运行结果 2 1
这里的 setImmediate 就永远先于 setTimeout 执行
通过上面的两个例子可以得出:
- 如果两者都在主模块调用,那么执行先后取决于进程性能,即随机。
- 如果两者都不在主模块调用(即在一个 I/O callbcacks 中调用),那么 setImmediate 的回调永远先执行
假设 如果都在主模块调用, 而我们机器的性能一般, 那么进入timer阶段时,1ms 可能已经过去了,这时 setTimeout(fn, 0) 相当于 setTimeout(fn, 1), timer 中的回调会首先执行
如果没有 1ms , 在 check 阶段时 , setImmediate 的回调会先执行。
为什么 fs.readFile 回调里设置的,setImmediate 始终先执行?因为fs.readFile的回调执行是在 poll 阶段,所以,接下来的 check 阶段会先执行 setImmediate 的回调。
setImmediate(function A() {
console.log(1);
setImmediate(function B(){
console.log(2);
process.nextTick(function () {
console.log('nextTick');
});
setTimeout(function t1() {
console.log('t1');
})
});
});
setTimeout(function t2() {
console.log('t2');
setTimeout(function t3() {
console.log('t3');
});
setTimeout(function t4() {
console.log('t4');
});
}, 0);
答案: 1 => t2 => 2 => nextTick => t3 => t4 => t1 或者
t2 => 1 => t3 => t4 => 2 => nextTick => t1
你答对了吗 0-0