深入学习Promise(一):基础以及实例方法

73 阅读10分钟

以下为阅读《红宝书》第11章 期约与异步函数 所作笔记,并手写感受Promise~

以往的异步编程模式

成功 失败的回调处理

// 这种模式已经不可取了 因为必须在初始化异步操作时定义回调
function double(value, success, failure) {
    setTimeout(() => {
        try {
            if (typeof value !== 'number') {
                throw 'value must be number'
            }
            success(2 * value)
        } catch (err) {
            failure(err)
        }
    }, 2000)
}
const successCallback = (x) => console.log('success:', x)
const failCallback = (e) => console.log('err:', e)
double(2, successCallback, failCallback)
double('a', successCallback, failCallback)

嵌套异步回调

随着代码越来越复杂,回调策略是不具有扩展性的。深度嵌套的回调函数称为‘回调地狱’

const successCallbackNest = (x) => {
      double(x, (y) => console.log('success:', y))
}

对Promise的理解

Promise是异步编程 的一种解决方案,主要解决回调地狱的问题,之前是使用函数层层回调;语法上Promise是一个构造函数,我们可以利用它实例化对象

基础

  1. promise(期约) 可以通过 new 操作符来实例化,创建时需要传入执行器executor函数作为参数,如果不提供执行器函数,会抛出错误。实例处于待定 pending状态

    // let p = new Promise() 报错
    let p = new Promise(() => {})
    console.log(p);    // Promise {<pending>}
    
  2. promise是一个有状态的对象,可能处于如下3个状态之一,一旦状态发生改变,是不可逆的

    1. pending 待定
    2. fulfilled 兑现/解决,resolved
    3. rejected 拒绝
  3. promise 的状态是 私有的,不能通过js检测到,也不能被外部js代码修改,promise故意将异步行为封装起来,从而分离外部的同步代码。所以只能在内部进行操作,内部操作在promise的 执行器函数 中完成

    执行器函数主要有两项职责:初始化promise的异步行为控制状态的最终转换(通过调用它的两个函数参数实现的,通常命名为 resolve-将状态切换为成功 和 reject -将状态切换为失败并抛出错误)

    let p1 = new Promise((resolve,reject) => resolve())
    let p2 = new Promise((resolve,reject) => reject())
    console.log(p1);  // Promise {<fulfilled>: undefined}
    
    console.log(p2);  // Promise {<rejected>: undefined}
                      // Uncaught (in promise) undefined
    
  4. Promise.resolve()

    Promise.resolve() 等同于 new Promise((resolve,reject) => resolve())

    1. 通过该静态方法,可以实例化一个已成功的promise

    2. 使用该方法

      • 可以把任何非promise值都转换为一个promise,包括错误对象,会将其转换为成功的promise,导致出现不符合预期的行为
      • 如果传入的值本身是一个Promise,那它的行为就类似一个空包装,该方法可以说是一个幂等方法
      • 这个幂等性会保留传入promise的状态
          console.log(Promise.resolve());  // Promise {<fulfilled>: undefined}
          console.log(Promise.resolve(3)); // Promise {<fulfilled>: 3}
          // 多余的参数会被忽略
          console.log(Promise.resolve(4, 5, 6)); // Promise {<fulfilled>: 4}
      
          let p = Promise.resolve(7);
          console.log(p === Promise.resolve(p));  // true
          console.log(p === Promise.resolve(Promise.resolve(p))); // true
      
          let p1 = new Promise(() => { })
          console.log(p1 === Promise.resolve(p1));   // true
      
          let p2 = Promise.resolve(new Error('foo'))
          console.log(p2);  // Promise {<fulfilled>: Error: foo
      
  5. Promise.reject()

    Promise.reject() 等同于 new Promise((resolve,reject) => reject())

    1. 通过该方法,可以实例化一个拒绝的promise,并抛出一个异步错误,这个错误不能通过 try/catch 捕获,只能通过拒绝处理程序捕获

    2. 传入第一个参数会作为后序的拒绝处理程序

    3. 如果给它传一个promise对象,则这个promise会成为它返回的拒绝promise的理由

      let pp1 = Promise.reject(1)
      console.log(pp1);   // Promise {<rejected>: 1}
      console.log(Promise.reject(Promise.resolve()));  //Promise {<rejected>: Promise<resolved>}
      

promise的实例方法

