event loop ? microtask ? macrotask ? 不好意思,我记不住,关于这方面的解释以及深入理解一大堆的中英结合,中间穿插着各种 setImmediate、requestAnimationFrame。‘我才学,脑袋大,通俗点,886 Ctrl+W’。
我想用俗话说
我先把主要围绕的例子列出来,也主要是通过这个最常见代码去解释一下我对event loop的理解:
for(var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
},1000*i);
}
A:哇,你个水货,先出来一个5 然后 一秒一个 5,面试经典题还用你教? (张学友的问候.jpg)
me: 好像还可以在深入一下....,比如:
- 为什么每次都是5;
- 为什么是你理解的1s一次;
- 为什么用立即执行函数(IIFE)就可以解决, 以及ES6中的let。
我想,等我废话说完你可能会有个大概理解了,到时候我想基于这个理解再写一篇关于AMD的白话文实现。
上道具
阻塞事件
js是单线程,在代码执行阶段对代码进行分类处理,按照我的理解就是:当我找到你的时候,你就必须去做或者立刻告诉我结果。就好比我去订餐,有没有位子就在此次通话内告诉我。
那我们的例子来说就是:
for(var i = 0; i < 5; i++) {}
=> var i = 0; {做点什么}(i++); {做点什么}(i++); {做点什么}(i++); {做点什么}(i++);...
//当然会说了,明明还要 i<5 的判断了,这个里判断也算是阻塞。很小的一部分。
这里就是 就往 阻塞队列增加了5个事件(当然肯定不止5个,每个事件内部还有事件),并按照先进先出(堆)的原则去执行。
非阻塞事件
当程序读取到了非阻塞事件,将该事件如加入到当前的非堵塞队列,等待同一个任务队列中的阻塞队列执行结束后,再去执行非阻塞队列中的任务,当然也是先进先出
setTimeout(function(){console.log('1')},500); //A
setTimeout(function(){console.log('2')},100); //B
=>
2
1
不要以为B比A先出来,就以为B比A先执行。这个,肯定要讲。
根据列子来说:
setTimeout(function() {
console.log(i);
},1000*i);
// 就是往 非阻塞队列 添加了 五个非阻塞时间
// 这个时候我们一定要区分 console.log(i)与 setTimeout(fn,1000*i) 中的 i;
//当然他们是同一个i,只是时刻不同,那么时刻不用是什么?
- console.log(i)中的 i: 是当这个非阻塞事件可以执行的时候去获取的 i;
- setTimeout(fn,1000*i)中的 i : 是注册这个非阻塞事件并向非阻塞事件队列添加的时候的 i,这个I就是0 1 2 3 4(这一个跟开始我抛出的,为什么是你理解的1秒1次5有关);
整体分析
- 首先必须明确的是,非阻塞队列只有在对应的阻塞队列执行完毕后才能执行。
- 非阻塞队列,并不一定是一次循环执行就能结束的,也就是说按照注册顺序执行,满足条件的事件执行回调并移除该队列。
解答
为什么输出console.log(i),输出5个5;
因为所有非阻塞队列中的fn0-fn4中都是 console.log(i);
根据js设计思想,只有当用到某个变量时,才去寻找这个变量的值,这个时候非阻塞队列是在阻塞队列执行完全之后再执行的;
那么这个时候的i已经经过了5个for循环5个i++从一开始的0变成了5,所以在fn0-fn5执行的时候console.log(i),i 的值 已经是5了。
为什么是你理解的1s一个5
首先它的确是 0s 5 ;1s 5 ;2s 5 ;3s 5 ;4s 5 ;
为什么又是1s一个5呢? 参照物
当你根据每个console .log(i)出现为基准,就如同大家都在一个房间,A出门走1步,B出门走2步,C出门走3步。。。
相对于ABC...每个人来说,他们之间的确相差了1步,
但是相对这个房间来说,ABC...每个人相对于房间可就不是固定的1步了。
我在阻塞队列注册了五个非阻塞事件,谁告你这几个setTimeout之间有关系的。。。,
我一个人玩的setTimeout为什么要根据你的setTimeout结束才能玩?
解答:
在阻塞队列执行的时候,注册了五个非阻塞事件,例如:
阻塞事件中通过setTimeout注册了一个非阻塞事件,以这个注册时间为基准,至少1000*i ms(这里的i肯定0 1 2 3 4 而不一直都是5,因为我注册事件的时候就要用到i,那就要拿到当时的i值)之后去执行这个fn(setTimeout里面的fn回调函数);
那么五次循环之后(阻塞队列执行完毕),实际上就是已经注册了五个不同时间节点的非阻塞事件,虽然说可以说五个非阻塞事件注册时间基本上是同一个时间点,但是程序执行的确是要花费时间的,只能说太快,事件太少,所以我们基本可以理解为这个5个setTimeout注册几乎就在一个房间,而每次注册的时候已经告诉了这个回调函数至少多少ms之后执行(就是告诉ABC...要相对这个房间走多少步)。
所以,他们的确是针对注册这个非阻塞事件时 , 0s 5 ;1s 5 ;2s 5 ;3s 5 ;4s 5 ;
为什么用立即执行函数(IIFE)就可以解决, 以及ES6中的let
因为闭包
for(var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
},1000*i);
})(i)
}
实际上就是每个非阻塞事件再执行的时候去查找变量,通过IIFE给setTimeout与for循环之间多加了一层向上查找变量的作用于,本来是
其实你可以试试 把加入 IIFE 的console.log( j )变成console.log( i );
结果就还是 ** 5 个 5 因为 IIFE提供的闭包环境中 只提供了一个 j 变量, 当没有找到 i 时 ,又得向上查找,这个时候就找到了阻塞事件**执行完毕之后的 i ,那还不就 5 嘛~
let 其实就是让每个for循环有自己作用域块,从** 5 个 console.log(i)对应一个 ** i (不加let)变成 5 个 console.log(i)对应5个 i ,这每个 i 并不是同一个了。
结束
写到这里,其实已经很长了,再写下去可能就要开始乏味了。那我再说两个小点吧
- promise : 其实就是 阻塞事件 的 干儿子,不管setTimeout在promise之前多少(当然我们这里说的是同一个任务),每次去注册非阻塞事件的时候,promise肯定是在阻塞队列结束之后立即去执行的,就好比 阻塞事件每次执行完了就会说 “干儿子(promise),我好了你赶紧到我后面来,别让setTimeout它们先跑了”
- setTimeout(fn,times),setInterval(fn,times): 它们中的 times,并不是说 隔 **xxx ms ** 后去执行,而是至少 xxx ms 只执行,为什么呢,说太多写不下,自己慢慢体会。