本人JS萌新一枚,最近在编写NodeJS服务器逻辑的时候遇到了大量异步并发、异步顺序逻辑的问题,于是终于学会了Promise的用法,因此记录下来与大家分享。

1 Promise的基础用法:

let prom = new Promise(function (resolve, reject) {
  resolve('resolve');
});
prom.then(function (data) {
  console.log(data);
},function () {
  console.log('error!');
});

输出

resolve

Promise内的函数有两个参数,resolve和reject,但是这两个函数绝对不是让用户定义的回调函数。而是Promise对象自动传递的方法。当调用resolve(param)函数时,当前Promise对象的状态由pending转变为resolved,同时将value传递给Promise.then()方法定义的resolve函数。同样,调用reject(param)会将状态由pending转变为rejected,并将param传递给then()方法定义的的reject函数。

也可以理解为Promise.then()方法中的两个函数是实参,new Promise()中的resolve和reject是形参,这种“先调用函数,再定义函数”的逻辑也就是promise(承诺)的类名的由来。

状态只能改变一次,之后的resolve()和reject()都不会被执行。

2 Promise.then()的链式调用

2.1 基础用法

let prom = new Promise(function (resolve, reject) {
  console.log('Origin Promise');
  resolve();
});
prom.then(function() { console.log('first then') })
  .then(function() { console.log('second then') })
  .then(function() { console.log('third then') });

输出结果如下:

Origin Promise
first then
second then
third then

2.2 链式传参

首先,Promise.then()方法之所以可以链式调用,是因为then()方法隐式返回了一个Promise对象,相当于:

Promise.then(function () {
  // do something
  return new Promise(function (resolve, reject) { resolve() }); // auto return
})

同时我们知道,Promise对象通过调用resolve函数来向下一步传递参数。然而默认的then()方法返回的Promise对象不会传递任何参数。也就是说,默认的链式调用方法只能用于顺序执行程序而不能传参。

 想要让每一步之间互相关联(传参),就必须显式地返回我们自定义的Promise对象。

let prom = new Promise(function (resolve, reject) {
    let data = 'Important data!';
    resolve(data);
});
prom.then(function(incomeData) {
   return new Promise(function (resolve) { resolve(incomeData) });
 })
 .then(function(incomeData) {
   return new Promise(function (resolve) { console.log(incomeData) });
 });

// Important data!

每次都要返回那么长一串代码是不是很麻烦呢?所以我们可以自己写一个修饰函数。

// 一个Promise修饰函数
let myThen = function (fun) {
  return function (data) {
    return new Promise(function (resolve) { resolve(fun(data)) });
  }
};
// 使用示例
myThen(() => 'Important data!')()
  .then(myThen(data => data))
  .then(data => console.log(data));

// Important data!

使用上面的修饰后的函数,就可以专注于编写业务逻辑,而不用关心参数传递了。通过普通的return就可以传递参数。

当然,我的解决方案也不是那么美观(多了一层函数嵌套),各位在了解Promise原理后也可以书写自己喜欢的封装函数。

另外说一个偏方:可以在Promise之外定义一个变量,用于存储Promise各个步骤之间传递的参数,但是这个参数不能在Promise之外的地方调用(无法确认其内容),且占用的内存在Promise逻辑全部执行完毕之前不能释放,可能存在内存溢出问题。

2.3 生成“子线程”

一般来说,一个Promise对象用于一个异步顺序逻辑。但是实际上,一个Promise对象可以接多个then()方法,这时,每个then()方法都会生成一个新的Promise对象,各个对象之间相互独立,且可以继承相同的父代参数,但是互相之间不能传参。当然Node.js是单线程的,所以这里只是模拟了多个子线程而已。

2.4 易错点

这里记录几个我在使用过程中出现的易错点:

(1)new Promise与Promise.then()方法不同,Promise.then()方法可以不传入任何参数而正常执行(相当于执行了一个空的步骤而进入下一步),而new Promise必须传入一个函数作为参数,且该函数最好有一个resolve参数并且被调用过。

    new Promise没有传入函数:报错

    new Promise传入函数没有resolve或resolve未被调用(且未报错或reject):Promise始终处于pending状态且不执行下一步

(2)有时候为了让程序更容易理解,可能会在一段链式调用逻辑结束以后,重新调用对象名来强调一下逻辑依然在该链式调用之后。例如:

let prom = new Promise(function (resolve, reject) {
    let data = 'Important data!';
    resolve(data);
});
// 注意要将返回的对象传回原变量
prom = prom.then(function(incomeData) {
   return new Promise(function (resolve) { resolve(incomeData) });
 })
