2/21 个人理解的 JavaScript for 循环机制

写在前面

  昨天 我查资料 写了一篇比较乱的 博客 来探究

  为什么 在某些情况下 Promise 失效的原因

  起初 以为 只是 js 单线程 没有控制好 Event Queue 里面各项异步任务 的处理时间

  但是 最核心的问题 就是 for 循环的机制


 

首先一段代码

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

  这是大家 喜闻乐见 的一段代码,对于一些总结过的朋友,他们会说,当 for 循环中 遇到了执行时间比较长 的代码

  for 循环 会跳过 这一段处理时间长的代码,等到 for循环 结束 才会运行这段代码

  于是输出 3 3 3

  

  那么时间很小呢?

for(var i=0;i<3;i++){
     setTimeout(function(){
        console.log(i);
    }, 0); 
}

  

  怎么还是 这样呢?

  不应该是 0 1 2 吗

  .......  

  但是这只是 var i = 0 全局作用域的情况

  那么 let i = 0 呢

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

  这个时候输出 却变成了这样

  

  欸? 不是前面说

  for 循环 会跳过 这一段处理时间长的代码,等到 for循环 结束 才会运行这段代码

  这个时候 i 不是应该是 3了吗

  输出 也应该是 3 3 3 阿

  

  为什么会是这个样子呢,我们又再来看一下 这个代码

for (let i = 0; i < 3; i++) {
                    setTimeout(function () {
                        console.log(i);
                    }, Math.random() * 10000)
                }

  好家伙 直接乱序了 ( 大家可以试一下 var 的情况 )

  输出 结果

  

 

  那好,为了不让他 乱序 我们加一个 Promise 怎么样? 

 for (let i = 0; i < 3; i++) {
                    let timer = new Promise(function (resolve, reject) {
                        setTimeout(function () {
                            resolve(i); 
                        }, Math.random() * 10000)
                    })
                    timer.then((data) => {
                        console.log(data);
                    })
                }

  好家伙 为什么又炸了

  

 

  有些朋友 会觉得 百思不得其解

  但是 我现存一个理论 来解释这个东西 当然并不是权威的解释 只是我通过查资料 推导出来的

  我怕 误人子弟 如果您觉得这可能不是浪费时间的东西 请您接着往下面看

 

js的运行机制

 

     我们要谈为什么会出现这样的情况的话,还是要扯到 js 的运行机制

  js 是单线程的 如果有很多任务是同步操作的话, 上一个操作完成 才会处理下一个

  这无疑是开销巨大的

  所以 js 采用了一些 处理方式 来适应单线程 并对外看似是 多线程协调工作:

  js 把任务分为 异步任务 同步任务

  异步任务会 先注册一个函数 但是不执行 然后 放入 事件队列 等到 合适的时候 进入 主线程

  同步任务 会在主线程中 先执行 执行完了 才会 让处于事件队列的 异步任务 注册的的函数 进行执行

  当然 如果您没有适当接触过js的运行机制的话 您可能觉得 “这在说些什么”

  在此 我推荐 一位大佬的 文章 

  https://m.jb51.net/article/149358.htm

  加之一位up主的视频 来学习一下

  https://www.bilibili.com/video/BV1kf4y1U7Ln

  

  如果您看完 或者多少有些了解过后 请 接着 浪费时间 往下看

  如果按照 视频 里面的说法 ( 这应该是一种精简总结过的 结论 )

  js 处理任务的时候 有三部分

  1. call back stack 调用栈

  2. message queue 消息队列

  3. micro-task queue 微任务队列

  我的总结就是

  同步任务 就要先放到 调用栈里面

  而 异步任务 ( setTimeout ... fileReader.onload )  就要 先放到 消息队列中 

  对于 Promise 这种就得放到 微任务队列中 ( ta 的级别高一点 )

 

  对于之前的第一段代码

