JavaScript深入浅出异步编程-promise原理
其实Promise
本身并不具备异步
的能力,而之所以这里需要单独开一篇说明其原理,是因为Promise
在异步编程
的过程中是一个不可或缺的一环。原因下面细说。
在说promise之前,有必要先说下JS中的回调方式。比如下面:
function doSomethingAfterTime(time, something) { setTimeout(fun, time); }
但是这样的回调方式有一个问题,可读性太差。另外当回调的层次多了以后,容易陷入回调地狱。举个例子:
function func1(cb){ // do something cb(); } function func2(cb){ // do something cb(); } function func3(cb){ // do something cb(); } // do func1(function(){ func2(function(){ func3(function(){ }); }); });
这样的代码读起来简直就是折磨,晕死!
下面试着改进下代码,试着将回调函数封装起来。顺便剖析下promise的原理。
Promise的原理
先来一个最简单的。
function Promise(something){ var callback = null; this.then = function(onCompelete){ callback = onCompelete; }; something(function (value){ callback(value); }); }
下面是调用代码。
// 事件1 function func1(){ return new Promise(function(resolve){ // do something setTimeout(function(){ console.log('func1'); resolve(); },1000); }); } func1().then(function(){ console.log('func1 complete'); });
上面对Promise
的封装算是最简单的版本,只是模拟了Promise
的调用方法,比如then
,还有Promise的构造函数
。但是这样的封装无法实现链式调用,链式调用
的核心就是当调用某个方法的时候返回该对象本身
或者该对象对应class的全新对象
。而对于Promise
的改造也很简单.
then
方法返回Promise
本身Promoise
需要支持多个callback
。
function Promise(something){ var callbacks = []; this.then = function(onCompelete){ callbacks.push(onCompelete); return this; }; function resolve(value){ callbacks.forEach(function(cb){ cb(value); }); } something(resolve); }
调用代码如下:
func1().then(function(){ console.log('func1 complete'); }).then(function(){ console.log('then2'); }).then(function(){ console.log('then3'); });
现在的Promise
执行上面的代码后能够得到正确的执行结果,但是有一个问题,如果我们想在then
方法再调用一个返回promise
的方法?比如这样:
// 事件2 function func2(){ return new Promise(function(resolve){ // do something setTimeout(function(){ console.log('func2'); resolve(); },1000); }); } func1().then(func2).then(function(){ console.log('all complete'); });
输出如下:
func1 all complete func2
你会发现虽然func2
成功调用了,但是输出顺序乱了,我们期望的正确输出顺序应该是:
func1 func2 all complete
分析下问题出在哪里?问题就出在Promise
中的callbacks
,第一个then
是在func1
返回的Promise
上调用的,而第二个then
事实上还是在func1
返回的Promise
上调用的。然而我们希望的是,第二个then
应该是在func2
返回的Promise
调用,这时候就需要考虑如何进一步改造Promise
了。
对于then
传入的onCompelete
函数参数,它是不知道这个函数具体是否会返回Promise
,只有调用了onCompelete
方法才能知道具体返回的数据。但是onCompelete
是回调函数,你无法直接在then
中调用。因此需要考虑其他的方式。
如果then
方法里面返回一个新的Promise
对象呢?用这个新的Promise
作为中间代理,比如这样:
function Promise(something){ var callbacks = []; this.then = function(onCompelete){ return new Promise(function (resolve) { callbacks.push({ onCompelete: onCompelete, resolve: resolve }); }); }; function resolve(value){ callbacks.forEach(function(cb){ var ret = cb.onCompelete(value); cb.resolve(ret); }) } something(resolve); }
但是运行的时候你会发现输出顺序还是没变,还是有问题的。那么继续分析问题出在哪里?
通过调试发现,resolve
传入的value
有可能是Promise
对象,而我们已经在then
方法里面返回了新的promise对象了
,交由该对象作为代理了。因此resolve
传入的value
如果是Promise
对象的话,那么就需要把当前Promise
的resolve
处理权交出去,交给传入的Promise
对象。相当于代理人
把权力交还给实际应该处理的对象。可能有点绕,我再详细的描述下
func1
返回的Promise
为p1
,then
返回的Promise
为p2
,resolve
传入的Promise
对象为p3
,func2
返回的Promise
对象为p4
。上面一共提到4个
Promise
对象。
说下描说下调用顺序。
首先由func1
创建p1
,然后调用then
方法创建了p2
,然后再次调用了then
方法,由p2
创建了p3
。p2
和p3
都是由then
创建的代理人。
这时候func1中的异步代码执行了,1秒过后由func1
调用了p1
的resolve
方法,并且将callbacks
数组内的方法依次调用,然后由cb.onCompelete(value)
方法间接得到func2
返回的p4
,接着调用p2
的resolve
方法将p4
传入。但是上面说了,p2
只是个代理,应该把权力
交还给p4
来执行。这样p4
得到权力--回调函数
,当func2
的异步代码执行完毕后,由p4
来执行回调函数。
因此resolve
方法需要进行如下改造。
function resolve(value) { // 交还权力,并且把resolve传过去 if (value && (typeof value.then === 'function')) { value.then.call(value, resolve); return; } callbacks.forEach(function (cb) { var ret = cb.onCompelete(value); cb.resolve(ret); }); }
上面的代码就是交权
的代码。这样完全的Promise
修改如下:
function Promise(something) { var callbacks = []; this.then = function (onCompelete) { return new Promise(function (resolve) { callbacks.push({ onCompelete: onCompelete, resolve: resolve }); }); }; function resolve(value) { if (value && (typeof value.then === 'function')) { value.then.call(value, resolve); return; } callbacks.forEach(function (cb) { var ret = cb.onCompelete(value); cb.resolve(ret); }); } something(resolve); }
这样修改过后,再执行如下代码:
func1().then(func2).then(function () { console.log('all complete'); });
现在就能得到正确的执行结果了。
至此,一个简单的Promise
定义完了。这时候有一个问题,如果调用then
方法之前resolve
已经被执行了怎么办呢,岂不是永远都得不到回调了?比如这样:
(new Promise(function (resolve) { resolve(); })).then(function(){ console.log('complete'); });
你会发现then
里面的回调就不会执行了。其实这时候只需要做一个小小的改动就行了。改造如下:
function Promise(something) { var callbacks = []; this.then = function (onCompelete) { return new Promise(function (resolve) { callbacks.push({ onCompelete: onCompelete, resolve: resolve }); }); }; function resolve(value) { if (value && (typeof value.then === 'function')) { value.then.call(value, resolve); return; } setTimeout(function(){ callbacks.forEach(function (cb) { var ret = cb.onCompelete(value); cb.resolve(ret); }); },0); } something(resolve); }
你会发现,这里只是在resolve
方法里面,将执行的回调放入setTimeout
中,并且timeout
设为0
。这里稍微说下原理
在第一篇中提到setTimeout
类似定时器
,JS内容在执行setTimeout
的回调函数的时候使用线程调度
的方式将回调函数调度到JS线程
执行。但凡涉及到线程调度
那么肯定需要等待JS线程空闲的时候才能调度过来。这时候将timeout设为0,相当于改变了代码执行顺序。
在实际的开发过程中,上面的Promise
代码还是缺少了一个功能,那就是状态管理
,比如:pending
、fulfilled
、rejected
。下面的代码继续加入状态管理
的代码,先添加pending
和fulfilled
的状态:
function Promise(something) { var callbacks = []; var state = 0;//0:pending,1:fulfilled var resultValue = null; this.then = function (onCompelete) { return new Promise(function (resolve) { handleCallBack({ onCompelete: onCompelete, resolve: resolve }); }); }; function handleCallBack(callback){ switch(state){ case 0:{ callbacks.push(callback); break; } case 1:{ var ret = callback.onCompelete(resultValue); callback.resolve(ret); break; } default:{ break; } } } function resolve(value) { if (value && (typeof value.then === 'function')) { value.then.call(value, resolve); return; } state = 1; resultValue = value; setTimeout(function(){ callbacks.forEach(function (cb) { handleCallBack(cb); }); },0); } something(resolve); }
下面再继续加入reject
功能。
function Promise(something) { var callbacks = []; var state = 0;//0:pending,1:fulfilled 2:reject var resultValue = null; this.then = function (onCompelete, onReject) { return new Promise(function (resolve) { handleCallBack({ onCompelete: onCompelete, resolve: resolve, reject: onReject }); }); }; function handleCallBack(callback) { switch (state) { case 0: { callbacks.push(callback); break; } case 1: { var ret = callback.onCompelete(resultValue); callback.resolve(ret); break; } case 2: { if(callback.reject){ var ret = callback.reject(resultValue); } callback.resolve(ret); break; } default: { break; } } } function reject(error) { state = 2; resultValue = error; setTimeout(function () { callbacks.forEach(function (cb) { handleCallBack(cb); }); }, 0); } function resolve(value) { if (value && (typeof value.then === 'function')) { value.then.call(value, resolve); return; } state = 1; resultValue = value; setTimeout(function () { callbacks.forEach(function (cb) { handleCallBack(cb); }); }, 0); } something(resolve,reject); }
OK,通过上面一步一步对Promise
进行修改,基本上是把Promise
的功能完善了。
从这个上面一步一步剖析Promise
原理的过程中,我们发现,Promise
本身并不提供异步
功能,Promise
只是对函数的回调功能进行了封装,甚至可以理解为Promise
就是一个回调代理。但是正是有了这个回调代理,使得我们的回调方式发生了彻底的改变,甚至直接影响了项目的架构设计。而在平时的开发过程中,Promise
在异步编程中起到了几乎不可替代的作用。