当你在用promise-polyfill的时候,你其实在用什么

1,545 阅读3分钟

做项目碰到这么一个问题:

在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的执行时序跟nativePromise不一样的, promise-polyfill会把then之后的放在所有循环结束后再一起执行, 如果此时打印的是闭包变量,在所有循环结束后该变量就已经变成了最后一个t, 所以才会输出t,t,t,t.

那么,promise-polyfill为什么执行顺序跟原生不一样呢

这就是个坑啊! - ^ -

我们来看一下promise-polyfill的代码, 代码很短的不要怕,我们需要去找一下在resolve之后,为什么没有立马执行then里的回调函数:

  • 进入resolve函数后会执行finale这个函数
  • finale内部回去执行handle, 传入 self._deferredsdeferreds就是我们定义在then里面的回调啦
  • handle里面又调用了 Promise._immediateFn, 到目前为止看起来没问题,立即(immediate)就要执行我们的回调了,
  • 接下来是怎么样功败垂成的,看看Promise._immediateFn的代码:

	Promise._immediateFn =
		(typeof setImmediate === 'function' &&
			function(fn) {
				setImmediate(fn);
			}) ||
		function(fn) {
			setTimeoutFunc(fn, 0);
		};
  • 看到没有!说是立即执行,其实并没有啊!
  • 这就是setImmediatesetTimeout函数其实是加入了宏认为队列, 而promisethen是微任务队列,目前应该没有办法用polyfill的方式将任务加入微任务队列,因此才导致了这种不能完全兼容的问题。