JS中的异步处理方法之Promise

前言

为什么需要Promise

首先,Promise是为了实现异步编程,而对于js这种单线程语言来说,同步和异步操作时代码运行的核心机制。那么之前的异步操作是怎么实现的呢?
基本上使用setTimeout来实现的:

function add() {
	let a = 1
	setTimeout(() => a + 1, 1000)
}

那这样看来还是不需要Promise,可以很多时候我们不是简单的只执行一个异步操作就结束了,往往我们需要等这个异步的结果,并把它传给下一个异步当作初始数据

function foo(num, callback = null) {
      setTimeout(() => {
        console.log(num++);
        callback ? callback() : console.log('无回调函数');
      }, 1000)
    }


function count(num) {
  foo(num, () => {
    let res = ++num
    foo(res, () => {
      let res2 = ++res
      foo(res2, () => {
        let res3 = ++res2
        foo(res3, () => {
          let res4 = ++res3
          foo(res4)
        })
      })
    })
  })
}

count(1)

由上可见,虽然我们实现了功能,但是这样的代码并不清晰,尤其是当其中掺杂了了一些复杂的逻辑代码时,就更难使人找到每一步的回调所在了,我们通常称这种代码为回调地狱。也正是为了解决这一类问题,需要用到Promise。

1、Promise的基本概念

1.1 Promise的创建

Promise(或者可以说是期约)是ES6新增的引用类型,可以通过new来创建实例,创建实例时需要后才能如传入参数(执行器函数)。

const p = new Promise(() => {})

1.2 Promise的状态

Promise有3中状态,分别为待定(pending)、兑现(fulfilled)、拒绝(rejected)。其中待定是Promise的初始状态,在这个状态下,可以将Promise设置为兑现(fulfilled)或者拒绝(rejected)状态。而且,只要Promise的状态从初始状态改变了,那么就不可以再次改变。当然,Promise可以一直处于待定状态。

1.3 改变Promise的状态

可以通过执行函数来改变Promise的状态:

// 改变为兑现状态
let p = new Promise((resolve, reject) => setTimeout(() => {
   resolve()
}, 0))
setTimeout(console.log, 0, p) // Promise {: undefined}
// 改编为拒绝状态
let p = new Promise((resolve, reject) => setTimeout(() => {
   reject()
}, 0))
setTimeout(console.log, 0, p) // Promise {: undefined}

1.4 Promise.resolve()方法

Promise并不一定非要是以待定状态初始化,我们可以直接调用Promise.resolve()创建一个处于兑现状态的Promsie实例:

// 下面两个的结果是一致的
const p = new Promise((resolve, reject) => resolve())
const p1 = Promise.resolve()

// Promise.resolve()可以将任何传入的参数转为期约(Promise实例)
const p1 = Promise.resolve(1) // Promise {: 1}
const p2 = Promise.resolve([1, 2]) // Promise {: Array(2)}

注意:当往Promise.resolve()中传入一个Promsie的时候,那该静态方法的行为就像相当于一个空包装,由于这个机制,所以Promise.resolve()会保留传入Promise的状态:

const p = Promise.resolve(1)
setTimeout(console.log, 0, p === Promise.resolve(p))
// true

const p2 = Promise.resolve(new Promise(() => {}))
setTimeout(console.log, 0, p2)

1.5 Promise.reject()

同理。Promise.reject()方法可以创建一个拒绝状态的Promise,与Promise.resolve()相似,往该方法中传入非Promise的参数,将会返回对应的拒绝状态的Promise对象
但是如果传入的是Promise对象,那么这个对象会被直接作为拒绝状态的Promise的理由。

const p = Promise.reject(3)
console.log(p) // Promise {: 3}

const p1 = Promise.reject(Promise.resolve())
console.log(p1) // Promise {: Promise}

2、Promise的实例方法

Promise存在then方法,具体为Promise.prototype.then(),这个方法接收最多两个参数:onResolved函数(成功状态下的回调函数)和onRejected处理函数(拒绝状态下的回调函数)。这两个参数都是可选的。

 const p2 = p1.then(
   value => {
     // 成功状态进入该函数
     console.log('成功', value);
   },
   res => {
     // 拒绝状态时进入该函数
     log('拒绝', res)
   }
 )
 console.log(p2); // Promise