for(var i=0;i<3;i++){
     setTimeout(function(){
        console.log(i);
    }, (i+1)*1000); 
}

  如果按照视频里面总结过说的那样 ( 没有宏任务 微任务 那么细致 )

  而且 var 作用全局作用域

  我们的流程应该是这样

  注意

   

  对于 这些 操作 我认为这也是 同步任务 ( 仔细想一想这句话 )

  所以流程

  第一次 循环

  

  顺序执行,当遇到同步操作 放入主线程 ( 调用栈 ) 遇到 异步操作 注册一个函数 然后 放入 消息队列 ( 消息对列中的任务 调入到 调用栈的条件是 调用栈为空 && 任务时延结束

  接下来 还是如此

  

  

 

  此时 应该是 将 注册过后的 放在 消息队列里面的 异步操作 的函数 等待时延放入 我们的 调用栈中

  因为 这个时候 栈不为空 i 最终还得加一个 1 i = 3

  而且 var 声明的东西 全局范围都能用 并不是想 let 那些的“副本” 所以按照时延调入到 栈中

  

 

  依次下去 因为时间间隙不是很大 然后就像一次执行完了 3 3 3 

 

  同理 对于第二个代码

for(var i=0;i<3;i++){
     setTimeout(function(){
        console.log(i);
    }, 0); 
}

  其实 

  setTimeout ( fn ,0 ) 的含义是指定某个任务在主线程最早可得的空闲时间执行,意思是不用等待多少秒了,只要主线程内的同步任务执行完毕,栈为空,马上就执行

  说明 我们的 i ++ 的 过程 是一直在 栈( 主线程 ) 里面运行 的 

  而且 注册的函数 中的参数 指的是 var i 意思是 i 最后的结果 带入到我们注册的函数中

  那么 这次的 输出 也是 3 3 3 

  

  第三段 代码 

for(let i=0;i<3;i++){
    setTimeout(function(){
         console.log(i);
    }, (i+1)*1000); 
}

  我们触类旁通

  正如 谈到 var 的时候一样, 这个是个全局变量 相当于 只有一个内存空间 我们操作一次 就改变了ta

  但是 let 不一样 对他的操作 只限定于 局部 作用域 当我的 新的循环开始 相当于 我又多了一个 let 像一个副本一样 这两个 let 互不相同

  如果 这样 那么怎么 控制 let 的自增呢

  其实是 js 引擎内部 自己的事情 他会记录 let 的改变情况

  所以 对于let 我们稍微改一下 上面对于 var 操作的第三幅流程图

  

 

 

  当我们将消息队列中的 注册过的函数 调入的时候

  我们调用的 不一样的 i 

  也有 i_3 但是 for 循环外面会报错 因为作用于块 找不到

  结果

  

 

  倒数第二个代码

for (let i = 0; i < 3; i++) {
        setTimeout(function () {
        console.log(i);
        }, Math.random() * 10000)
}

  其实 大家看到这里 应该都会推了

  我写一下最后的调入

  

 

   ( 这个是我试出来 满足上面 可能的情况 )

 

 

 

  然后是 最后 一个代码 那个有 promise 的

for (let i = 0; i < 3; i++) {
                    let timer = new Promise(function (resolve, reject) {
                        setTimeout(function () {
                            resolve(i); 
                        }, Math.random() * 10000)
                    })
                    timer.then((data) => {
                        console.log(data);
                    })
                }

  这个 就要牵涉到 micro-task queue 了

  首先 提示一下 .then 也是 异步操作 也得放到 message queue 里面

  于是 第一次的循环 应该是这个样子的 

  

 

  循环结束时刻的样子

  此时,// 栈空 i_3 = 3

  然后 微任务队列中的 timer 开始进入 栈 ( 这里应该是先进先出 )

  

 

  第一次 调入

  

  最终 是这个样子的

  

 

 

     最后的调入省略

  输出 所以还是这个样子

  当然 如果想阻塞后面的输出的话 可以用 async await 来处理

  阻塞后面 i 的自增

  详情 请看 之前 我写的一篇有些许错误的 随笔( 相对于这一篇 文章 )

  https://www.cnblogs.com/WaterMealone/p/14423722.html

参考文献

  https://www.cnblogs.com/hq233/p/8042995.html

  https://www.bilibili.com/video/BV1kf4y1U7Ln

  https://m.jb51.net/article/149358.htm

总结

  以后 写一些代码还是 用 let 吧 虽然有些地方看似不会有冲突

  但是考虑 作用域的话 还是用 var 好一些 下面代码 可以找得到 变量

  以后 推荐用 async await 做到同步 来阻塞后面代码的执行

  for 循环考虑 同步操作 i 的自增

 

posted @ 2021-02-21 14:08  WaterMealone  阅读(292)  评论(0编辑  收藏  举报