prom.then(function(incomeData) {
   return new Promise(function (resolve) { console.log(incomeData) });
 });

// Important data!

注意要将then()方法返回的Promise对象传回原变量,这样才能正常地执行下去,否则将分成两个独立的异步顺序逻辑(类似于一个线程生成两个子线程)。

3 Promise.catch()

一般来说,我们不建议使用reject方法,而是使用catch方法。

调用reject表示“程序出现了我意料之中的错误,这个错误可以被正常地处理”并进入reject分支。

调用catch表示“程序出现了错误(不论是否提前考虑到了),这个错误将被处理”,随后进入catch()方法,程序结束。

3.1 Promise.reject()用法

来看看reject的用法:

let prom = new Promise(function (resolve, reject) {
    let data = 'Failed!';
    reject(data); // 进入reject
});
prom.then(function() {}, function(e) {
    console.log(e, 'reject');
    return Promise.reject(); // 继续reject
  })
  .then(function () {}, function () {
    console.log('next reject!');
  })
  .catch(function(e) {console.log(e, 'catch')});

// Failed! reject
// next reject!

看到这里我们就明白为什么我们不推荐使用reject了,因为reject也是可以链式调用的!其实reject跟resolve处于平等的地位,相当于if与else的区别,即便我们不使用resolve,一直使用reject进行链式调用也不会有任何问题。更过分的是:

let prom = new Promise(function (resolve, reject) {
    throw 'error!'; // 抛出错误
});
prom.then(function() {}, function(e) {
    console.log(e, 'reject'); // reject捕获了error
  })
  .catch(function(e) {console.log(e, 'catch')}); // 没有进入catch分支
// error! reject

reject也“包办”了catch的功能!

这还没完,再看一段代码:

let prom = new Promise(function (resolve, reject) {
    throw 'error!';
});
prom.then(function() {}, function(e) {
    console.log(e, 'reject');
    return new Promise(function (resolve, reject) { resolve() }); // 调用resolve
  })
  .then(function () {
    console.log('next resolve!'); // 由reject进入resolve
  }, function () {
    console.log('next reject!');
  })
  .catch(function(e) {console.log(e, 'catch')});

// error! reject
// next resolve!

没错,reject分支也可以进入下一步的resolve分支!

所以说,reject和resolve只是名字不同的两个参数而已,没必要经常使用reject,真的没多大区别……

3.2 Promise.catch()用法

Promise.catch()的用法就没那么多坑了:

let prom = new Promise(function (resolve, reject) {
    throw 'first error!'; // 第一个报错
    resolve();
});
prom.then(function() {
    throw 'second error!';
  })
  .then(function () {
    throw 'third error!';
  })
  .catch(function (e) { console.log(e) });

// first error!
let prom = new Promise(function (resolve, reject) {
    // throw 'first error!';
    resolve();
});
prom.then(function() {
    throw 'second error!'; // 第二个报错
  })
  .then(function () {
    throw 'third error!';
  })
  .catch(function (e) { console.log(e) })

// second error!

可以看到catch可以捕捉到链式调用中任意一步抛出的错误,并且后续指令都不执行。

另外:

let prom = new Promise(function (resolve, reject) {
    // throw 'first error!';
    reject('first reject!'); // 调用reject
});
prom.then(function() {
    throw 'second error!';
  })
  .then(function () {
    throw 'third error!';
  })
  .catch(function (e) { console.log(e) })

// first reject!

reject方法传递的参数也可以使用catch()捕获,reject的存在意义又少了一层……

当然这也许就是resolve与reject最大的区别了…… 

4 Promise.all()

4.1 基础用法

Promise.all()接收一个Promise对象数组,并监视该数组中的Pormise对象,当所有Promise都进入resolved之后执行then():

let p1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 1 complete!');
    resolve('p1');
  }, 1000);
});
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 2 complete!');
    resolve('p2');
  }, 2000);
});
let p3 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 3 complete!');
    resolve('p3');
  }, 3000);
});
Promise.all([p1, p2, p3]).then(function (data) {
  console.log('All complete!');
  console.log('data: ', data);
})

输出结果如下:

Promise 1 complete!
Promise 2 complete!
Promise 3 complete!
All complete!
data:  [ 'p1', 'p2', 'p3' ]

Promise.all()会监视数组中所有Promise对象并将其结果作为一个数组传递给then()方法。
Promise.all()接收的数组也可以包含非Promise元素,当数组元素不是Promise对象时,会自动生成一个resolved状态的Promise对象,并且将元素作为参数传递下去。

