ES6 Promise原理总结

1,177 阅读6分钟

概要

  • 学习一门技术,最好的方式就是先了解这门技术是如何诞生的,及它解决了什么问题?
  • 接下来将从一下几个方面介绍Promise
    • 异步编程的问题:代码逻辑不连续;
    • 回调地狱:嵌套了太多的回调函数;
    • Promise:消灭嵌套调用;
    • Promise:合并多个任务的错误处理;
    • Promise与微任务的关系;
  • 首先明确一下,Promise解决的是异步编码风格的问题,而不是一些其他的问题

异步编程的问题:代码逻辑不连续

  • 假设有一个请求,使用XMLHttpRequest来实现,代码如下:
    // 执行状态
    function onResolve(response) { console.log(response); }
    function onReject(error) { console.log(error); }
    
    let xhr = new XMLHttpRequest();
    xhr.ontimeout = function(e) { onReject(e); };
    xhr.onerror = function(e) { onReject(e); };
    xhr.onreadystatechange = function () { onResolve(xhr.response); };
    
    // 设置请求类型,请求URL,是否同步信息
    let URL = 'https://localhost:8080/getList';
    xhr.open('Get', URL, true);
    
    // 设置参数
    xhr.timeout = 3000; // 设置xhr请求的超时时间
    xhr.responseType = "text"; // 设置响应返回的数据格式
    xhr.setRequestHeader("X_TEST","time.geekbang");
    
    // 发出请求
    xhr.send();
    
    • 可见上述这短短的一段代码里面竟然出现了五次回调,这么多的回调会导致代码的逻辑不连贯、不线性,非常不符合人的直觉,这就是异步回调影响到我们的编码方式

回调地狱:嵌套了太多的回调函数

  • 当然也可以把上述的异步代码封装成一个函数,调用的时候传入相应的请求参数、回调函数,就可以让处理流程变得线性,代码如下:

    // request,请求信息,请求头,延时值,返回类型等
    // resolve, 执行成功,回调该函数
    // reject  执行失败,回调该函数
    function XFetch(request, resolve, reject) {
        let xhr = new XMLHttpRequest();
        xhr.ontimeout = function (e) { reject(e); };
        xhr.onerror = function (e) { reject(e); };
        xhr.onreadystatechange = function () {
            if (xhr.status = 200)
                resolve(xhr.response);
        };
    
        xhr.open(request.method, request.url, request.sync);
        xhr.timeout = request.timeout;
        xhr.responseType = request.responseType;
        // 补充其他请求信息
        // ...
    
        xhr.send();
    }
    
    // 调用封装的异步代码
    XFetch(
        { method: 'GET', url: 'https://localhost:8080/getList', sync: true },
        function resolve(data) {
            console.log(data);
        }, 
        function reject(e) {
            console.log(e);
        }
    );
    
  • 封装异步代码在一些简单逻辑下可以满足需求,但如果逻辑复杂一点,会产生嵌套很多回调函数,从而陷入了回调地狱,代码如下:

    XFetch(
        { method: 'GET', url: 'https://localhost:8080/getList', sync: true },
        function resolve(response) {
          console.log(response);
          XFetch(
              { method: 'GET', url: 'https://localhost:8080/getList1', sync: true },
              function resolve(response) {
                  console.log(response);
                  XFetch(
                      { method: 'GET', url: 'https://localhost:8080/getList2', sync: true },
                      function resolve(response) {
                          console.log(response);
                      },
                      function reject(e) {
                          console.log(e);
                      })
              },
              function reject(e) {
                  console.log(e);
              })
        }, 
        function reject(e) {
          console.log(e);
        }
    );
    
  • 上述代码看上去很乱,归根结底有两点原因:

    • 嵌套调用:下一个任务依赖上一个任务的请求结果,并在上一个任务的回调函数内部执行新的业务逻辑,当嵌套层次多了之后,代码的可读性就变得非常差;
    • 任务的不确定性:执行每个任务都有两种可能的结果(成功或者失败),所以对每个任务都要进行一次额外的错误处理的方式,其明显增加了代码的混乱程度;
  • 原因分析出来后,那么就需要解决这两个问题:

    • 消灭嵌套调用
    • 合并多个任务的错误处理
  • ES6引入Promise,就是为了解决上述这两个问题,下面具体介绍;

Promise:消灭嵌套调用