then()

  1. Promise.prototype.then()是为promise实例添加处理程序的主要方法,接收最多两个参数

    1. onResolved处理程序 和 onRejected处理程序
    2. 这两个参数都是可选
    3. 如果提供的话,会在promise分别进入 ‘兑现’ 和 ‘拒绝’ 状态时执行
    4. 因为Promise只能转换为最终状态一次,所以这两个操作一定是 互斥
        let p1 = new Promise((resolve, reject) => setTimeout(resolve, 2000))
        let p2 = new Promise((resolve, reject) => setTimeout(reject, 2000))
        p1.then(
            () => console.log('p1 resolved'),
            () => console.log('p1 reject')
        )    // p1 resolved
        p2.then(
            () => console.log('p2 resolved'),
            () => console.log('p2 rejected')
        )    // p2 rejected
    
  2. 两个处理程序参数都是可选的

    1. 传给then()的任何 非函数类型的参数 都会被 静默忽略
    2. 若想只提供onRejected参数,应在onResolved参数的位置上传入 undefined / null,有助于避免在内存中创建多余的兑现,对Promise的类型系统也是一个交代
        let p3 = new Promise((resolve, reject) => setTimeout(reject, 2000))
        // 不传onResolved处理程序的规范写法
        p3.then(null, () => console.log('p3 rejected'))
    
  3. 该方法返回的是一个新的Promise实例

    1. 这个新promise实例基于onResolved处理程序的返回值构建。换句话说,该处理程序的返回值会通过Promise.resolve()包装来生成新的promise,如果没有提供处理程序,则Promise.resolve()会包装上一个Promise解决后的值。如果没有显示的返回语句,则会包装默认的返回值 undefined
        let p1 = new Promise(() => { })
        let p11 = new Promise((resolve, reject) => resolve())
        let p2 = p1.then()
        let p22 = p11.then()
        console.log(p2);   // Promise {<pending>}
        console.log(p11);  // Promise {<fulfilled>: undefined}
        console.log(p22);  // Promise {<fulfilled>: undefined}
        console.log(p2 === p1); // false
    
        // 不提供或没有显示的返回语句
        let p1 = Promise.resolve('foo');
        let p2 = p1.then()
        console.log(p2);   // Promise {<fulfilled>: foo}
        let p3 = p1.then(() => undefined);
        console.log(p3);   // Promise {<fulfilled>: undefined}
        let p4 = p1.then(() => { });
        console.log(p4);   // Promise {<fulfilled>: undefined}
        let p5 = p1.then(() => Promise.resolve());
        console.log(p5);   // Promise {<fulfilled>: undefined}
    
        // 有显示的返回值,会包装这个值
        let p6 = p1.then(() => 'bar')
        console.log(p6);   // Promise {<fulfilled>: bar}
        let p7 = p1.then(() => Promise.resolve('bar'))
        console.log(p7);   // Promise {<fulfilled>: bar}
        let p8 = p1.then(() => Promise.reject())
        console.log(p8);   // Promise {<rejected>: undefined}
    
        // 抛出异常会返回拒绝的promise
        let p9 = p1.then(() => { throw 'baz' })
        console.log(p9);    // Promise {<rejected>: baz}
        let p10 = p1.then(() => Error('qux'))    // 返回错误值不会触发上面的拒绝行为,会将错误对象包装在一个解决的promise中
        console.log(p10);   // Promise {<fulfilled>: Error:qux}
    
    1. onRejected处理程序返回的值也会被Promise.resolved包装,该处理程序是在捕获异步错误,捕获错后不抛出异常是符合promise的行为,返回一个resolved的promise

catch()

  1. Promise.prototype.catch()用于给promise添加拒绝处理程序,只接受一个参数
  2. 该方法是一个语法糖,相当于 Promise.prototype.then(null,onRejected)
  • 注:一般不要在then方法中定义Rejected状态的回调函数(即 then的第二个参数),而应总是使用catch方法
  1. 该方法返回的是一个新的Promise实例
// 这两种添加拒绝处理程序的方式是一样的
let p = Promise.reject()
p.then(null, () => console.log('rejected'))  // rejected
p.catch(() => console.log('rejected'))  // rejected

finally()

  1. Promise.prototype.finally()在promise转换为解决或拒绝状态都会执行,但该方法无法知道promise的状态是解决还是拒绝,主要用于添加清理代码

  2. 该方法不接受任何参数

  3. 该方法返回一个新的Promise实例

    新Promise实例不同于 then或catch 方式返回的实例,在大多数情况下它表现为父Promise的传递,对于已解决或被拒绝状态都是如此

let p1 = Promise.resolve()
let p2 = Promise.reject()
p1.finally(() => console.log('finally'))  // finally
p1.finally(() => console.log('finally'))  // finally

// 父promise的传递
let p1 = Promise.resolve('foo')
let p2 = p1.finally()
let p3 = p1.finally(() => undefined)
let p4 = p1.finally(() => 'bar')
let p5 = p1.finally(() => Error('qux'))
console.log(p2);  // Promise {<fulfilled>: foo} 
console.log(p3);  // Promise {<fulfilled>: foo}
console.log(p4);  // Promise {<fulfilled>: foo}
console.log(p5);  // Promise {<fulfilled>: foo}

// 如果返回的是一个待定的Promise,或 finally处理程序抛出了错误(显示抛出或返回一个拒绝promise),则会返回相应的promise