4.2 有reject的情况

现在我们让其中一个Promise变成rejected状态:

let p1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 1 complete!');
    resolve('p1');
  }, 1000);
});
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 2 complete!');
    reject('p2'); // 这里reject
  }, 2000);
});
let p3 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 3 complete!');
    resolve('p3');
  }, 3000);
});
Promise.all([p1, p2, p3]).then(function (data) {
  console.log('All complete!');
  console.log('data: ', data);
}, function (rejectData) {
  console.log('Reject: ', rejectData); // 进入rejected分支
})

结果如下:

Promise 1 complete!
Promise 2 complete!
Reject:  p2
Promise 3 complete!

可以看到一旦有一个Promise变为rejected,Promise.all()就会直接进入then(),且会将reject的传参传递给then中定义的rejected分支函数(未定义rejected分支函数的话会报错),其他Promise会继续执行,但是无论是已经完成的还是未完成的Promise都不再监听,且其结果也没有接收。

4.3 有报错的情况

let p1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 1 complete!');
    resolve('p1');
  }, 1000);
});
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 2 complete!');
    throw 'p2 error!'; // 添加报错
  }, 2000);
})
  .catch(function (e) { console.log('local log: ', e) }); // 尝试本地处理
let p3 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 3 complete!');
    resolve('p3');
  }, 3000);
});
Promise.all([p1, p2, p3]).then(function (data) {
  console.log('All complete!');
  console.log('data: ', data);
}).catch(function (e) { // 尝试捕获
  console.log('error: ', e);
})

结果如下:

Promise 1 complete!
Promise 2 complete!

D:\test\test.js:39
    throw 'p2 error!';
    ^
p2 error!

结果我们可以发现,不论是在本地捕获还是在all()之后捕获,都是无效的!Error直接穿过了Promise对象,导致程序报错退出。

而且我们知道,try...catch...方法对于异步逻辑是无效的。因此,想要捕捉Promise.all()内部的报错,只能在每个Promise的异步操作执行的时候捕捉。

let p1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 1 complete!');
    resolve('p1');
  }, 1000);
});
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    // 本地捕捉error
    try{
      console.log('Promise 2 complete!');
      throw 'p2 error!';
    }
    catch (e){
      console.log('local error: ', e);
    }
  }, 2000);
});
let p3 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 3 complete!');
    resolve('p3');
  }, 3000);
});
try{
  Promise.all([p1, p2, p3]).then(function (data) {
    console.log('All complete!');
    console.log('data: ', data);
  })
}
catch (e) {
  console.log('Error: ', e);
}

输出如下:

Promise 1 complete!
Promise 2 complete!
local error:  p2 error!
Promise 3 complete!

程序没有退出,但是Promise.all()还是终止了,且没有触发catch()方法。 

从这里我们可以看出,Promise还是存在诸多问题的。

5 Promise.race()

5.1 基础用法

Promise.race()方法也接收一个Promise对象数组,当元素不是Promise对象时会自动生成resolved状态的Promise对象并将元素作为参数传递。

Promise.race()当任何一个Promise对象完成时进入resolved状态,其他Promise会继续执行但是不再监控也不获取结果:

let p1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 1 complete!');
    resolve('p1');
  }, 2000);
});
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 2 complete!');
    resolve('p2');
  }, 1000);
});
let p3 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 3 complete!');
    resolve('p3');
  }, 3000);
});
Promise.race([p1, p2, p3]).then(function (data) {
  console.log('All complete!');
  console.log('data: ', data);
});

其结果如下:

Promise 2 complete!
All complete!
data:  p2
Promise 1 complete!
Promise 3 complete!

5.2 有reject的情况

当Promise对象中存在reject时:

let p1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 1 complete!');
    resolve('p1');
  }, 2000);
});
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 2 complete!');
    reject('p2'); // reject
  }, 1000);
});
let p3 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log('Promise 3 complete!');
    resolve('p3');
  }, 3000);
});
Promise.race([p1, p2, p3]).then(function (data) {
  console.log('All complete!');
  console.log('data: ', data);
}, function (e) {
  console.log('Reject: ', e);
});

打印结果如下:

Promise 2 complete!
Reject:  p2
Promise 1 complete!
Promise 3 complete!

由于Promise对象的状态只会转变一次,所以第一个Promise进入resolved状态后,Promise.race()就不再监听了,之后如果有reject也不会产生效果。