2.1 成功状态下的回调结果

/**
  * Promise.prototype.then()返回的是一个新的Promise实例这个新期约实例基于 onResovled 处理程序的返回值构建。
  * 换句话说,该处理程序的返回值会通过Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则 Promise.resolve()就会包装上一个期约解决之后的值。
  * 如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回值 undefined。
 */
const p1 = Promise.resolve('foo')
console.log(p1);

const p2 = p1.then() // 默认情况下,由于没有提供resolve处理函数,所以返回的是用Promise.resolve()包装上一个Promise返回的结果:Promise : undefined
// 注:此处由于then方法是异步,所以不能用同步打印的方式来打印结果
setTimeout(console.log, 0, p2)
const p3 = p1.then(() => Promise.resolve()) // 不是显式的返回值,作默认处理
const p4 = p1.then(2) // 直接传入非函数,会被忽略,所以不推荐这样写;这样返回的是上一个promise的结果
const p5 = p1.then(() => undefined) // 不是显式的返回值,作默认处理
const p6 = p1.then(() => {}) // 不是显式的返回值,作默认处理
setTimeout(console.log, 0 , 'p3:', p3) // Promise {: undefined}
setTimeout(console.log, 0 , 'p4:', p4) // Promise {: 'foo'}
setTimeout(console.log, 0 , 'p5:', p5) // Promise {: undefined}
setTimeout(console.log, 0 , 'p6:', p6) // Promise {: undefined}

// 有显式的返回值
const p7 = p1.then(() => 'a')
const p8 = p1.then(() => Promise.resolve('b'))
setTimeout(console.log, 0 , 'p7:', p7) // Promise {: 'a'}
setTimeout(console.log, 0 , 'p8:', p8) // Promise {: 'b'}

const p9 = p1.then(() => new Promise(() => {}))
const p10 = p1.then(() => Promise.reject('error')) // 传入Promise.reject()会报错
// Uncaught (in promise) error
setTimeout(console.log, 0 , 'p9:', p9) // Promise {}
setTimeout(console.log, 0 , 'p10:', p10) // Promise {: 'error'}


// 抛出异常会返回拒绝的Promise
const p11 = p1.then(() => { throw 'error' })
const p12 = p1.then(() => Error('error'))
// Uncaught (in promise) error
setTimeout(console.log, 0 , 'p11:', p11) // Promise {: error}
setTimeout(console.log, 0 , 'p12:', p12) // Promise {: Error: error}

2.2 拒绝状态下的回调结果

const p = Promise.reject('foo')

const p1 = p.then() // 没有传入任何程序,照原样往后传
const p2 = p.then(null, 2) // 传入非函数,照原样往后传
setTimeout(console.log, 0, 'p1', p1) // Promise {: 'foo'}
setTimeout(console.log, 0, 'p2', p2) // Promise {: 'foo'}

// 注意,往Promise.reject()的then()中传入各种处理方法,返回的值还是会被Promise.resolve()所包装
const p3 = p.then(null, () => {})
const p4 = p.then(null, () => Promise.resolve())
const p5 = p.then(null, () => undefined)
setTimeout(console.log, 0, 'p3', p3) // Promise {: undefined}
setTimeout(console.log, 0, 'p4', p4) // Promise {: undefined}
setTimeout(console.log, 0, 'p5', p5) // Promise {: undefined}


// 传入函数并返回非空值
const p6 = p.then(null, () => 'res')
const p7 = p.then(null, () => Promise.resolve('res'))

setTimeout(console.log, 0, 'p6', p6) // Promise {: 'res'}
setTimeout(console.log, 0, 'p7', p7) // Promise {: 'res'}

// 传入新的Promise,会保留这个Promise的状态
const p8 = p.then(null, () => new Promise(() => {}))
const p9 = p.then(null, () => Promise.reject())
// Uncaught (in promise) undefined
setTimeout(console.log, 0, 'p8', p8) // Promise {}
setTimeout(console.log, 0, 'p9', p9) // Promise {: undefined}

