js中 setTimeout 等定时器执行顺序问题 + async

requestAnimationFrame介绍请看 http://www.w3cplus.com/javascript/requestAnimationFrame.html

setTimeout和setInterval介绍请看 http://www.w3cplus.com/javascript/JavaScript-setTimeout-and-setInterval.html

for循环中使用定时器打印输出问题

例1:

1
2
3
4
5
6
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
// 不断打印 5

      一开始我认为上述示例跟作用域有关,事实上是我理解偏差了。因为强行用作用域来解释我没法接受,直到百度到了下面这篇文章, https://blog.csdn.net/aitangyong/article/details/46800615 ,我才恍然大悟。这里继续贴上来:

      JavaScript是单线程执行的,无法同时执行多段代码。当某一段代码正在执行的时候,所有后续的任务都必须等待,形成一个队列。一旦当前任务执行完毕,再从队列中取出下一个任务,这也常被称为 “阻塞式执行”
      所以一次鼠标点击,或是计时器到达时间点,或是Ajax请求完成触发了回调函数,这些事件处理程序或回调函数都不会立即运行,而是立即排队,一旦线程有空闲就执行。假如当前 JavaScript线程正在执行一段很耗时的代码,此时发生了一次鼠标点击,那么事件处理程序就被阻塞,用户也无法立即看到反馈,事件处理程序会被放入任务队列,直到前面的代码结束以后才会开始执行。
      如果代码中设定了一个 setTimeout,那么浏览器便会在合适的时间,将代码插入任务队列,如果这个时间设为 0,就代表立即插入队列,但不是立即执行,仍然要等待前面代码执行完毕。所以 setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。

是不是跟作用域没关系呢?改写下就知道了
例2:

1
2
3
4
5
6
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
// 0 1 2 3 4

为什么会这样呢?貌似用上面那个解释行不通啊?
      这里是真的需要用作用域知识来解释了。letes6语法,js默认是没有块作用域的,但是!上述for循环内使用了let声明变量i,无形之中将for循环及其{}内部的代码构造成了一个块作用域,如果有后端比如java基础的人应该知道,此时必须先将该块作用域内的代码全部执行完才能执行下一段代码。这里可以将i = 0看成i0块,接下来的i = 1看做i1块,以此类推,或许比较好理解。(PS: 这种理解在原理上应该是不对的,只能算是根据结论去找类比。。。)
      当然,还可以继续改写,原理差不多。
例3:

1
2
3
4
5
6
for (var i = 0; i < 5; i++) {
setTimeout((function () {
console.log(i)
})(i), 1000)
}
// 0 1 2 3 4

      上例用到了立即执行函数,立即执行函数相当于构建了一个函数作用域,具有隔离作用域效果,可以参考:http://lipeng1667.github.io/2016/12/20/IIFE-in-js/
      说实话,一开始我不是很懂立即执行函数的原理,只知道()会立即调用函数,但其实我并不理解上例中for循环内为什么立即执行函数能正确打印输出?明明它在setTimeout内部啊,即使立即执行函数构建了函数作用域,但应该仅限于它内部啊,对于外部来说,不应该还是for循环完后才开始调用setTimeout吗???
      直到看了这里对js执行上下文栈的分析才大概了解点:https://github.com/mqyqingfeng/Blog/issues/4 ,比较重要的理解点:JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。以及当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做”执行上下文(execution context)”。
      我的理解是当js引擎检测上例代码时,先执行for循环内的var i = 0,然后去循环体内部查看是否有需要执行的代码。如果仅仅是setTimeout的话,由于浏览器对定时器有至少400ms的延迟,再加上定时器并不需要立即执行所以可以跳过(相当于异步操作了);但是如果定时器内部有立即执行函数,那么会立即构建一个函数作用域,使得线程进入该作用域内执行完毕才能出来。

      网上找到另一个解释:https://segmentfault.com/a/1190000010034556#articleHeader3。 这链接解释出现该情况的原因是settimeout的回调函数作用域链中最近的i不再是全局的i,而是块级作用域的i,也就是每一次不同的0,1,2,3,4,而不是全局i最后值5。

扩展 Promise的队列与setTimeout的队列有何关联

https://www.zhihu.com/question/36972010

个人总结

      个人理解,如果没有let或者立即执行函数等构建块作用域,那么setTimeout会等循环结束后调用,但是如果循环内构建了块作用域,那么setTimeout会立即调用。

后续更新

promise、async和await之执行顺序的那点事:https://segmentfault.com/a/1190000015057278

Newer Post

spring boot 踩坑

入门学习自: http://www.ityouknow.com/springboot/2016/01/06/springboot(%E4%B8%80)-%E5%85%A5%E9%97%A8%E7%AF%87.html踩坑 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;照着链 …

继续阅读
Older Post

vue爬坑成长汇总

目录 1.阻止事件点击 2.v-bind:src时图片加载问题 3.props传值问题 4. .native原生事件 5. vue-router 懒加载 6. vue项目里使用阿里的 iconfont 7. vue 父组件调用子组件方法并接收参数 $emit 8. vue watch 监听(父子组件 …

继续阅读