先举个栗子,以下:javascript
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log('i: ',i); //一秒以后输出几乎没有时间间隔依次输出5个5 }, 1000); } console.log(i); //当即输出5
想必不少人看到立马能看出答案吧,可是为何定时器不能依次打印出0, 1,2,3,4呢?答案稍后分晓。java
那到底怎么才能依次输出咱们想要的结果呢?你们可能都想到是利用闭包,或者是利ES6中的let声明,再或者能够用Promise, 若是还不过瘾就用ES7 的async或者await; 以下,可是今天咱们主要不讲这个。web
//利用闭包 for (var i = 0; i < 5; i++) { (function(i) { setTimeout(function() { console.log(i); //0,1,2,3,4 }, 1000); })(i); } console.log( i); //5 //let for (let i = 0; i < 5; i++) { setTimeout(function() { console.log (i); }, 1000); //0 ,1,2,3,4 } console.log( i); //这里会报错,由于let声明的块级做用域,外面是拿不到这个i的,使用没有声明的变量固然会报错啦
1、为何js是单线程?浏览器
你们都知道js不一样于其余语言,它是单线程的。那么问题来了,为何不是多线程呢?按道理来讲多线程不是可以同时解决问题提升效率么?除了多线程产生冲突、抢占资源等答案,还能够是什么呢?多线程
其实,这跟它做为浏览器脚本语言的用途有关,浏览器的脚本语言主要的用途是用来与用户互动,会产生DOM的操做,这就是问题的关键,假设js是多线程,有一个线程是删除DOM操做,有个在当前DOM添加内容,这时候浏览器应该怎么办呢?这就决定了js应该被设计成单线程。那js有没有多线程的可能呢?答案是确定的,HTML5提出的web Worker标准,可是,闭包
“为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质” ----阮一峰的一篇博客。异步
二 、 Event Loop 事件循环async
由于js是单线程,因此执行时候须要排队,前一个任务结束以后,后一个任务才会执行,那么若是前一个任务执行好久,后面一个任务就要等好久。若是一些等待不是由于CPU计算慢产生的,好比IO设备的使用,那么js会把等待的任务挂起,执行后面的任务,等IO返回结果,再回头执行直线挂起的任务。函数
这样任务就有了同步任务和异步任务,同步任务是主线程上执行的任务,造成一个执行栈(execution context stack),是排队进行的。异步任务不是在主线程执行的,是在“任务队列”(Queue)里面,只有当主线程全部同步的任务执行完成,任务队列中的异步任务才会进入主线程,而后被执行。oop
异步执行机制以下:---阮一峰
可视化描述---MDN
主线程上:
一个函数1被调用了,建立一个堆栈帧,包含了函数1的参数和局部变量,当函数1中又调用了函数2,又建立了一个堆栈帧,此时这个堆栈帧在第一个堆栈帧以前,包含了函数2的参数和局部变量。主线程先执行了至于顶层的堆栈帧(函数2产生的),当函数2返回时,对应的堆栈帧就出栈了,接着继续执行函数1的堆栈帧,直到栈空了。
消息队列:
一个 js运行时包含了一个待处理的消息队列。每个消息都与一个函数相关联。当栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及于是建立了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。
添加消息:
在浏览器里,添加事件能够是当一个事件出现且有一个事件监听器被绑定时,消息被随时添加。也能够是在调用setTimeout等函数时候,
在未来的某个时间后在消息对列中添加。
setTimeout:
调用该函数时候会在未来的某个时间后在消息对列中添加一个消息,若是执行栈中有其余任务没有完成(假设有一个很耗时的计算),setTimeout消息必须必须等到执行栈的任务完成才会处理,因此说该函数的第二个参数仅仅表示最少的时间 而非确切的时间。
即便你设置零延迟:
setTimeout(function cb1() { console.log(‘我是第二个被执行的’); }, 0); console.log(‘我是第一个被执行的’); //先打印这句
事实上,js中规定,定时器的第二个参数设置最少不能小于4ms, 小于的话就按最小的4ms执行。
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各类外部API,它们在"任
务队列"中加入各类事件(click,load)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
3、回头看看开头的栗子
for循环和外面的console.log()是主线程上的同步任务,他们按循序执行,先执行for循环结束(此时i变为5),再执行console.log();
for循环中的setTimeout是在任务队列中,它只有等在前主线程中的栈空了以后,到一个时间才会被被执行,此时做用域中的i已经变为5,此时setTimeout中的回调函数所读取的i就是5了。
还有一个问题?
输出5的时间间隔是多少?答案显而易见是,当即打印一个5,1000ms以后几乎同时输出5个5; why???
正如上面解释的,for循环一次,会在任务队列中加上一个setTimeou任务(该任务是在1000ms后执行回调函数),这样循环结束,任务列表里面就有了5个setTimeou任务,且当主线程中栈空了以后,任务列表就开始进栈,等待1000ms以后执行回调,(注意此时的i变量已经变成5了)因此后面的5个5几乎在同时依次打印出来。
4、任务队列
JS分为同步任务和异步任务;
同步任务都是在主线程上执行,造成一个执行栈;
主线程以后,事件触发线程管理着一个任务队列【宏任务(MacroTask)和微任务(MicroTask)】,只要异步任务有了运行结果,就会往任务队列里面放置一个事件。
一旦执行栈中的全部同步任务执行完毕,此时JS引擎空闲,系统就会读取任务队列,将以前异步任务的结果(事件)添加到可执行栈中,开始执行。
4.1 宏任务
task(又称之为宏任务),能够理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了可以使得JS内部task与DOM任务可以有序的执行,会在一个task执行结束后,在下一个(macro)task 执行开始前,对页面进行从新渲染,流程以下:
task主要包含:script(总体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
microtask(又称为微任务),能够理解是在当前 task 执行结束后当即执行的任务。也就是说,在当前task任务后,下一个task以前,在渲染以前。
因此它的响应速度相比setTimeout(setTimeout是task)会更快,由于无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的全部microtask都执行完毕(在渲染前)。
microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
在事件循环中,每进行一次循环操做称为 tick,但关键步骤以下: