1.关于javascript
javascript是一门 单线程 语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!
2.JavaScript 事件循环 Event Loop
javascript 上任务分为两种,分别为同步任务和异步任务。
同步任务:
在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务:
不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
在掘金上盗了一张图
** 导图要表达的内容用文字来表述的话:**
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
1_example
$.ajax({
url:'xxx',
data:{},
success: (res)=>{
console.log('发送成功');
}
});
console.log('代码执行结束');
// 代码执行结束 发送成功
** 上面一段简易的ajax请求 分析**
- ajax进入Event Table,注册回调函数success。
- 执行console.log('代码执行结束')。
- ajax事件完成,回调函数success进入Event Queue。
- 主线程从Event Queue读取回调函数success并执行。
2_example
我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?
答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。举例说明:
console.log("先执行这里");
setTimeout(() =>{
console.log('执行setTimeout');
},0);
console.log('执行结束');
//先执行这里
//执行结束
//执行setTimeout
3_example
上面说完了setTimeout,当然不能错过它的孪生兄弟setInterval。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。这句话请读者仔细品味。
let t = +new Date();
setInterval(()=>{
while((+new Date() -t)<3000){
//此处模拟睡眠3秒钟
}
console.log('11111');
},1000)
// 控制台会3秒后 立刻输出2个11111 后续会恢复正常 1秒打印一次11111
除了广义的同步任务和异步任务,我们对任务有更精细的定义:
macro-task(宏任务):
可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
包括整体代码script,setTimeout,setInterval ,setImmediate(Node环境支持)
micro-task(微任务):
可以理解是在当前task执行结束后立即执行的任务
包括 Promise.then,promise.catch ,promise.finally, process.nextTick(Node环境支持)
宏任务与微任务关系图
4_example
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务
setTimeout(()=>{
console.log('setTimeout');
},1000);
new Promise(resolve =>{
console.log("promise");
resolve();
}).then(function(){
console.log('then');
});
console.log('console');
// promise
//console
//then
//setTimeout
- 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。
- 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
- 遇到console.log(),立即执行。
- 整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
- 第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event
Queue中setTimeout对应的回调函数,立即执行。 结束
5_example
console.log(1);
setTimeout(()=>{
console.log(2);
new Promise(resolve =>{
console.log(3);
resolve();
}).then(()=>{
console.log(4);
});
},0);
new Promise(resolve =>{
console.log(5);
resolve();
}).then(()=>{
console.log(6);
});
setTimeout(()=>{
console.log(7);
new Promise(resolve=>{
console.log(8);
resolve();
}).then(()=>{
console.log(9);
})
},0);
console.log(10);
// 1 5 10 6 2 3 4 7 8 9
第一轮事件循环
1.整体script作为第一个宏任务进入主线程,遇到console.log(1);
2.遇到setTimeout, 将其回调函数分发到宏任务Event Queue中,暂且记为timer1
3.遇到promise ,new Promise直接执行,输出5。 遇到then 被分发到微任务Event Queue中,记为then1
4.遇到setTimeout, 将其回调函数分发到宏任务Event Queue中,暂且记为timer2
5.遇到console.log(10) 输出10
关系如图所示:
宏任务 | 微任务 |
---|---|
timer1 | then1 |
timer2 |
到这里 第一轮宏任务执行完成 ,看下微任务列表中then1 一个微任务,执行 输出6 。第一轮事件执行完成依次输出 : 1 5 10 6
第二次事件循环
从宏任务列表中timer1 开始:
- 遇到console.log(2) 输出2
- 遇到new promise 直接执行,输出3, 遇到then 分发到微任务Event Queue中,记为then2
3.第二个宏任务执行完成,查看是否有微任务,发现有一个then2的微任务 ,输出 4
关系图如下
宏任务 | 微任务 |
---|---|
then2 |
到这里 第二轮事件循环执行完成,输出: 2 3 4
第三次事件循环
从宏任务列表中timer2 开始:
- 遇到console.log(7) 输出7
- 遇到new promise 直接执行,输出8, 遇到then 分发到微任务Event Queue中,记为then3
- 第二个宏任务执行完成,查看是否有微任务,发现有一个then3的微任务 ,输出9
关系图如下
宏任务 | 微任务 |
---|---|
then3 |
到这里 第三轮事件循环执行完成,输出: 7 8 9
最后整体输出为: 1 5 10 6 2 3 4 7 8 9 (此结果只是浏览器环境输出结果,Node环境输出略有不同,与执行方式有关)
Nodejs中的事件循环
参照阮一峰大神 [JavaScript 运行机制详解:再谈Event Loop](http://www.ruanyifeng.com/blog/2014/10/event-loop.html)
写在最后
- javascript是一门单线程语言
- Event Loop是javascript的执行机制