// 抛出错误会返回拒绝的Promise, 但是直接返回错误,并不会返回拒绝的Promise
const p10 = p.then(null, () => {throw 'error'})
const p11 = p.then(null, () => Error('error'))

setTimeout(console.log, 0, 'p10', p10) // Promise {: 'error'}
setTimeout(console.log, 0, 'p11', p11) // Promise {: Error: error}

2.3 Promise.prototype.catch()方法

Promise.prototype.catch()方法,其本质就是一个语法糖,相当于:

// 以下两种写法效果一致
let p = Promise.reject('error')
p.then(null, (e) => {
	console.log(e)
})

const p1 = p.catch(() => (e) => {
	console.log(e)
})

Promise.prototype.catch()返回一个新的Promsie实例,它的返回和Promsie.prototype.then()的onRejected处理程序一致

2.4 Promise.prototype.finally()

Promise.prototype.finally()方法用于给期约(Promise)添加 onFinally 处理程序,这个处理程序在Promise转换为解决或拒绝状态时都会执行。所以如果想处理一些公共的逻辑可以放在此处,以避免onResolved和onRejected处理程序的代码冗余。
Promise.prototype.finally()也会返回一个新的Promise实例。

const p1 = Promise.resolve('foo')
const p2 = Promise.reject('error')

function onFinally() {
  setTimeout(console.log, 0, 'finally deal')
}
// Promise.prototype.finally()返回的值新的Promise实例,且大多数时候都是传递父级Promise
const p3 = p1.finally(onFinally)
// const p4 = p1.finally(onFinally)
const p4 = p1.finally()
const p5 = p1.finally(() => {})
const p6 = p1.finally(() => null)
const p7 = p1.finally(() => Promise.resolve('bar'))
const p8 = p1.finally(() => 'bar')
const p9 = p1.finally(() => Error('error1'))
setTimeout(console.log, 0, 'p3', p3); // Promise {: 'foo'}
setTimeout(console.log, 0, 'p4', p4); // Promise {: 'foo'}
setTimeout(console.log, 0, 'p5', p5); // Promise {: 'foo'}
setTimeout(console.log, 0, 'p6', p6); // Promise {: 'foo'}
setTimeout(console.log, 0, 'p7', p7); // Promise {: 'foo'}
setTimeout(console.log, 0, 'p8', p8); // Promise {: 'foo'}
setTimeout(console.log, 0, 'p9', p9); // Promise {: 'foo'}

// 如果返回的是一个待定的Promise,或者说处理程序抛出了错误(throw了错误或者返回了拒绝状态的Promise),那么就会返回对应的结果
const p10 = p1.finally(() => new Promise(() => {}))
const p11 = p1.finally(() => {throw 'error1'})
const p12 = p1.finally(() => Promise.reject('error1'))

const p13 = p2.finally(() => Promise.reject('error2'))
setTimeout(console.log, 0, 'p10', p10); // Promise {}
setTimeout(console.log, 0, 'p11', p11); // Promise {: 'error1'}
setTimeout(console.log, 0, 'p12', p12); // Promise {: 'error1'}
setTimeout(console.log, 0, 'p13', p13); // Promise {: 'error2'}

总结:
1、finallly()返回的值根据其处理程序而定,如果处理程序返回的是空、成功状态的Promise或字符串之类的结果,那么finally()返回的值会继承父级的结果,而不会采用自身处理程序返回的值
2、如果finallly()显式的抛出了错误或是一个拒绝Promise又或者返回了一个处于待定状态的Promise,那么最终返回的就是对应的Promise,此时的情况预then()方法类似。

3 非重入期约方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的。这个特性由 JavaScript 运行时保证,被称为“非重入”(non-reentrancy)特性。(出自《JacaScript高级程序处理 第四版》)

const p = new Promise((resolve) => {
  console.log('new Promise');
  resolve('foo')
})
p.then(
  value => {
    console.log(value);
  },
  error => {}
)
console.log('哈哈哈');
// new Promise
// 哈哈哈
// foo

