Promise 完全解读

作者: 青栀灬 | 来源:发表于2017-08-15 16:13 被阅读215次

    目录

    • 一. Promise 简介
      • Promise 是什么?
      • 我们为什么需要 Promise?
      • Promise 能解决什么?
      • Promise 的特点
      • Promise 的缺点
    • 二. Promise API 解析
      • Promise 的构造方法
      • Promise 的实例方法
      • Promise 类级别方法
    • 三. Promise 深度剖析
      • Promise 与 setTimeout
      • 多层级 .then()

    一. Promise 简介

    1. Promise 是什么?

    ES6 出现的目标是为了使 JavaScript 语言可以编写大型的复杂应用程序,使之成为企业级的开发语言,Promise 也是其中的一环。

    从语法上讲 Promise 是个内置对象,抽象来看,它像是一个容器,里面保存着一个异步操作的执行结果,Promise 提供了一套 API 以保证所有异步操作的统一处理方法。

    2. 我们为什么需要 Promise?

    我们先来举一个简单的 “栗子”,某人需要做三件事(A,B,C),并且要按照这个顺序依次执行,现在将这个转换为传统的代码方式:

    // 首先我们要先定义这三件事(A,B,C)分别为三个函数
    // 这三个函数都需要提供一个回调来表示事情的结束
    function A(callback) {
      console.log('Do A.');
      callback();
    }
    
    function B(callback) {
      console.log('Do B.');
      callback();
    }
    
    function C(callback) {
      console.log('Do C.');
      callback();
    }
    
    // 现在,如果我想要依次做这三件事我需要这样。。。
    A(function() {
      // 其它的一些代码
      B(function() {
        // 其它的一些代码
        C(function() {
          // 其它的一些代码
        });
      });
    });
    

    可以看出,当涉及到异步操作时,曾经的大部分方式都是靠回调的嵌套来实现的,然而这样的方式造成了几个十分严重的问题:

    • 出现了多层回调,当加上业务代码后,将会使整体显得臃肿且凌乱。
    • 回调的出现导致逻辑的流程不清晰,不具有可读性和维护性。
    • 当嵌套层级过多时会产生大量无用数据的滞留以及数据的混杂。
    • 使用回调函数便完全浪费了 return 和 throw 关键字的能力。

    综上所述,我们需要一种更好的解决办法在某些(并不是全部)方面来取代回调(callback)方式的异步操作。

    3. Promise 能解决什么?

    • 编写大型应用时,一种高级、实用且能解决实际问题的高大上语法。
    • 避免层层嵌套的回调函数,将异步操作以同步操作的流程表达出来,使逻辑更加清晰。
    • Promise 提供统一的接口来进行异步操作。

    4. Promise 的特点

    Promise 是基于状态的,一个 Promise 对象表示了一个异步操作,而这个操作会有三种状态:Pending(进行中)、Resolved(已成功)和 Rejected(已失败)。只有异步操作的执行结果可以决定是哪一种状态,任何其它操作都无法改变。而且,一旦状态改变,就不会再变化。

    Promise 的异步操作不需要像回调一样执行异步操作后立即就会调用回调,Promise 允许任何时候都可以得到这个异步操作的结果。也就是说它其实和事件的机制是完全不同的,事件触发时,如果你没有处于监听状态,那么错过了就再也得不到此次事件的结果,而 Promise 则是将结果状态会凝固,等待你去观察这个异步操作的结果。

    5. Promise 的缺点

    一旦构造了 Promise 实例就代表执行了一个异步操作,也就是说它会立即执行,并且中途无法取消。

    如果不设置回调,Promise 内部抛出的错误,不会反应到外部。(有利有弊,具体看怎么用)

    Promise 其实只适合单一的异步程序,并不适合不断发生的事件处理,所以使用时,要找好最适合使用的场景。

    二. Promise API 解析

    1. Promise 构造方法

    Promise 本身就是个构造函数,用来生成 Promise 实例。

    Promise 构造函数接受一个函数作为参数,而该函数的两个参数分别为 resolve 和 reject,它们分别是两个函数,由 JavaScript 引擎提供。

    resolve 函数的作用是将 Promise 对象的状态从 Pending 变为 Resolved,在异步操作成功时调用,并将异步操作的结果作为参数传递出去。

    reject 函数的作用是将 Promise 对象的状态从 Pending 变为 Rejected,在异步操作失败时调用,并将异步操作失败所抛出的错误,作为参数传递出去。

    const promise = new Promise((resolve, reject) => {
      // 模拟一个异步操作
      setTimeout(function() {
        if ( /* 异步操作成功 */ ) {
          // 异步操作成功,携带载荷将状态变为 resolved
          resolve(payload);
        } else {
          // 异步操作失败,携带错误将状态变为 rejected
          reject(error);
        }
      }, 1000);
    });
    

    Promise 实例生成后,异步操作就已经开始执行了。

    要注意,resolve 和 reject 的调用是对 Promise 对象状态的变更和数据的传递,并不会影响函数的执行,所以 resolve 和 reject 后面如果有可以正常执行的流程代码,它们仍然会被正常执行。如果不想这样,可以使用 return 强制函数执行的结束。

    2. Promise 实例方法

    (1) Promise.prototype.then()

    Promise 实例生成以后,可以用 then 方法来指定 Resolved 状态和 Rejected 状态的回调函数

    promise.then((payload) => {
      // Resolved 时执行
    }, (error) => {
      // Rejected 时执行
    });
    

    Promise.prototype.then() 的两个参数都需要传递函数,代表着两个状态转变所要执行的回调,每个回调都可以接受 Promise 对象状态转变时传出的值作为参数。其中,then() 的第二个函数是可选的。

    回到一开始我们举的 “栗子”,现在,我想通过 Promise 的方式来实现多个异步程序的依次执行,我们可以在 then() 的调用中显式的返回一个新的 Promise 实例,然后就可以链式的且依次的执行 then(),看下面的代码:

    const promise = new Promise((resolve, reject) => {
      // 模拟异步程序 1:睡觉 1s
      setTimeout(() => {
        resolve('睡完觉了');
      }, 1000);
    });
    
    promise.then((val) => {
      console.log(val);
      return (new Promise((resolve, reject) => {
        // 模拟异步程序 2:吃饭 1s
        setTimeout(() => {
          resolve('吃完饭了');
        }, 1000);
      }));
    }).then((val) => {
      console.log(val);
      return (new Promise((resolve, reject) => {
        // 模拟异步程序 3:喝水 1s
        setTimeout(() => {
          resolve('喝完水了');
        }, 1000);
      }));
    }).then((val) => {
      console.log(val);
    });
    

    通过在 then() 的第一个回调函数中,返回新的 Promise 实例,我们可以用同步的流程将异步的操作表示出,相比使用 callback 更加的逻辑清晰。

    (2) Promise.prototype.catch()

    Promise.prototype.catch 方法是 .then(null, rejection) 的别名,用于指定发生错误时的回调函数,也可以捕获 then() 运行中所抛出的错误。

    一般来说,好的方式是不再 then() 里面指定 Rejected 的回调,而是使用 catch() 来对所有错误的捕获(包括 then() 里面抛出的错误)

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, 1000);
    });
    
    promise.then(() => {
      return temp + 2;
    }).catch((error) => {
      console.error(error);
    });
    
    // ReferenceError: temp is not defined
    

    3. Promise 类级别方法

    (1)Promise.all()

    Promise.all() 方法用于将多个 Promise 实例包装成一个新的 Promise 实例。

    Promise.all() 接受一个类数组(具有 Iterator)作为参数,每个成员应为 Promise 实例,如果不是,会调用 Promise.resolve() 方法将其转换为 Promise 实例。

    Promise.all() 返回的新的 Promise 实例的状态由传递的类数组成员的共同状态决定:

    • 当所有成员的状态都变为 Resolved,Promise 实例的状态才会变为 Resolved,此时所有成员的返回值组成一个数组,传递给 Promise 实例的回调函数
    • 只要有一个成员的状态变为 Rejected,Promise 实例的状态就变成 Rejected,此时第一个被 Rejected 的实例的返回值会传递给 Promise 实例的回调函数

    如果作为 Promise.all() 参数的 Promise 实例自己定义了 catch 方法,那么它的状态变为 Rejected 时,只会触发它自己的 catch(),不会触发 Promise.all() 的 catch()

    (2)Promise.race()

    Promise.race() 方法和 Promise.all() 几乎是一样的,只是对状态的处理存在差别。

    Promise.race() 返回的新的 Promise 实例的状态由传递的类数组成员中最先改变状态的成员的状态决定。

    (3)Promise.resolve()

    Promise.resolve() 将现有对象转换为 Promise 对象。

    Promise.resolve() 的参数分成四种情况:

    • Promise 实例:直接返回这个实例。
    • thenable 对象
      • thenable 对象指的是具有 then 方法的对象。
      • Promise.resolve() 会将这个对象转为 Promise 对象,然后就立即执行 thenable 对象的 then 方法,相当于将这个对象的 then 方法作为 Promise 构造函数的参数,返回一个新的 Promise 实例。
    • 其余情况
      • Promise.resolve() 返回一个新的 Promise 对象,状态为 Resolved,使用 then() 时,会将 Promise.resolve() 的参数传递给 then() 的回调函数中。
      • 这个立即 Resolved 的 Promise 对象,实在本次 “事件循环” 的结束时才开始执行。

    (4)Promise.reject()

    Promise.reject() 会返回一个新的 Promise 实例,状态为 Rejected。

    Promise.reject() 等价于下面的写法:

    Promise.reject(obj);
    // 等价于
    new Promise((resolve, reject) => reject(obj));
    

    Promise.reject() 会将参数原封不动的作为 reject() 的参数。

    三. Promise 深度剖析

    1. Promise 与 setTimeout

    我们来看这样的一段代码:

    setTimeout(() => {
      console.log(1);
    }, 0);
    
    (new Promise((resolve, reject) => {
      resolve();
    })).then(() => {
      console.log(2);
    });
    
    console.log(3);
    

    这段代码的运行结果的顺序是 3,2,1,原因如下:

    • 对于 setTimeout 我们应该都知道,它是放在下一轮 “事件循环” 的开始,所以它一定要在本轮事件结束后才会执行,也就是输出 1 一定要在输出 3 以后
    • 接下来就是 Promise 异步执行的问题,虽然 Promise 实例中并没有真正意义上的异步程序,而是直接将状态变更为 Resolved,且立即使用 then 进行状态的观察,但是实质上,立即 Resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务,所以,输出 2 一定要在输出 3 以后
    • 最后,就是 Promise 和 setTimeout 之间的问题,上面也说到了,setTimeout 是在下一轮事件的开始,而 Promise 又实在本一轮事件的结束,所以,很明显,输出 1 要在输出 2 以后

    2. 多层级 .then()

    一个 Promise 实例可以连续使用 .then() 来绑定回调函数:

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, 1000);
    });
    
    promise.then(() => {
      console.log(1);
    }).then(() => {
      console.log(2);
    });
    
    // 1
    // 2
    

    如果在 .then() 的回调中显式的指定一个返回值(非 Promise 实例),这个值会被作为下一个链式 .then() 回调函数中的参数:

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, 1000);
    });
    
    promise.then(() => {
      console.log(1);
      return 'Promise';
    }).then((val) => {
      console.log(val);
    });
    
    // 1
    // Promise
    

    如果在 .then() 的回调中返回的是一个 Promise 实例,那么下一个链式 .then() 会在这个 Promise 实例的状态变更时会被调用,并且 resolve 所传递的值会被作为下一个链式 .then() 回调函数中的参数:

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, 1000);
    });
    
    promise.then(() => {
      console.log(1);
      return (new Promise((resolve, reject) => {
        setTimeout(function() {
          resolve('Promise');
        }, 1000);
      }));
    }).then((val) => {
      console.log(val);
    });
    
    // 1
    // Promise
    

    相关文章

      网友评论

        本文标题:Promise 完全解读

        本文链接:https://www.haomeiwen.com/subject/eesrrxtx.html