做项目碰到这么一个问题:
在for循环里发promise请求,then里返回的结果在chrome下是正确的, 在IE11(没错还在兼容IE11哟)下返回的结果是不正确的。IE又搞什么幺蛾子呢, 排查之后发现是因为在IE上用了promise-polyfill的问题
便于说明和分析,我把这个问题简化成了一个demo: 点击这个 demo in jsfiddle查看, 没有梯子可以看这个demo in codepen
代码很简单:
<input placeholder="abcd"></input>
<button onclick="run(Promise)">for Each with native promise </button>
<button onclick="run(PromisePolyfill)">for Each with promise polyfill </button>
<div id="result"></div>
复制代码
let widget;
function run(PromiseC) {
let value = document.querySelector('input').value || 'abcd';
document.querySelector('#result').innerText = '';
value.split('').forEach((item) => {
async1(PromiseC, item).then(item => {
console.log('in resolve', item);
document.querySelector('#result').innerText += ' in resolve ' + item + '\n';
})
})
};
function async1(PromiseC, item) {
return new PromiseC((resolve, reject) => {
setTimeout(() => {
widget = item;
// console.log(widget);
resolve(item);
}, 100);
})
}
复制代码
-
demo里引入了promise-polyfill,用的是CDN-promise-polyfill@7的代码, 修改了最后导出的语句,将其挂到了window.PromisePolyfill中,方便模拟IE11的情况
window.PromisePolyfill = Promise; 复制代码
-
PromiseC这个参数是用于传入native的Promise和 PromisePolyfill
测试步骤
-
输入任意字符串, 按左边的
for Each with native promise
按钮,可以看到用原生Promise运行的结果: -
跟你们期望的一样,按照顺序输出了
t,e,s,t
-
按右边的
for Each with native promise
按钮,可以看到用promise-polyfill运行的结果: -
这次输出的结果是
t,t,t,t
, 连续输出了最后一个字母,(当然也可能是第一个,我断定是第一个是因为我试了其他的字符串 :), 这里看出这个例子不是很合适,但是换一个其他字符串就能看出来了)
--
给一点时间思考...
--
原因分析
细心的小伙伴应该发现了在then
里打印的是变量widget
, 说为什么不打印 item
, 打印item
不就对了吗?
在这个问题里,widget
是一个闭包变量,为了是让这个问题暴露得明显一些,如果改成打印item
, 结果确实会输出 t,e,s,t
, 但其实是 掩盖了这个问题。
我们来看一下这个修改后的 demo in jsfiddle, or demo in codepen
变更内容:
- 在
then
里打印的是变量item
- 在
async1
执行的setTimeout内部增加打印了widget
.
两次的结果如下:
-
native Promise 结果
-
polyfill Promise 结果
聪明的小伙伴看到这里就明白为什么上面一直输出 t,t,t,t
了, 因为对于promise-polyfill
, 在for
循环下then
的执行时序跟native
的Promise
不一样的, promise-polyfill
会把then
之后的放在所有循环结束后再一起执行, 如果此时打印的是闭包变量,在所有循环结束后该变量就已经变成了最后一个t
, 所以才会输出t,t,t,t
.
那么,promise-polyfill
为什么执行顺序跟原生不一样呢
这就是个坑啊! - ^ -
我们来看一下promise-polyfill
的代码, 代码很短的不要怕,我们需要去找一下在resolve
之后,为什么没有立马执行then
里的回调函数:
- 进入
resolve
函数后会执行finale
这个函数 - 在
finale
内部回去执行handle
, 传入self._deferreds
,deferreds
就是我们定义在then
里面的回调啦 handle
里面又调用了Promise._immediateFn
, 到目前为止看起来没问题,立即(immediate)就要执行我们的回调了,- 接下来是怎么样功败垂成的,看看
Promise._immediateFn
的代码:
Promise._immediateFn =
(typeof setImmediate === 'function' &&
function(fn) {
setImmediate(fn);
}) ||
function(fn) {
setTimeoutFunc(fn, 0);
};
复制代码
- 看到没有!说是立即执行,其实并没有啊!
- 这就是
setImmediate
和setTimeout
函数其实是加入了宏认为队列, 而promise
的then
是微任务队列,目前应该没有办法用polyfill的方式将任务加入微任务队列,因此才导致了这种不能完全兼容的问题。
复制代码