非重入promise方法

  1. 当promise状态改变后,与其相关的处理程序仅仅会被排期而非立即执行。例如以下例子,跟在处理程序后面的同步代码一定会执行,这个特性由js运行时保证,‘非重入’

    在p上调用then()会把onResolve处理程序推进消息队列,但是这个处理程序在当前线程上的同步代码执行完成前不会执行

        let p = Promise.resolve()
        p.then(() => console.log(1))
        console.log(2); 
        //  2   1
    
  2. 先添加处理程序,再改变promise状态也是一样的

    即使状态改变在处理程序之后,处理程序也会等到运行的消息队列让它出列时才执行

        let syncResolve;
        // 创建一个promise并将解决函数保存在一个局部变量中
        let p = new Promise((resolve) => {
            syncResolve = function () {
                console.log(1);
                resolve()
                console.log(2);
            }
        })
        p.then(() => console.log(3))
        syncResolve()
        console.log(4);
        // 1  2  4  3
    
  3. 非重入适用于 onResolved/Rejected处理程序、catch处理程序、finally处理程序

    该例子演示了这些程序都只能异步执行

        let p1 = Promise.resolve()
        p1.then(() => console.log('p1 then onResolved'))
        console.log(1);
        let p2 = Promise.reject()
        p2.then(null, () => console.log('p2 then onRejected'))
        console.log(2);
        let p3 = Promise.reject()
        p3.catch(() => console.log('p3 catch onRejected'))
        console.log(3);
        let p4 = Promise.resolve()
        p1.finally(() => console.log('p4 finally onResolved'))
        console.log(4);
        //  1  2  3  4  p1 then onResolved    p2 then onRejected    p3 catch onRejected     p4 finally onResolved
    

邻近处理程序的执行顺序

  • 如果给promise添加了多个处理程序,当promise状态发生改变时,相关处理程序会按照它们的顺序依次执行

        let p1 = Promise.resolve()
        p1.then(() => console.log(1))
        p1.then(() => console.log(2))   // 1  2
    
        p1.then(() => setTimeout(() => console.log(1), 2000))
        p1.then(() => setTimeout(() => console.log(2), 1000))  //  2 1
    

传递 解决值 和 拒绝理由

  • 在执行函数中,解决的值和拒绝的理由分别是resolve()reject()的第一个参数往后传,然后这些值又会传给它们各自的处理程序,作为onResolved()onRejected()处理程序的唯一参数

  • Promise.resolve/reject() 在调用时就会接收 解决值拒绝理由,同样它们返回的promise也会像执行器一样把这些值传给onResolved()onRejected()处理程序

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

拒绝期约与拒绝错误处理

  1. 拒绝promise类似于 throw() 表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。在promise的处理函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由

    1. 以下都会以一个错误对象为由被拒绝
    2. promise可以以任何理由拒绝,包括undefined,但最好统一使用错误对象,主要是因为创建错误对象可以让浏览器捕获错误对象中的栈追踪信息,对调试非常关键,例如以下4个错误都是异步抛出且未处理的,通过错误对象捕获的栈追踪信息如图:
    3. 注意Promise.resolve().then()的错误最后才出现,因为它需要在运行时消息队列中添加处理程序,也就是说在最终抛出错误之前它还会创建另一个promise
  2. 上面例子揭示了异步错误有意思的副作用,正常情况,通过throw()关键字抛出错误时,js运行时的错误处理机制会停止抛出错误后的任何指令,但是promise抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令

        throw Error('foo')  // Uncaught Error: foo
        console.log('bar'); // 不会执行
    
        Promise.reject(Error('foo'))
        console.log('bar');
        // bar
        // Uncaught Error: foo
    
  3. then()catch()的onRejected处理程序在语义上相当于 try/catch。出发点都是捕获错误之后将其隔离,同时不影响正常逻辑执行。onRejected处理程序的任务应该是在捕获异步错误之后返回一个解决的promise

    以下为 同步错误处理 与 异步错误处理

        // 同步
        console.log(1);
        try {
            throw Error(2)
        } catch (e) {
            console.log('catch error:', e);
        }  
        console.log(3);        // 1   catch error:Error: 2   3
    
        // 异步
        new Promise((resolve, reject) => {
            console.log(1);
            reject(Error(2))
        }).catch((e) => {
            console.log('catch error:', e);
        }).then(() => {
            console.log(3);
        })                   // 1   catch error:Error: 2   3
    

    自我总结

    1. 通过 new操作符 可以实例化一个有状态的对象Promise,并传入一个执行器函数作为参数,此时该Promise处于pending状态
    2. 要想改变其状态,必须通过调用执行器提供的两个函数参数 resolvereject ,分别可以将promise的状态转换为fulfilled或rejected,两个状态是互斥的,且状态一旦发生改变,不可逆
    3. 可以通过 Promise.resolve()Promise.reject() 实例化一个已经解决或拒绝的Promise实例
    4. Promise原型上的 then catch finally 方法为Promise添加处理程序,均返回新的Promise实例
    5. then方法最多传入两个处理程序作为参数,均为可选,当promise进入fulfilled或rejected时执行,所以这两个处理程序也一定是互斥的
    6. catch方法相对于then方法执行第二个处理程序,只接受一个处理程序作为参数
    7. finally方法无论Promise实例进入哪个状态都会执行