-
为什么javascript是单线程的?
因为javascript最初设计是运行在浏览器的,首先系统分配给浏览器的内存不会很多,其次javascript运行在浏览器的目的是为了与用户进行交互,假设javascript支持多线程,那么线程A要新增一个节点,线程B要删除一个节点,浏览器就不知道到底要干嘛了。
随着CPU的发展,HTML5提出了web worker。允许web worker创建子线程,但是子线程得受控于主线程,且不能操作DOM。
Event loop是一个执行模型,在不同的地方有不同的实现。在浏览器中遵循的是HTML的标准,在node中是基于libuv库来实现的。
浏览器中的Event Loop
HTML标准这样定义事件循环:
为了协调
事件
(event),用户交互
(user interaction),脚本
(script),渲染
(rendering),网络
(networking)等,用户代理(user agent)必须使用事件循环
(event loops)。有两类事件循环:一种针对
浏览上下文
(browsing context),还有一种针对worker
(web worker)。
在主线程执行的过程中会产生堆和栈。执行栈中的代码会依次去调用外部的API。这时候就会分同步代码和异步代码。同步方法就去执行栈执行,调用到异步方法的时候就会产生任务队列,异步方法的回调就去排队。执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行,在同步代码执行完了,空闲的时候才去看有没有异步任务要执行,如果这个时候任务队列正好有排队的任务,就去执行。
-
任务队列分为macro-task和micro-task:
macro-task:setTimeout,setInterval,setImmediate,I/O,UI rendering
micro-task:process.nextTick,Promise,Object.observer,MutationObserver
在执行任务队列中的方法的时候,会先去执行micro-task队列,再循环执行macro-task队列。
举个例子来说,在浏览器执行以下代码:
setTimeout(()=>{
console.log(1);
})
console.log(2);
new Promise((resolve,reject)=>{
console.log(3);
resolve();
}).then(()=>{
console.log(4)
});
console.log('end');
这样一段代码,执行结果是:2,3,end,4,1
分析一下:
1、在遇到setTimeout的时候,这个方法会去macro-task排队。
2、显而易见,同步方法,输出2。
3、输出3在resolve()之前,不用等待resolve()执行完,所以是同步的,输出3;输出4在then()中,需要等待执行,属于Promise,在micro-task排队。
4、显而易见,同步方法,输出end。
5、自此同步代码已经执行完了。
6、开始执行micro-task队列,所以输出4。
9、开始执行macro-task队列,所以输出1。
node中的Event Loop
nodejs的event loop分为6个阶段,每个阶段的作用如下:
timers:执行
setTimeout()
和setInterval()
中到期的callback。I/O callbacks:上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段执行
idle, prepare:仅内部使用(这个存疑,我也不是很懂内部在干嘛)
poll:最为重要的阶段,执行I/O callback,这个指的就是比如去读数据库啊,读文件之类的。在适当的条件下会阻塞在这个阶段(所谓适当的条件我要去查一查,现在不确定具体是什么样的条件叫适当)
check:执行setImmediate的callback
close callbacks:执行close事件的callback,例如
socket.on("close",func)
当然还是需要注意的就是,在每一个阶段执行之前还是会先把micro-task里的任务执行掉。
|-----------------micro tasks
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
| | <----- micro tasks
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
| | <----- micro tasks
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘
| | <----- micro tasks
| |
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
| | └───────────────┘
| | <----- micro tasks
│ ┌──────────┴────────────┐
│ │ check │
│ └──────────┬────────────┘
| | <----- micro tasks
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
举个例子,在node下执行代码:
setTimeout(()=>{
console.log('timer1');
Promise.resolve().then((resolve,reject)=>{
console.log('promise1')
})
})
setTimeout(()=>{
console.log('timer2');
Promise.resolve().then((resolve,reject)=>{
console.log('promise2')
})
})
根据node下的Event Loop执行顺序来看,输出结果应该是:
timer1,timer2,promise1,promise2
分析一下:
首先,micro-task是空的,执行timer阶段,执行setTimeout,输出timer1,timer2。promise1的then和promise2的then去micro-task排队。
然后,micro-task队列里有方法,输出promise1,promise2
再举个例子:
setTimeout(()=>{
console.log('timer1');
Promise.resolve().then(()=>{
console.log('promise1')
})
})
setTimeout(()=>{
console.log('timer2');
Promise.resolve().then(()=>{
console.log('promise2')
})
})
Promise.resolve().then(()=>{
console.log('promise3')
})
这段的输出是:promise3,timer1,timer2,promise1,promise2
分析一下:因为在执行timer阶段之前,会先把micro-task队列里的方法执行一下,所以promise3虽然写在后面了,也得先输出来。
再再举个栗子:
setTimeout(()=>{
console.log('timer1');
Promise.resolve().then(()=>{
console.log('promise1')
})
})
setTimeout(()=>{
console.log('timer2');
Promise.resolve().then(()=>{
console.log('promise2')
})
})
Promise.resolve().then(()=>{
console.log('promise3')
})
process.nextTick(()=>{
console.log('nexttick')
})
这段的输出是:nexttick,promise3,timer1,timer2,promise1,promise2
分析一下:因为在micro-task队列里的优先级,process.nextTick一定比promise先执行
需要注意的是:是异步方法的回调在任务队列排队。
/////setTimeout和setImmediate的执行顺序有待商榷,主要是会涉及到代码执行的时间,和HTML规定setTimeout默认的4ms问题,不足4ms补足4ms