// 即使先添加处理程序后解决Promise,结果也是一样
let dealSync
const p2 = new Promise((resolve) => {
  dealSync = function() {
    console.log('1: new Promise');
    resolve()
    console.log('2: resolve后');
  }
})
p2.then(() => { console.log('3: fulfilled'); })
dealSync()
console.log('4: end');
// 1: new Promise
// 2: resolve后
// 4: end
// 3: fulfilled

4 传递解决值和拒绝理由

当Promise状态有待定改变后,就会传递对应的结果给后续的处理程序,在执行函数中,是通过reslove()和reject()来传递结果的,他们分别被传递给onReloved()和onReject()函数,作为这个两个函数的唯一参数

let p1 = new Promise((resolve, reject) => resolve('foo')); 
p1.then((value) => console.log(value)); // foo 
let p2 = new Promise((resolve, reject) => reject('bar')); 
p2.catch((reason) => console.log(reason)); // bar

5 拒绝状态下的Promise的处理

在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。因此以下这些期约都会以一个错误对象为由被拒绝:

// 下面几种方法都会返回Promise.reject
const p1 = Promise.reject(Error('error1'))
const p2 = new Promise((resolve, reject) => { throw Error('error2') })
const p3 = Promise.resolve().then(() => {throw Error('error3')})
const p4 = Promise.reject(Error('error4'))

setTimeout(console.log, 0, '1', p1) // Promise {: Error: error1
setTimeout(console.log, 0, '2', p2) // Promise {: Error: error2
setTimeout(console.log, 0, '3', p3) // Promise {: Error: error3
setTimeout(console.log, 0, '4', p4) // Promise {: Error: error4

1、在期约(Promise)中抛出错误时,由于是异步抛出的,所以不会阻止Promise同步代码的执行;
2、Promise的异常只能通过OnReject异常处理程序捕获,所以在Promise外部使用try catch是无效的,但是在Promise内部是可以使用的

// 无效
try { 
 Promise.reject(Error('foo')); 
} catch(e) {}

// 有效
let p = new Promise((resolve) => {
  resolve()
  try {
    throw Error('在new Promise中的error')
  } catch (error) {
    console.log('--------------', error);
  }
  // resolve()
})

setTimeout(console.log, 0, '5', p)

6 Promise连锁和Promise合成

这个就要回到我们开头说的问题了,怎么解决回调地狱?我们可以用Promise连锁和Promise合成来尝试解决。
多个期约组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现:期约连锁与期约合成。前者就是一个期约接一个期约地拼接,后者则是将多个期约组合为一个期约(出自《JacaScript高级程序处理 第四版》)。

6.1 Promise连锁

let p1 = new Promise((resolve, reject) => { 
 console.log('p1 executor'); 
 setTimeout(resolve, 1000); 
}); 
p1.then(() => new Promise((resolve, reject) => { 
 console.log('p2 executor'); 
 setTimeout(resolve, 1000); 
 })) 
 .then(() => new Promise((resolve, reject) => { 
 console.log('p3 executor'); 
 setTimeout(resolve, 1000); 
 })) 
 .then(() => new Promise((resolve, reject) => { 
 console.log('p4 executor'); 
 setTimeout(resolve, 1000); 
 })); 
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

以上的方法中,也可以将生成Promise的代码提取出去,这样更简约

function getPromise(msg) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(msg);
      resolve()
    }, 1000)
  })
}
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('p1 start');
    resolve()
  }, 1000)
})
p.then(value => getPromise('p2 start'))
.then(value => getPromise('p3 start'))
.then(value => getPromise('p4 start'))
// p1 start (1秒后)
// p2 start (2秒后)
// p3 start (3秒后)
// p4 start (4秒后)

6.2 Promise合成

6.2.1 Promise.all()和Promise.race()

Promise类提供了两个方法将多个Promise组合成一个Promise,它们分别是Promise.all()和Promise.race()
Promise.all()
Promise.all()创建的Promise实例会在所有Promise都解决后再解决,需要传入一个可迭代对象,它会返回一个新的Promise对象:

 // Promise.all()方法创建的期约会在一组Promise全部结束后在解决
const p = Promise.all([
  Promise.resolve('a'),
  Promise.resolve('b')
])
setTimeout(console.log, 0, 'p', p) // Promise {: Array(2)}