Promise主要通过下面两步解决嵌套回调问题:

  • Promise实现了回调函数的延时绑定

    // 创建Promise对象promise1,并在executor函数中执行业务逻辑
    function executor(resolve, reject){
        resolve(100);
    }
    let promise1 = new Promise(executor);
    
    // promise1延迟绑定回调函数onResolve
    function onResolve(value){
        console.log(value);
    }
    promise1.then(onResolve);
    
    • 如上所示:
      • 回调函数的延时绑定在代码上体现就是先创建Promise对象promise1
      • 通过Promise的构造函数executor来执行业务逻辑;
      • 创建好Promise对象promise1之后,再使用promise1.then()来设置回调函数;
    • 总之,Promise实现回调函数的延时绑定,能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数
  • Promise将回调函数onResolve的返回值穿透到最外层

    // 创建Promise对象promise1,并在executor函数中执行业务逻辑
    function executor(resolve, reject) {
        resolve(100);
    }
    const promise1 = new Promise(executor);
    
    // promise1延迟绑定回调函数onResolve
    function onResolve(value) {
        console.log(value);
    
        function executor2(resolve, reject) {
            resolve(value + 1);
        }
        return new Promise(executor2);
    }
    
    // promise2为内部返回值穿透到了最外层
    const promise2 = promise1.then(onResolve);
    
    promise2.then((value) => {
        console.log(value);
    });
    
    • 如上所示,onResolve函数内部创建好的Promise对象返回到最外层,这样就可以摆脱嵌套循环了

Promise:合并多个任务的错误处理

function executor(resolve, reject) {
    let rand = Math.random();
    console.log(1);
    console.log(rand);
    if (rand > 0.5)
        resolve();
    else
        reject();
}
var p0 = new Promise(executor);

var p1 = p0.then((value) => {
    console.log("succeed-1");
    return new Promise(executor);
});

var p3 = p1.then((value) => {
    console.log("succeed-2");
    return new Promise(executor);
});

var p4 = p3.then((value) => {
    console.log("succeed-3");
    return new Promise(executor);
});

p4.catch((error) => {
    console.log("error");
})
console.log(2);
  • 上述代码,链式调用了四个Promise对象:p0~p4无论哪个对象里面抛出异常,都可以通过最后一个对象p4.catch来捕获异常
  • 通过这种方式可以将所有Promise对象的错误合并到一个函数来处理,这样就解决了每个任务都需要单独处理异常的问题;
  • 之所以可以使用最后一个对象来捕获所有异常,是因为Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被onReject函数处理或catch语句捕获为止
  • 具备了这样“冒泡”的特性后,就不需要在每个Promise对象中单独捕获异常了;

Promise与微任务的关系

function executor(resolve, reject) {
    resolve(100)
}
let demo = new Promise(executor)

function onResolve(value){
    console.log(value)
}
demo.then(onResolve)
  • 如上代码:

    • 首先执行new Promise时,Promise的构造函数会被执行;
    • 接下来,Promise的构造函数会调用Promise的参数executor函数;
    • 然后在executor中执行了resolve
    • 执行resolve函数,会触发demo.then设置的回调函数onResolve
      • 所以可以推测,resolve函数内部调用了通过demo.then设置的onResolve函数;
  • 注:由于Promise采用了回调函数延迟绑定技术,所以在执行resolve函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行

  • 为了方便理解,这里实现了一个简单的Promise对象:

    function Bromise(executor) {
        var onResolve_ = null;
        var onReject_ = null;
         //模拟实现resolve和then,暂不支持rejcet
        this.then = function (onResolve, onReject) {
            onResolve_ = onResolve;
        };
        function resolve(value) {
              //setTimeout(()=>{
                onResolve_(value);
               // },0)
        }
        executor(resolve, null);
    }
    
  • 调用上述定义的Bromise对象:

    function executor(resolve, reject) {
        resolve(100);
    }
    // 调用Bromsie
    let demo = new Bromise(executor);
    
    function onResolve(value) {
        console.log(value);
    }
    demo.then(onResolve);
    
    • 这时会报错,因为在执行executor函数的时候,还没有通过demo.then()设置回调函数,Bromise中的onResolve_还为空,所以就报错了;
  • 这时就需要改造Bromise中的resolve方法,让resolve延迟调用onResolve_

    • 要让resolve中的onResolve_ 函数延后执行,可以在resolve函数里面加上一个定时器,让其延时执行onResolve_函数,代码如下:
      function Bromise(executor) {
          var onResolve_ = null;
          var onReject_ = null;
          // 模拟实现resolve和then,暂不支持rejcet
          this.then = function (onResolve, onReject) {
              onResolve_ = onResolve;
          };
          function resolve(value) {
                // 使用setTimeout定时器来延时`onResolve_`函数的执行
                setTimeout(() => {
                  onResolve_(value);
                }, 0);
          }
          executor(resolve, null);
      }
      
    • 上面采用了定时器来推迟onResolve的执行,不过使用定时器的效率并不是太高;
  • 所以Promise又把这个定时器改造成了微任务了,这样既可以让onResolve_延时被调用,又提升了代码的执行效率,这就是Promise中使用微任务的原因

参考原文:
极客时间:Promise:使用Promise,告别回调函数
大白话讲解Promise(一)