写在前面
昨天把以前写的一个代码改了
原因是因为发现 Promise 的异步处理 竟然没有效果
我不知道为什么会出现这种情况
于是去问大佬,人家说要用 async await
在此之前我试过很多东西 以为是 for 循环机制的问题 ( 有待商榷 // 斜眼笑 // )
但是研究了一会儿 无果
今天弄了之后 发现万变不离其宗
这是 js 单线程的机制问题
( 这篇随笔是我自己对知识内化的一个过程 有些地方是我自己想的 可能不对 误人子弟 希望有缘人 辩证地看 )
首先一段代码
1 for (let i = 0; i < 5; i++) { 2 let timer = new Promise(function (resolve, reject) { 3 setTimeout(() => { 4 resolve(i) 5 }, Math.random() * 10000) 6 }) 7 8 timer.then((data) => { 9 console.log(data); 10 }) 11 }
运行一下,你会发现,本来想到的是同步处理的方式 0,1,2,3,4,变成了随机输出,
" 是呀,不是 应该 每一次 timer 中的 resolve 是在 setTimeout 时延结束了才会 resolve的吗,然后才触发 then 的 "
" 所以 为什么 会出现这种情况呢?”
看似 我们的 promise 失去效果了 但是其实不是 这是因为我们没有深刻理解 js 的执行顺序( 加上这里有一个 for 循环 )
如果运行出来,你清楚怎么回事,那么应该没有什么问题 ( 当然我是一个小白 ... 下面只是说一下我查阅资料并自己研究之后的结果 )
JS引擎的机制 ( 自我理解版本 )
其实解决 Promise 失效的问题的话,用 async await 就好了
如果不想探究 请参考 我那天写的随笔
https://www.cnblogs.com/WaterMealone/p/14418421.html
但是 为什么 呢?
我准备 探究一下 这个问题
要说 async 和 await 的话,我们不得不说 先说 Promise 、
说到 Promise 又要扯到 js 的执行顺序 Event Loop
......
话说回来 我先自己复习一下 Event loop ( 下面的说法酌情看 我觉得可能是错的 )
比如 如下代码
有些时候,对于 js引擎 不太熟悉的朋友 就会觉得 可能就是一直 按照顺序打出
但是结果是这个 ( undefined 应该是没有返回值 参考 eval( ) )
其实在执行的时候,有三个东西,( 有些地方比较抽象 分成了 宏任务 macro-task 和微任务 mirco-task , 还有 各自的 event queue )
一个是 调用栈 call back stack
第二个是 消息队列 message queue
第三个 是 微任务队列 microtask queue
具体参考如下视频
https://www.bilibili.com/video/BV1kf4y1U7Ln
下面是我的想法 如果以后看了相关资料 如果有错会更正
针对上面的代码:
console.log() 是同步任务 应该是属于 调用栈
setTimeout() 是异步任务 应该是属于消息队列
for 循环是依次执行的
当第一次循环 中
根据代码的顺序 setTimeout 进入 消息队列 ( 看 setTimeout 的 时间延迟&&栈空 才将代码放入 调用栈 )
然后 console.log 被压入调用栈 并执行
( 也就是先在 主线程中 进行同步任务,异步任务 ( 函数 ) 在 Event table 中 被注册 并且 自己的时延结束 然后才放到 Event queue 等到 主线程的同步任务处理完后 才在队列里面的 读取对应的函数 )
这里我的想法就是 虽然现在的 栈被执行 空了 之后,本应该执行 消息队列的代码,但是因为 最小时延 为4ms
( setTimeout ( fn ,0 ) 的含义是指定某个任务在主线程最早可得的空闲时间执行,意思是不用等待多少秒了,只要主线程内的同步任务执行完毕,栈为空,马上就执行 )
紧接着 for 的第二次 循环也到了 调用栈 不为空 由此压入了console.log() 如此反复
( 也有可能是 for循环相当于 一次次 代码的累积 顺次执行 )
当 i = 4 时
这个时候,console.log 马上就要执行完了
栈为空 按顺序执行 setTimeout 中的代码
setTimeout 中的代码被 压入 调用栈 中开始执行
综上,我们得到的是
当然 这里 setTimeout 的执行 时间几乎是同一时刻,这里我认为是因为 执行的时间并没有多长
那么这几个语句几乎就只差 没少的ms 在视觉层面上就像是 一瞬间 输出 0 1 2 3 4
所以我这样改了一下代码
这个的效果 就是 最先打出来 5 个 out loop
然后 等待 1 秒钟
0 - 4 依次从控制台 输出出来
简单的 自问自答 结束了 ( 我不知道 这样的想法有没有错 但是 现在在没有其他资料的情况下 这个解释 能适合我 )
但是,如果我们想要 按照顺序来弄 ( 同步操作 )
我们应该怎么做呢
这里 我首选的想法就是 Promise ( 但是有些时候会不尽人意 )
Promise
Promise 简单来说 就是一个承诺,这个承诺指的是,Promise 实例里面的代码做完了 ( resolve() ,或者是 reject( ) 传出参数 ) 之后才能继续 ( then( ) )
( 简单理解一下,但是有些情况下并不是这样的 )
具体 我之前写了一篇随笔
可以将就看一下
https://www.cnblogs.com/WaterMealone/p/14396590.html
然后 如果看了我之前的随笔还是不明白的话,可以再去看看其他大佬的分析
然后 Promise 最好用的 结构 应该是 套用在一个 funtion 里面 再去调用 ( 这样之后可以传递参数 )
这里 我并没有这样做...
我们可以将 原来的代码改成这样
然后我们的输出就变成了 这样 ( Promise 的pending 状态 表示 此刻正在处理 应该是 当 for 循环调用 timer() 时,return 的那个 Promise )
这里省略了then异步操作的流程,因为那样讲的话太过臃肿了
这里的执行过程 应该是 timer 进入了 微任务队列,当我们栈里面为空的时候,才执行 微任务队列里的任务
然后根据 setTimeout() 的时间延迟 把message queue 队列中的 setTimeout ()放到 栈中执行
因为有了resolved() 返回的参数 then 得以执行( 这里 setTimeout 的时间延迟都是一样的 所以是几乎一下子顺序输出 // 有一些处理时间,但是应该不是很大的时间间隙 // )
所以 输出了上面 按序输出的结果
解释最开头的代码
好了 看了那么多之后,我相信大家对于这个执行流程 应该有了一个简单的认识
如果 现在有缘人看到这里没有被我 语无伦次的文章绕昏了的话
我相信你现在自己也能 推出为什么开头的代码会是这个样子
其实 我开头的那段代码其实是在模拟 一个 fileReader.onload 的操作 ,因为 onload 钩子函数是在 执行完load之后执行的
所以这一段时间是随着 文件的性质 决定的
模拟的这个代码如图
那么为什么会造成这样的情况呢,其实就是因为 执行时间的不同
再回顾一下 最前面的问题代码
这个的执行顺序应该是这个样子的,这次我们也同样省略一下 then
其实前面的处理过程和之前的差不多,最后的一部分差了
前面无非也是
第一次循环 timer_1 进入 微任务队列,然后栈空 timer_1 进入栈... timer_1中的 setTimeout_1 进入 消息队列 等待栈空&&时延结束 进行调入栈
第二次循环 timer_2 进入 微任务队列,然后栈空 timer_2 进入栈... timer_2中的 setTimeout_2 进入 消息队列 ( 此时可能setTimeout_1 执行 也可能 setTimeout_1 和 setTimeout_2 都存在 ) 等待栈空&&时延结束 进行调入栈
....
我们的 message queue 里面的 setTimeout 的时延 并不是一样的,导致了调入 栈中的 顺序不同 ( 可能 后进入队列的先调用 然后处理就先给 then 了 )
那么我们的解决方法是什么呢?
解决方法
async await
现在 目前对于这种 ”队列情况“ 我想到最简单的方法就是 await 了
( 当然 可以写个递归函数 但是太麻烦了 // 为什么写递归呢 可以接着看 // )
在一篇大佬 介绍 async await 的文章中,我找到了这个
https://segmentfault.com/a/1190000007535316
我们需要阻塞,不然这些个代码这是会按照 时延 给调到 栈里面执行时延少的就先给执行了
于是 我们这样开始改代码
1 for (let i = 0; i < 5; i++) { 2 let timer = new Promise(function (resolve, reject) { 3 setTimeout(() => { 4 resolve(i) 5 }, Math.random() * 10000) 6 }) 7 testTimer(); 8 async function testTimer() { 9 let num = await timer; 10 console.log(num); 11 } 12 13 }
哦豁 为什么 还是错的呢?
其实 这里
唯一合理的解释就是 for 循环中 对于循环变量的操作 比如 i 的自增 也是同步操作
前面的操作中不明显,虽然前面只是缺了这个条件,但是仔细想一下前面的理论也成立( 只是 省略了 for 循环 中的 循环变量的操作)
我现在才意识到这个问题
所以只有在 await 将我们的 循环变量 i 的自增 进行阻塞 才能真正的 阻塞后面的操作
从而 实现 同步操作
1 function timer(i) { 2 return new Promise(function (resolve, reject) { 3 setTimeout(() => { 4 resolve(i) 5 }, Math.random() * 10000) 6 }) 7 } 8 testTimer(); 9 function testTimer() { 10 for (let i = 0; i < 5; i++) { 11 let num = await timer(i); 12 console.log(num); 13 } 14 }
参考文章
推荐大家读一下这些对我帮助很大的文章,我写的真的是很随意
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://segmentfault.com/a/1190000007535316
https://m.jb51.net/article/149358.htm
总结
终于写完了,原来是 for 循环 变量的问题
rlgl
最开始我就是认为 for 循环机制的问题,但是网上没人写 就写个立即执行函数包着...
当时人都傻了
然后疯狂查资料...
好了,今天休息了