// 往Promise.all()中传入空的数组,相当于Promise.resolve,注意,Promise.all()方法中不不能不传,否则会报错
const p1 = Promise.all([])
setTimeout(console.log, 0, 'p1', p1) // Promise {: Array(0)}

// 由上可见,Promise.all()返回的还是一个Promise实例
// Promise.all()必须等内部所有的Promise等有结果后才会返回
const p2 = Promise.all([
  Promise.resolve(),
  new Promise(() => {})
])
setTimeout(console.log, 0, 'p2', p2) // Promise {};可以看到Promise.all()会一直处于pending状态

1、只要有一次拒绝,那么整个Promise.all()的返回就会是一个拒绝状态的Promise,
2、如果都被解决了,那么返回的就是一包装了由所有期约解决值组成的数组的fulfilled状态的Promise。

const p3 = Promise.all([
  Promise.resolve(),
  Promise.reject(),
  Promise.resolve()
])
setTimeout(console.log, 0, 'p3', p3) // Promise {: undefined}

const p4 = Promise.all([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
])
setTimeout(console.log, 0, 'p4', p4) // Promise {: Array(3)}

/**
 * 另外,当出现了拒绝时,Promise.all指挥返回第一个拒绝的Promise的结果,后续的拒绝返回值不会用到,
 * 但是之后的拒绝并不是就不执行了,它们会被静默处理
 */
const p5 = Promise.all([
  Promise.reject('error'),
  new Promise((resolve, reject) => setTimeout(reject, 1000))
])
setTimeout(console.log, 0, 'p5', p5) // Promise {: 'error'}

Promise.race()
Promise.race()静态方法返回一个Promise实例,它接收一个可迭代对象,并返回这个对象中最先解决或拒绝的Promise的镜像(会对结果用Promise.resolve进行包装)

let p1 = Promise.race([
  Promise.resolve(),
  Promise.reject()
])

setTimeout(console.log, 0 , 'p1', p1) // Promise {: undefined}

let p2 = Promise.race([1, 4])
setTimeout(console.log, 0, 'p2', p2) // Promise {: 1}

// 先解决的才会被返回,
let p3 = Promise.race([
  // Promise.resolve('foo').then(res => {console.log(res)}), // 这个还是会执行,但是不会返回,因为下一个先执行完
  new Promise(resolve => setTimeout(resolve, 1000)), // 这个还是会执行,但是不会返回结果,因为下一个先执行完
  Promise.reject('foo')
])
// Uncaught (in promise) foo
setTimeout(console.log, 0, 'p3', p3) // Promise {: 'foo'}

let p4 = Promise.race([]) // 不传入可迭代对象,则相当于传入了new Promise(() => {})
setTimeout(console.log, 0 , 'p4', p4)

let p5 = Promise.race() // 报错,必须传入参数
setTimeout(console.log, 0, 'p5', p5) // Uncaught (in promise) TypeError: undefined is not iterable

// 当结果有多个拒绝拒绝时,Promise.race()只会返回第一个错拒绝, 但是后续的拒绝也会被静默执行,和Promise.all()类似
let p6 = Promise.race([
  Promise.reject('error2'),
  new Promise((resolve, reject) => setTimeout(reject, 1000))
])
setTimeout(console.log, 1000, 'p6', p6) // Promise {: 'error2'}

6.2.2 串行期约(Promise)合成

我们可以参考将多个函数合成为一个函数的方式来串行多个Promise以合成一个Promise。如

function addOne(x) {
  return x + 1
}
function addTwo(x) {
  return x + 2
}
function addThree(x) {
  return x + 3
}

function addAsync(x) {
  return new Promise((resolve) => {
    resolve(x)
  }).then(addOne).then(addTwo).then(addThree)
}
addAsync(10).then(value => {console.log(value)})

还可以进行进一步的简化

// 使用reduce改造
function addAsync(x) {
  return [addOne, addTwo, addThree]
  .reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
addAsync(12).then(console.log)


//  进一步提炼,现在可以传入任意数目的方法
function addModel(...fns) {
  return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x)) 
}
const addAsync2 = addModel(addOne, addTwo, addThree)
addAsync2(4).then(console.log)

你可能感兴趣的:(js,javascript,前端)