我想在你身上,作春天在樱桃树身上作的事情。——巴勃罗·聂鲁达node
首先来看看promise的用法,从名字能够看出它是个构造函数,因此咱们得new它,获得一个Promise实例p,咱们打印p看看jquery
let p = new Promise
console.log(p) // TypeError: Promise resolver undefined is not a function
复制代码
报错信息告诉咱们,Promise须要一些参数,这里须要一个函数(咱们叫它执行器)做为参数,该函数有两个参数————resolve和reject,这两个参数也是函数(由js引擎提供),咱们能够在Promise内部调用,当异步操做成功时,调用resolve,不然reject。git
let p =new Promise(function(resolve, reject){
if(/* 异步操做成功 */){
resolve(data)
}else{
reject(err)
}
})
复制代码
如今咱们须要知道一个重要概念,Promise是有“状态”的,分别是pending(等待态)、fulfilled(成功态)、rejected(失败态),pending能够转换为fulfilled或rejected,但fulfilled和rejected不可相互转化。程序员
resolve方法能够将pending转为fulfilled,reject方法能够将pending转为rejected。github
经过给Promise示例上的then方法传递两个函数做为参数,能够提供改变状态时的回调,第一个函数是成功的回调,第二个则是失败的回调。ajax
p.then(function(data){ // resolve方法会将参数传进成功的回调
console.log(data)
}, function(err){ // reject方法会将失败的信息传进失败的回调
console.log(err)
})
复制代码
let p = new Promise(function(resolve, reject){
setTimeout(function(){
let num = Math.random()
if (num > 0.5) {
resolve(num)
}else{
reject(num)
}
}, 1000)
})
p.then(function(num){
console.log('大于0.5的数字:', num)
},function(num){
console.log('小于等于0.5的数字', num)
})
// 运行第一次:小于等于0.5的数字 0.166162996031475
// 运行第二次:大于0.5的数字: 0.6591451548308984
...
复制代码
在Promise执行器中咱们进行了一次异步操做,并在咱们以为合适的时候调用成功或失败的回调函数,并拿到了想要的数据以进行下一步操做编程
除此以外,每个then方法都会返回一个新的Promise实例(不是原来那个),让then方法支持链式调用,并能够经过返回值将参数传递给下一个then数组
p.then(function(num){
return num
},function(num){
return num
}).then(function(num){
console.log('大于0.5的数字:', num)
},function(num){
console.log('小于等于0.5的数字', num)
})
复制代码
catch方法等同于.then(null, reject),能够直接指定失败的回调(支持接收上一个then发生的错误)promise
这多是个颇有用的方法,它能够统一处理多个Promise缓存
Promise.all能将多个Promise实例包装成一个Promise实例
let Promise1 = new Promise(function(resolve, reject){})
let Promise2 = new Promise(function(resolve, reject){})
let Promise3 = new Promise(function(resolve, reject){})
let p = Promise.all([Promise1, Promise2, Promise3])
p.then(funciton(){
// 三个都成功则成功
}, function(){
// 只要有失败,则失败
})
复制代码
这个组合后的Promise实例和普通实例同样,有三种状态,这里有组成它的几个小Promise的状态决定 :
一、当Promise1, Promise2, Promise3的状态都为成功态,则p为成功态; 二、当Promise1, Promise2, Promise3中有任意一个为失败态,则p为失败态;
与all方法相似,也能够讲多个Promise实例包装成一个新的Promise实例
不一样的是,all时大Promise的状态由多个小Promise共同决定,而race时由第一个转变状态的小Promise的状态决定,第一个是成功态,则转成功态,第一个失败态,则转失败态
能够生成一个成功的Promise
Promise.resolve('成功')等同于new Promise(function(resolve){resolve('成功')})
能够生成一个失败的Promise
Promise.reject('出错了')等同于new Promise((resolve, reject) => reject('出错了'))
以jquery的ajax为例(@1.5.0版本之前,后来jquery也引入了Promise的概念),看看从前咱们是如何解决异步问题的。
$.get('url', {data: data}, function(result){
console.log('成功', result)// 成功的回调,result为异步拿到的数据
});
复制代码
看起来还能够?
想象一个场景,当咱们须要发送多个异步请求,而请求之间相互关联相互依赖,没有请求1就不会有请求2,没有请求2就不会有请求3........
这时咱们须要这样写
$.get('url', {data: data}, function(result1){
$.get('url', {data: result1}, function(result2){
$.get('url', {data: result2}, function(result3){
$.get('url', {data: result3}, function(result4){
......
$.get('url', {data: resultn}, function(resultn+1){
console.log('成功')
}
}
}
}
});
复制代码
这样的话,咱们就掉入了传说中的回调地狱,万劫不复,不能自拔。
这种代码,难以维护和调试,一旦出现bug,牵一发而动全身。
下面咱们看看Promise是如何解决的,咱们以node中的fs访问文件举例
先建立三个相互依赖的txt文件
1.txt的内容:
2.txt
复制代码
2.txt的内容:
3.txt
复制代码
3.txt的内容:
完成
复制代码
js代码:
let readFile = require('fs').readFile; // 加载node内置模块fs 利用readFile方法异步访问文件
function getFile(url){ // 建立一个读取文件方法
return new Promise(function(resolve, reject){ // 返回一个Promise对象
readFile(url, 'utf8', function(err,data){ // 读取文件
resolve(data) // 调用成功的方法
})
})
}
getFile('1.txt').then(function(data){ // then方法进行链式调用
console.log(data) // 2.txt
return getFile(data) //拿到了第一次的内容用来请求第二次
}).then(function(data){
console.log(data) // 3.txt
return getFile(data) //拿到了第二次的内容用来请求第三次
}).then(function(data){
console.log(data) // 完成
})
复制代码
(这里咱们先没必要搞懂代码,下面会介绍具体用法)
看起来多了几行代码[尴尬],但咱们经过建立一个读取函数返回一个Promise对象,再利用Promise自带的.then方法,将嵌套的异步代码弄得看起来像同步同样,这样的话,出现问题能够轻易的调试和修改。
接下来是本文的重头戏,根据PromiseA+(Promise的官方标准)动手实现一个180行左右代码的promise,功能可实现多数(then catch all race resolve reject),这里会将的比较详细,一步一步理清思路。
根据使用方法咱们能够知道,Promise是一个须要接受一个执行器的构造函数,执行器提供两个方法,内部有状态机制,原型链上有then方法。
开始撸:
// myPromise
function Promise(executor){ //executor是一个执行器(函数)
let _this = this // 先缓存this以避免后面指针混乱
_this.status = 'pending' // 默认状态为等待态
_this.value = undefined // 成功时要传递给成功回调的数据,默认undefined
_this.reason = undefined // 失败时要传递给失败回调的缘由,默认undefined
function resolve(value) { // 内置一个resolve方法,接收成功状态数据
// 上面说了,只有pending能够转为其余状态,因此这里要判断一下
if (_this.status === 'pending') {
_this.status = 'resolved' // 当调用resolve时要将状态改成成功态
_this.value = value // 保存成功时传进来的数据
}
}
function reject(reason) { // 内置一个reject方法,失败状态时接收缘由
if (_this.status === 'pending') { // 和resolve同理
_this.status = 'rejected' // 转为失败态
_this.reason = reason // 保存失败缘由
}
}
executor(resolve, reject) // 执行执行器函数,并将两个方法传入
}
// then方法接收两个参数,分别是成功和失败的回调,这里咱们命名为onFulfilled和onRjected
Promise.prototype.then = function(onFulfilled, onRjected){
let _this = this; // 依然缓存this
if(_this.status === 'resolved'){ // 判断当前Promise的状态
onFulfilled(_this.value) // 若是是成功态,固然是要执行用户传递的成功回调,并把数据传进去
}
if(_this.status === 'rejected'){ // 同理
onRjected(_this.reason)
}
}
module.exports = Promise // 导出模块,不然别的文件无法使用
复制代码
注意:上面代码的命名不是随便起的,像onFulfilled和onRjected,是严格按照Promise/A+规范走的,不信你看图
这样咱们就实现了第一步,能够建立Promise实例并使用then方法了,测试一下
let Promise = require('./myPromise') // 引入模块
let p = new Promise(function(resolve, reject){
resolve('test')
})
p.then(function(data){
console.log('成功', data)
},function(err){
console.log('失败', err)
})
// 成功 test
复制代码
再试试reject
let Promise = require('./myPromise') // 引入模块
let p = new Promise(function(resolve, reject){
reject('test')
})
p.then(function(data){
console.log('成功', data)
},function(err){
console.log('失败', err)
})
// 失败 test
复制代码
看起来不错,但回调函数是当即执行的,没法进行异步操做,好比这样是不行的
let p = new Promise(function(resolve, reject){
setTimeout(function(){
resolve(100)
}, 1000)
})
p.then(function(data){
console.log('成功', data)
},function(err){
console.log('失败', err)
})
// 不会输出任何代码
复制代码
缘由是咱们在then函数中只对成功态和失败态进行了判断,而实例被new时,执行器中的代码会当即执行,但setTimeout中的代码将稍后执行,也就是说,then方法执行时,Promise的状态没有被改变依然是pending态,因此咱们要对pending态也作判断,而因为代码多是异步的,那么咱们就要想办法把回调函数进行缓存,而且,then方法是能够屡次使用的,因此要能存多个回调,那么这里咱们用一个数组。
在实例上挂两个参数
_this.onResolvedCallbacks = []; // 存放then成功的回调
_this.onRejectedCallbacks = []; // 存放then失败的回调
复制代码
then方法加一个pending时的判断
if(_this.status === 'pending'){
// 每一次then时,若是是等待态,就把回调函数push进数组中,何时改变状态何时再执行
_this.onResolvedCallbacks.push(function(){ // 这里用一个函数包起来,是为了后面加入新的逻辑进去
onFulfilled(_this.value)
})
_this.onRejectedCallbacks.push(function(){ // 同理
onRjected(_this.reason)
})
}
复制代码
下一步要分别在resolve和reject方法里加入执行数组中存放的函数的方法,修改一下上面的resolve和reject方法
function resolve(value) {
if (_this.status === 'pending') {
_this.status = 'resolved'
_this.value = value
_this.onResolvedCallbacks.forEach(function(fn){ // 当成功的函数被调用时,以前缓存的回调函数会被一一调用
fn()
})
}
}
function reject(reason) {
if (_this.status === 'pending') {
_this.status = 'rejected'
_this.reason = reason
_this.onRejectedCallbacks.forEach(function(fn){// 当失败的函数被调用时,以前缓存的回调函数会被一一调用
fn()
})
}
}
复制代码
如今能够执行异步任务了,也能够屡次then了,一个穷人版Promise就完成了,
上面的代码虽然能用,但经不起考验,真正的Promise若是在实例中抛出错误,应该走reject:
new Promise(function(resolve, reject){
throw new Error('错误')
}).then(function(){
},function(err){
console.log('错误:', err)
})
// 错误: Error: 错误
复制代码
咱们实现一下,思路很简单,在执行器执行时进行try catch
try{
executor(resolve, reject)
}catch(e){ // 若是捕获发生异常,直接调失败,并把参数穿进去
reject(e)
}
复制代码
上面说过了,then能够链式调用,也是这一点让Promise十分好用,固然这部分源码也比较复杂
咱们知道jquery实现链式调用是return了一个this,但Promise不行,为何不行?
正宗的Promise是这样的套路:
let p1 = new Promise(function(resolve, reject){
resolve()
})
let p2 = p1.then(function(data){ //这是p1的成功回调,此时p1是成功态
throw new Error('错误') // 若是这里抛出错误,p2应是失败态
})
p2.then(function(){
},function(err){
console.log(err)
})
// Error: 错误
复制代码
若是返回的是this,那么p2跟p1相同,固状态也相同,但上面说了,Promise的成功态和失败态不能相互转换,那就不会获得p1成功而p2失败的效果,而其实是可能发生这种状况的。
因此Promise的then方法实现链式调用的原理是:返回一个新的Promise
在then方法中先定义一个新的Promise,取名为promise2(官方规定的),而后在三种状态下分别用promise2包装一下,在调用onFulfilled时用一个变量x(规定的)接收返回值,trycatch一下代码,没错就调resolve传入x,有错就调reject传入错误,最后再把promise2给return出去,就能够进行链式调用了,,,,可是!
// 改动then
let promise2;
if (_this.status === 'resolved') {
promise2 = new Promise(function (resolve, reject) {
// 能够凑合用,可是是有不少问题的
try {
let x = onFulfilled(_this.value)
resolve(x)
} catch (e) {
reject(e)
}
})
}
if (_this.status === 'rejected') {
promise2 = new Promise(function (resolve, reject) {
// 能够凑合用,可是是有不少问题的
try {
let x = onRjected(_this.reason)
resolve(x)
} catch (e) {
reject(e)
}
})
}
if(_this.status === 'pending'){
promise2 = new Promise(function (resolve, rejec
_this.onResolvedCallbacks.push(function(){
// 能够凑合用,可是是有不少问题的
try {
let x = onFulfilled(_this.value)
resolve(x)
} catch (e) {
reject(e)
}
})
_this.onRejectedCallbacks.push(function(){
// 能够凑合用,可是是有不少问题的
try {
let x = onRjected(_this.reason)
resolve(x)
} catch (e) {
reject(e)
}
})
})
}
return promise2
复制代码
这里我先解释一下x的做用再说为何不行,x是用来接收上一次then的返回值,好比这样
let p = new Promise(function(resolve, reject){
resolve(data)
})
p.then(function(data){
return xxx // 这里返回一个值
}, function(){
}).then(function(data){
console.log // 这里会接收到xxx
}, function(){
})
// 以上代码中第一次then的返回值就是源码内第一次调用onRjected的返回值,能够用一个x来接收
复制代码
接下来讲问题,上面这样看起来是符合逻辑的,而且也确实能够链式调用并接受到,但咱们在写库,库就要经得起考验,把容错性提到最高,要接受使用者各类新(cao)奇(dan)操做,所谓有容nai大。可能性以下:
一、前一次then返回一个普通值,字符串数组对象这些东西,都没问题,只需传给下一个then,刚才的方法就够用。
二、前一次then返回的是一个Promise,是正常的操做,也是Promise提供的语法糖,咱们要想办法判断到底返回的是啥。
三、前一次then返回的是一个Promise,其中有异步操做,也是理所固然的,那咱们就要等待他的状态改变,再进行下面的处理。
四、前一次then返回的是本身自己这个Promise
var p1 = p.then(function(){// 这里得用var,let因为做用域的缘由会报错undefined
return p1
})
复制代码
五、前一次then返回的是一个别人本身随便写的Promise,这个Promise多是个有then的普通对象,好比{then:'哈哈哈'},也有可能在then里故意抛错(这种蛋疼的操做咱们也要考虑进去)。好比他这样写
let promise = {}
Object.defineProperty(promise,'then',{
value: function(){
throw new Error('报错气死你')
}
})
// 若是返回这东西,咱们再去调then方法就确定会报错了
复制代码
六、调resolve的时候再传一个Promise下去,咱们还得处理这个Promise。
p.then(function(data) {
return new Promise(function(resolve, reject) {
resolve(new Promise(function(resolve,reject){
resolve(1111)
}))
})
})
复制代码
七、可能既调resolve又调reject,得忽略后一个。
八、光then,里面啥也不写。
。。
稍等,我先吐一会。。。
好了我们调整心情继续撸,其实这一系列的问题,不少都是相关的,只要根据规范,均可以顺利解决,接上面的代码,先干三件事
一、问题7是最好解决的,若是没传resolve和reject,咱们就给他一个。
二、官方规范规定了一件事
简单说就是为免在测试中出问题onFulfilled和onRejected要异步执行,咱们就让他异步执行
三、问题1-7,咱们能够采起统一的以为方案,定义一个函数来判断和处理这一系列的状况,官方给出了一个叫作resolvePromise的函数
再看then方法
Promise.prototype.then = function (onFulfilled, onRjected) {
//成功和失败默认不传给一个函数,解决了问题8
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
return value;
}
onRjected = typeof onRjected === 'function' ? onRjected : function (err) {
throw err;
}
let _this = this;
let promise2; //返回的promise
if (_this.status === 'resolved') {
promise2 = new Promise(function (resolve, reject) {
// 当成功或者失败执行时有异常那么返回的promise应该处于失败状态
setTimeout(function () {// 根据规范让那俩家伙异步执行
try {
let x = onFulfilled(_this.value);//这里解释过了
// 写一个方法统一处理问题1-7
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
})
}
if (_this.status === 'rejected') {
promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try {
let x = onRjected(_this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
})
}
if (_this.status === 'pending') {
promise2 = new Promise(function (resolve, reject) {
_this.onResolvedCallbacks.push(function () {
setTimeout(function () {
try {
let x = onFulfilled(_this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
})
});
_this.onRejectedCallbacks.push(function () {
setTimeout(function () {
try {
let x = onRjected(_this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
});
})
}
return promise2;
}
复制代码
接下来看看resolvePromise该怎么写
function resolvePromise(promise2, x, resolve, reject) {
// 接受四个参数,新Promise、返回值,成功和失败的回调
// 有可能这里返回的x是别人的promise
// 尽量容许其余乱写
if (promise2 === x) { //这里应该报一个类型错误,来解决问题4
return reject(new TypeError('循环引用了'))
}
// 看x是否是一个promise,promise应该是一个对象
let called; // 表示是否调用过成功或者失败,用来解决问题7
//下面判断上一次then返回的是普通值仍是函数,来解决问题一、2
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 多是promise {},看这个对象中是否有then方法,若是有then我就认为他是promise了
try {
let then = x.then;// 保存一下x的then方法
if (typeof then === 'function') {
// 成功
//这里的y也是官方规范,若是仍是promise,能够当下一次的x使用
//用call方法修改指针为x,不然this指向window
then.call(x, function (y) {
if (called) return //若是调用过就return掉
called = true
// y可能仍是一个promise,在去解析直到返回的是一个普通值
resolvePromise(promise2, y, resolve, reject)//递归调用,解决了问题6
}, function (err) { //失败
if (called) return
called = true
reject(err);
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true;
reject(e);
}
} else { // 说明是一个普通值1
resolve(x); // 表示成功了
}
}
复制代码
PromiseA+提供了测试库promises-aplus-tests,github上明确讲解了使用方法 公开一个适配器接口:
Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise(function (resolve, reject) {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd
}
复制代码
用命令行: promises-aplus-tests myPromise.js
通过一系列测试获得结果
872 passing (18s)
复制代码
证实了咱们的promise是彻底符合规范的!
除了最重要的then方法,Promise还有不少方法,但都不难,这里一次性介绍一遍
// 捕获错误的方法,在原型上有catch方法,返回一个没有resolve的then结果便可
Promise.prototype.catch = function (callback) {
return this.then(null, callback)
}
// 解析所有方法,接收一个Promise数组promises,返回新的Promise,遍历数组,都完成再resolve
Promise.all = function (promises) {
//promises是一个promise的数组
return new Promise(function (resolve, reject) {
let arr = []; //arr是最终返回值的结果
let i = 0; // 表示成功了多少次
function processData(index, y) {
arr[index] = y;
if (++i === promises.length) {
resolve(arr);
}
}
for (let i = 0; i < promises.length; i++) {
promises[i].then(function (y) {
processData(i, y)
}, reject)
}
})
}
// 只要有一个promise成功了 就算成功。若是第一个失败了就失败了
Promise.race = function (promises) {
return new Promise(function (resolve, reject) {
for (var i = 0; i < promises.length; i++) {
promises[i].then(resolve,reject)
}
})
}
// 生成一个成功的promise
Promise.resolve = function(value){
return new Promise(function(resolve,reject){
resolve(value);
})
}
// 生成一个失败的promise
Promise.reject = function(reason){
return new Promise(function(resolve,reject){
reject(reason);
})
}
复制代码
结语:Promise是异步的较好的解决方案之一,经过对源码的解析,对Promise甚至js异步都有了深入的理解。Promise已经诞生好久了,若是你还不了解它,那你已经很落后了,抓紧时间上车。程序世界一日千里,做为程序员,要主动拥抱变化。
欢迎加个人我的微信深刻交流