第一次接触到Promise这个东西,是2012年微软发布Windows8操做系统后抱着做死好奇的心态研究用html5写Metro应用的时候。当时配合html5提供的WinJS库里面的异步接口全都是Promise形式,这对那时候刚刚毕业一点javascript基础都没有的我而言简直就是天书。我当时想的是,微软又在脑洞大开的瞎捣鼓了。javascript
结果没想到,到了2015年,Promise竟然写进ES6标准里面了。并且一项调查显示,js程序员们用这玩意用的还挺high。html
讽刺的是,做为早在2012年就在Metro应用开发接口里面普遍使用Promise的微软,其自家浏览器IE直到2015年寿终正寝了都还不支持Promise,看来微软不是没有这个技术,而是真的对IE放弃治疗了。。。html5
如今回想起来,当时看到Promise最头疼的,就是初学者看起来匪夷所思,也是最被js程序员广为称道的特性:then
函数调用链。java
then
函数调用链,从其本质上而言,就是对多个异步过程的依次调用,本文就从这一点着手,对Promise这一特性进行研究和学习。node
考虑以下场景,函数延时2秒以后打印一行日志,再延时3秒打印一行日志,再延时4秒打印一行日志,这在其余的编程语言当中是很是简单的事情,可是到了js里面就比较费劲,代码大约会写成下面的样子:程序员
var myfunc = function() {
setTimeout(function() {
console.log("log1");
setTimeout(function() {
console.log("log2");
setTimeout(function() {
console.log("log3");
}, 4000);
}, 3000);
}, 2000);
}
复制代码
因为嵌套了多层回调结构,这里造成了一个典型的金字塔结构。若是业务逻辑再复杂一些,就会变成使人闻风丧胆的回调地狱。编程
若是意识比较好,知道提炼出简单的函数,那么代码差很少是这个样子:promise
var func1 = function() {
setTimeout(func2, 2000);
};
var func2 = function() {
console.log("log1");
setTimeout(func3, 3000);
};
var func3 = function() {
console.log("log2");
setTimeout(func4, 4000);
};
var func4 = function() {
console.log("log3");
};
复制代码
这样看起来稍微好一点了,可是总以为有点怪怪的。。。好吧,其实我js水平有限,说不上来为何这样写很差。若是你知道为何这样写不太好因此发明了Promise,请告诉我。浏览器
如今让咱们言归正传,说说Promise这个东西。bash
这里请容许我引用MDN对Promise的描述:
Promise 对象用于延迟(deferred) 计算和异步(asynchronous ) 计算.。一个Promise对象表明着一个还未完成,但预期未来会完成的操做。
Promise 对象是一个返回值的代理,这个返回值在promise对象建立时未必已知。它容许你为异步操做的成功或失败指定处理方法。 这使得异步方法能够像同步方法那样返回值:异步方法会返回一个包含了原返回值的 promise 对象来替代原返回值。
Promise对象有如下几种状态:
pending状态的promise对象既可转换为带着一个成功值的fulfilled 状态,也可变为带着一个失败信息的 rejected 状态。当状态发生转换时,promise.then绑定的方法(函数句柄)就会被调用。(当绑定方法时,若是 promise对象已经处于 fulfilled 或 rejected 状态,那么相应的方法将会被马上调用, 因此在异步操做的完成状况和它的绑定方法之间不存在竞争条件。)
更多关于Promise的描述和示例能够参考MDN的Promise条目,或者MSDN的Promise条目。
基于以上对Promise的了解,咱们知道可使用它来解决多层回调嵌套后的代码蠢笨难以维护的问题。关于Promise的语法和参数上面给出的两个连接已经说的很清楚了,这里不重复,直接上代码。
咱们先来尝试一个比较简单的状况,只执行一次延时和回调:
new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout");
setTimeout(res, 2000);
}).then(function() {
console.log(Date.now() + " timeout call back");
});
复制代码
看起来和MSDN里的示例也没什么区别,执行结果以下:
$ node promisTest.js
1450194136374 start setTimeout
1450194138391 timeout call back
复制代码
那么若是咱们要再作一个延时呢,那么我能够这样写:
new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout 1");
setTimeout(res, 2000);
}).then(function() {
console.log(Date.now() + " timeout 1 call back");
new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout 2");
setTimeout(res, 3000);
}).then(function() {
console.log(Date.now() + " timeout 2 call back");
})
});
复制代码
彷佛也能正确运行:
$ node promisTest.js
1450194338710 start setTimeout 1
1450194340720 timeout 1 call back
1450194340720 start setTimeout 2
1450194343722 timeout 2 call back
复制代码
不过代码看起来蠢萌蠢萌的是否是,并且隐约又在搭金字塔了。这和引入Promise的目的背道而驰。
那么问题出在哪呢?正确的姿式又是怎样的?
答案藏在then
函数以及then
函数的onFulfilled
(或者叫onCompleted
)回调函数的返回值里面。
首先明确的一点是,then
函数会返回一个新的Promise变量,你能够再次调用这个新的Promise变量的then
函数,像这样:
new Promise(...).then(...)
.then(...).then(...).then(...)...
复制代码
而then
函数返回的是什么样的Promies,取决于onFulfilled
回调的返回值。
事实上,onFulfilled
能够返回一个普通的变量,也能够是另外一个Promise变量。
若是onFulfilled
返回的是一个普通的值,那么then
函数会返回一个默认的Promise变量。执行这个Promise的then
函数会使Promise当即被知足,执行onFulfilled
函数,而这个onFulfilled
的入参,便是上一个onFulfilled
的返回值。
而若是onFulfilled
返回的是一个Promise变量,那个这个Promise变量就会做为then
函数的返回值。
关于then
函数和onFulfilled
函数的返回值的这一系列设定,MDN和MSDN上的文档都没有明确的正面描述,至于ES6官方文档ECMAScript 2015 (6th Edition, ECMA-262)。。。个人水平有限实在看不懂,若是哪位高手能解释清楚官方文档里面对着两个返回值的描述,请必定留言指教!!!
因此以上为个人自由发挥,语言组织的有点拗口,上代码看一下你们就明白了。
首先是返回普通变量的状况:
new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout 1");
setTimeout(res, 2000);
}).then(function() {
console.log(Date.now() + " timeout 1 call back");
return 1024;
}).then(function(arg) {
console.log(Date.now() + " last onFulfilled return " + arg);
});
复制代码
以上代码执行结果为:
$ node promisTest.js
1450277122125 start setTimeout 1
1450277124129 timeout 1 call back
1450277124129 last onFulfilled return 1024
复制代码
有点意思对不对,但这不是关键。关键是onFulfilled
函数返回一个Promise变量可使咱们很方便的连续调用多个异步过程。好比咱们能够这样来尝试连续作两个延时操做:
new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout 1");
setTimeout(res, 2000);
}).then(function() {
console.log(Date.now() + " timeout 1 call back");
return new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout 2");
setTimeout(res, 3000);
});
}).then(function() {
console.log(Date.now() + " timeout 2 call back");
});
复制代码
执行结果以下:
$ node promisTest.js
1450277510275 start setTimeout 1
1450277512276 timeout 1 call back
1450277512276 start setTimeout 2
1450277515327 timeout 2 call back
复制代码
若是以为这也没什么了不得,那再多来几回也不在话下:
new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout 1");
setTimeout(res, 2000);
}).then(function() {
console.log(Date.now() + " timeout 1 call back");
return new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout 2");
setTimeout(res, 3000);
});
}).then(function() {
console.log(Date.now() + " timeout 2 call back");
return new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout 3");
setTimeout(res, 4000);
});
}).then(function() {
console.log(Date.now() + " timeout 3 call back");
return new Promise(function(res, rej) {
console.log(Date.now() + " start setTimeout 4");
setTimeout(res, 5000);
});
}).then(function() {
console.log(Date.now() + " timeout 4 call back");
});
复制代码
$ node promisTest.js
1450277902714 start setTimeout 1
1450277904722 timeout 1 call back
1450277904724 start setTimeout 2
1450277907725 timeout 2 call back
1450277907725 start setTimeout 3
1450277911730 timeout 3 call back
1450277911730 start setTimeout 4
1450277916744 timeout 4 call back
复制代码
能够看到,多个延时的回调函数被有序的排列下来,并无出现喜闻乐见的金字塔状结构。虽然代码里面调用的都是异步过程,可是看起来就像是所有由同步过程构成的同样。这就是Promise带给咱们的好处。
若是你有把啰嗦的代码提炼成单独函数的好习惯,那就更加画美不看了:
function timeout1() {
return new Promise(function(res, rej) {
console.log(Date.now() + " start timeout1");
setTimeout(res, 2000);
});
}
function timeout2() {
return new Promise(function(res, rej) {
console.log(Date.now() + " start timeout2");
setTimeout(res, 3000);
});
}
function timeout3() {
return new Promise(function(res, rej) {
console.log(Date.now() + " start timeout3");
setTimeout(res, 4000);
});
}
function timeout4() {
return new Promise(function(res, rej) {
console.log(Date.now() + " start timeout4");
setTimeout(res, 5000);
});
}
timeout1()
.then(timeout2)
.then(timeout3)
.then(timeout4)
.then(function() {
console.log(Date.now() + " timout4 callback");
});
复制代码
$ node promisTest.js
1450278983342 start timeout1
1450278985343 start timeout2
1450278988351 start timeout3
1450278992356 start timeout4
1450278997370 timout4 callback
复制代码
接下来咱们能够再继续研究一下onFulfilled
函数传入入参的问题。
咱们已经知道,若是上一个onFulfilled
函数返回了一个普通的值,那么这个值为做为这个onFulfilled
函数的入参;那么若是上一个onFulfilled
返回了一个Promise变量,这个onFulfilled
的入参又来自哪里?
答案是,这个onFulfilled
函数的入参,是上一个Promise中调用resolve
函数时传入的值。
跳跃的有点大一时间没法接受对不对,让咱们来好好缕一缕。
首先,Promise.resolve这个函数是什么,用MDN上面文邹邹的说法
用成功值value解决一个Promise对象。若是该value为可继续的(thenable,即带有then方法),返回的Promise对象会“跟随”这个value,采用这个value的最终状态;不然的话返回值会用这个value知足(fullfil)返回的Promise对象。
复制代码
简而言之,这就是异步调用成功状况下的回调。
咱们来看看普通的异步接口中,成功状况的回调是什么样的,就拿nodejs的上的fs.readFile(file[, options], callback)
来讲,它的典型调用例子以下
fs.readFile('/etc/passwd', function (err, data) {
if (err) throw err;
console.log(data);
});
复制代码
由于对于fs.readFile
这个函数而言,不管成功仍是失败,它都会调用callback这个回调函数,因此这个回调接受两个入参,即失败时的异常描述err
和成功时的返回结果data
。
那么假如咱们用Promise来重构这个读取文件的例子,咱们应该怎么写呢?
首先是封装fs.readFile
函数:
function readFile(fileName) {
return new Promise(function(resolve, reject) {
fs.readFile(fileName, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
复制代码
其次是调用:
readFile('theFile.txt').then(
function(data) {
console.log(data);
},
function(err) {
throw err;
}
);
复制代码
想象一下,在其余语言的读取文件的同步调用接口的里面,文件的内容一般是放在哪里?函数返回值对不对!答案出来了,这个resolve
的入参是什么?就是异步调用成功状况下的返回值。
有了这个概念以后,咱们就不难理解“onFulfilled
函数的入参,是上一个Promise中调用resolve
函数时传入的值”这件事了。由于onFulfilled
的任务,就是对上一个异步调用成功后的结果作处理的。
哎终于理顺了。。。
下面请容许我用一段代码对本文讲解到的要点进行总结:
function callp1() {
console.log(Date.now() + " start callp1");
return new Promise(function(res, rej) {
setTimeout(res, 2000);
});
}
function callp2() {
console.log(Date.now() + " start callp2");
return new Promise(function(res, rej) {
setTimeout(function() {
res({arg1: 4, arg2: "arg2 value"});
}, 3000);
});
}
function callp3(arg) {
console.log(Date.now() + " start callp3 with arg = " + arg);
return new Promise(function(res, rej) {
setTimeout(function() {
res("callp3");
}, arg * 1000);
});
}
callp1().then(function() {
console.log(Date.now() + " callp1 return");
return callp2();
}).then(function(ret) {
console.log(Date.now() + " callp2 return with ret value = " + JSON.stringify(ret));
return callp3(ret.arg1);
}).then(function(ret) {
console.log(Date.now() + " callp3 return with ret value = " + ret);
})
复制代码
$ node promisTest.js
1450191479575 start callp1
1450191481597 callp1 return
1450191481599 start callp2
1450191484605 callp2 return with ret value = {"arg1":4,"arg2":"arg2 value"}
1450191484605 start callp3 with arg = 4
1450191488610 callp3 return with ret value = callp3
复制代码