• 一文了解Promise使用与实现


    前言

    Promise 作为一个前端必备技能,不管是从项目应用还是面试,都应该对其有所了解与使用。

    常常遇到的面试五连问:

    • 说说你对 Promise 理解?
    • Promise 的出现解决了什么问题?
    • Promise 有哪些状态?
    • 你用过 Promise 哪些方法?
    • 如何实现一个 Promise ?

    什么是 Promise?

    Promise 是异步编程的一种解决方案:从语法上讲,promise 是一个对象,从它可以获取异步操作的消息;从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。

    Promise 有三种状态:pending(等待态),fulfiled(成功态),rejected(失败态);状态一旦改变,就不会再变。创造 Promise 实例后,它会立即执行。

    一般来说我们会碰到的回调嵌套都不会很多,一般就一到两级,但是某些情况下,回调嵌套很多时,代码就会非常繁琐,会给我们的编程带来很多的麻烦,这种情况俗称——回调地狱。

    这时候我们的Promise 就应运而生、粉墨登场了。

    Promise 的基本使用

    Promise 是一个构造函数,自己身上有 allrejectresolve 这几个眼熟的方法,原型上有 thencatch 等同样很眼熟的方法。

    首先创建一个 new Promise 实例

    let p = new Promise((resolve, reject) => {
        // 可以做一些异步操作,例如发送AJAX请求,这里使用 setTimeout 模拟
        setTimeout(() => {
            let num = Math.ceil(Math.random() * 10); // 生成 1-10 随机数
            if (num <= 5) {
                resolve('成功');
            } else {
                reject('失败');
            }
        }, 2000);
    });
    

    Promise 的构造函数接收一个参数:函数,并且这个函数需要传入两个参数:

    resolve:异步操作执行成功后的回调函数;

    reject:异步操作执行失败后的回调函数;

    then 链式操作的用法

    p.then((data) => {
        console.log('resolve', data); // 'resolve', '成功'
    }, (err) => {
        console.log('reject', err); // 'reject', '失败'
    })
    

    then 中传了两个参数,then 方法可以接受两个参数,第一个对应 resolve 的回调,第二个对应reject 的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到两种结果。

    catch 的用法

    我们知道Promise 对象除了then 方法,还有一个catch 方法,它是做什么用的呢?其实它和then 的第二个参数一样,用来指定reject 的回调。用法是这样:

    p.then((data) => {
        console.log('resolve', data); // 'resolve', '成功'
    }).catch((err) => {
        console.log('reject', err);   // 'reject', '失败'
    });
    

    效果和写在 then 的第二个参数里面一样。不过它还有另外一个作用:在执行 resolve 的回调(也就是上面then 中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个 catch 方法中。请看下面的代码:

    p.then((data) => {
        console.log('resolve', data);
        console.log(test);
    }).catch((err) => {
        console.log('reject', err);
    });
    
    // 当成功时先后打印
    resolve 成功
    reject ReferenceError: test is not defined
    

    resolve 的回调中,我们 console.log(test),而 test 这个变量是没有被定义的。如果我们不用 Promise,代码运行到这里就直接在控制台报错了,不往下运行了,也就是说进到catch方法里面去了,而且把错误原因传到了 reject 参数中。即便是有错误的代码也不会报错了,这与我们的 try/catch 语句有相同的功能。

    finally 的用法

    finally 方法返回一个 Promise。在 promise 结束时,无论结果是 fulfilled 或者是 rejected ,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。

    这避免了同样的语句需要在 thencatch 中各写一次的情况。

    // 加载 loading
    
    let isLoading = true;
    
    // 假装模拟AJAX请求
    function myRequest(){
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                let num = Math.ceil(Math.random() * 10); // 生成 1-10 随机数
                if (num <= 5) {
                    resolve('成功');
                } else {
                    reject('失败');
                }
            }, 1000);
        })
    }
    
    myRequest().then(function(data) { console.log(data); })
      .catch(function(error) { console.log(error); })
      .finally(function() { 
          // 关闭loading
          isLoading = false;
          console.log('finally');
       });
    

    resolve 的用法

    Promise.resolve(value) 方法返回一个以给定值解析后的 Promise 对象。如果这个值是一个 promise ,那么将返回这个 promise ;如果这个值是 thenable(即带有"then" 方法),返回的promise会“跟随”这个 thenable 的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。

    // 示例 1 基本使用
    
    const p = Promise.resolve("Success");
    
    p.then(function(value) {
      console.log(value); // "Success"
    }, function(value) {
      // 不会被调用
    });
    
    // 示例 2 resolve 一个数组
    
    var p = Promise.resolve([1,2,3]);
    p.then(function(arr) {
      console.log(arr[0]); // 1
    });
    
    // 示例 3 resolve 另一个Promise
    
    let original = Promise.resolve(33);
    let cast = Promise.resolve(original);
    cast.then(function(value) {
      console.log('value: ' + value);
    });
    console.log('original === cast ? ' + (original === cast));
    
    /*
    *  打印顺序如下,这里有一个同步异步先后执行的区别
    *  original === cast ? true
    *  value: 33
    */
    
    
    
    // 示例 4 resolve thenable 并抛出错误
    
    // Resolve一个thenable对象
    
    const p1 = Promise.resolve({
      then: function(onFulfill, onReject) { onFulfill("fulfilled!"); }
    });
    console.log(p1 instanceof Promise) // true, 这是一个Promise对象
    
    p1.then(function(v) {
        console.log(v); // 输出"fulfilled!"
      }, function(e) {
        // 不会被调用
    });
    
    // Thenable在callback之前抛出异常
    
    // Promise rejects
    
    const thenable = { then: function(resolve) {
      throw new TypeError("Throwing");
      resolve("Resolving");
    }};
    
    const p2 = Promise.resolve(thenable);
    
    p2.then(function(v) {
      // 不会被调用
    }, function(e) {
      console.log(e); // TypeError: Throwing
    });
    
    // Thenable在callback之后抛出异常
    
    // Promise resolves
    
    const thenable = { then: function(resolve) {
      resolve("Resolving");
      throw new TypeError("Throwing");
    }};
    
    const p3 = Promise.resolve(thenable);
    
    p3.then(function(v) {
      console.log(v); // 输出"Resolving"
    }, function(e) {
      // 不会被调用
    });
    

    reject 的用法

    Promise.reject()方法返回一个带有拒绝原因的Promise对象。

    Promise.reject(new Error('fail')).then(function() {
      // not called
    }, function(error) {
      console.error(error); // Stacktrace
    });
    

    all 的用法

    谁跑的慢,以谁为准执行回调。all 接收一个数组参数,里面的值最终都算返回 Promise 对象。

    Promise.all 方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。看下面的例子:

    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            let num = Math.ceil(Math.random() * 10); // 生成 1-10 随机数
            if (num <= 5) {
                resolve('成功');
            } else {
                reject('失败');
            }
        }, 1000);
    });
    
    let pAll = Promise.all([p, p, p]);
    
    pAll.then((data) => {
        console.log(data); // 成功时打印: ['成功', '成功', '成功']
    }, (err) => {
        console.log(errs); // 只要有一个失败,就会返回当前失败的结果。 '失败'
    })
    

    有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很酷?有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。在这里可以解决时间性能的问题,我们不需要在把每个异步过程同步出来。

    all 缺点就是只要有一个任务失败就会都失败。

    allSettled 的用法

    Promise.allSettled ****方法返回一个在所有给定的 promise 都已经fulfilledrejected后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。

    当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。

    相比之下,Promise.all 更适合彼此相互依赖或者在其中任何一个reject时立即结束。

    const p1 = Promise.resolve(3);
    const p2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
    
    Promise.allSettled([p1, p2]).
      then((results) => results.forEach((result) => console.log(result.status)));
    
    // 执行后打印
    // "fulfilled"
    // "rejected"
    

    any 的用法

    Promise.any 接收一个 promise 可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise 。如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promiseAggregateError 类型的实例,它是 Error 的一个子类,用于把单一的错误集合在一起。本质上,这个方法和 Promise.all 是相反的。

    const pErr = new Promise((resolve, reject) => {
      reject("总是失败");
    });
    
    const pSlow = new Promise((resolve, reject) => {
      setTimeout(resolve, 500, "最终完成");
    });
    
    const pFast = new Promise((resolve, reject) => {
      setTimeout(resolve, 100, "很快完成");
    });
    
    Promise.any([pErr, pSlow, pFast]).then((value) => {
      console.log(value);  // '很快完成'
    })
    

    race 的用法

    Promise.race ****方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise就会解决或拒绝。也可理解 谁跑的快,以谁为准执行回调。

    race 的使用场景:比如我们可以用race 给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:

        // 假设请加某个图片资源
        function requestImg() {
            return new Promise((resolve, reject) => {
                let img = new Image();
                img.onload = function() {
                    resolve(img);
                }
                // 尝试输入假的和真的链接
                img.src = '**';
            })
        }
        // 延时函数,用于给请求计时
        function timeout() {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    reject('请求超时');
                }, 5000)
            })
        }
    
        Promise.race([requestImg(), timeout()]).then((data) => {
            console.log(data); // 成功时 img
        }).catch((err) => {
            console.log(err);  // 失败时 "请求超时"
        })
        
    

    如何实现一个Promise

    1、创建一个 MyPromise 类,传入 executor(执行器)并添加 resolve 和 reject 方法

    
    class MyPromise {
      constructor(executor){
        // executor 是一个执行器,进入会立即执行
        // 并传入resolve和reject方法
        executor(this.resolve, this.reject) 
      }
      
      // 更改成功后的状态
      resolve = () => {}
      // 更改失败后的状态
      reject = () => {}
    }
    

    2、添加状态和 resolve、reject 事件处理

    // 定义三种状态
    const PENDING = 'pending';
    const FULFILLED = 'fulfilled';
    const REJECTED = 'rejected';
    
    class MyPromise {
      constructor(executor){
        executor(this.resolve, this.reject)
      }
    
      // 储存状态,初始值是 pending
      status = PENDING;
    
      // 成功之后的值
      value = null;
      
      // 失败之后的原因
      reason = null;
    
      // 更改成功后的状态
      resolve = (value) => {
        if (this.status === PENDING) {
          this.status = FULFILLED;
          this.value = value;
        }
      }
    
      // 更改失败后的状态
      reject = (reason) => {
        if (this.status === PENDING) {
          this.status = REJECTED;
          this.reason = reason;
        }
      }
    }
    
    

    3、.then 的实现

    then(onFulfilled, onRejected) {
      if (this.status === FULFILLED) {
        onFulfilled(this.value); // 调用成功回调,并且把值返回
      } else if (this.status === REJECTED) {
        onRejected(this.reason); // 调用失败回调,并且把原因返回
      }
    }
    
    

    上面三步已经简单实现了一个 Promise我们先来调用试试:

    const promise = new MyPromise((resolve, reject) => {
       resolve('success')
       reject('err')
    })
    
    promise.then(value => {
      console.log('resolve', value)
    }, reason => {
      console.log('reject', reason)
    })
    
    // 打印结果:resolve success
    

    经过测试发现如果使用异步调用就会出现顺序错误那么我们怎么解决呢?

    4、实现异步处理

    // 定义三种状态
    const PENDING = 'pending';
    const FULFILLED = 'fulfilled';
    const REJECTED = 'rejected';
    
    class MyPromise {
        constructor(executor) {
            executor(this.resolve, this.reject)
        }
    
        // 成功存放的数组
        onResolvedCallbacks = [];
        // 失败存放法数组
        onRejectedCallbacks = [];
        // 储存状态,初始值是 pending
        status = PENDING;
        // 成功之后的值
        value = null;
        // 失败之后的原因
        reason = null;
    
        // 更改成功后的状态
        resolve = (value) => {
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                this.onRejectedCallbacks.forEach((fn) => fn()); // 调用成功异步回调事件
            }
        }
    
        // 更改失败后的状态
        reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                this.onRejectedCallback.forEach((fn) => fn());  // 调用失败异步回调事件
            }
        }
        then(onFulfilled, onRejected) {
            if (this.status === FULFILLED) {
                onFulfilled(this.value); //调用成功回调,并且把值返回
            } else if (this.status === REJECTED) {
                onRejected(this.reason); // 调用失败回调,并且把原因返回
            } else if (this.status === PENDING) {
                // onFulfilled传入到成功数组
                this.onResolvedCallbacks.push(() => {
                    onFulfilled(this.value);
                })
                // onRejected传入到失败数组
                this.onRejectedCallbacks.push(() => {
                    onRejected(this.reason);
                })
            }
        }
    }
    
    

    修改后通过调用异步测试没有

    const promise = new MyPromise((resolve, reject) => {
      setTimeout(() => {
        resolve('success')
      }, 2000); 
    })
    
    promise.then(value => {
      console.log('resolve', value)
    }, reason => {
      console.log('reject', reason)
    })
    
    // 等待 2s 输出 resolve success
    

    但是如果使用链式调用 .then 就会发现有问题,而原生的 Promise 是支持的。 那么我们如何支持呢?

    链式调用示例:

    const promise = new MyPromise((resolve, reject) => {
      resolve('success')
    })
    
    function other () {
      return new MyPromise((resolve, reject) =>{
        resolve('other')
      })
    }
    promise.then(value => {
      console.log(1)
      console.log('resolve', value)
      return other()
    }).then(value => {
      console.log(2)
      console.log('resolve', value)
    })
    // 第二次 .then 将会失败
    

    5、实现 .then 链式调用

    class MyPromise {
        ...
    
        then(onFulfilled, onRejected) {
            // 为了链式调用这里直接创建一个 MyPromise,并 return 出去
            return new MyPromise((resolve, reject) => {
                if (this.status === FULFILLED) {
                    const x = onFulfilled(this.value);
                    resolvePromise(x, resolve, reject);
                } else if (this.status === REJECTED) {
                    onRejected(this.reason);
                } else if (this.status === PENDING) {
                    this.onFulfilledCallbacks.push(onFulfilled);
                    this.onRejectedCallbacks.push(onRejected);
                }
            })
        }
    }
    
    
    function resolvePromise(x, resolve, reject) {
        // 判断x是不是 MyPromise 实例对象
        if (x instanceof MyPromise) {
            // 执行 x,调用 then 方法,目的是将其状态变为 fulfilled 或者 rejected
            x.then(resolve, reject)
        } else {
            resolve(x)
        }
    }
    

    6、也可以加入 try/catch 容错

        ...
        constructor(executor) {
            try {
                executor(this.resolve, this.reject)
            } catch(error) {
                this.reject(error)
            }
        }
    
    

    最后

    本文就先到这里了,后续有时间再补充 .alL.any 等其他方法的实现。

    相关推荐

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

    作者:雨中愚
    链接:https://juejin.cn/post/6995923016643248165
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    java注解,通过反射解析注解,模仿hibernate,获取sql语句。
    Eclipse/Myeclipse中查看和调试JDK源代码的方法
    TCP为什么会出现 RST
    《浅谈F5健康检查常用的几种方式》—那些你应该知道的知识(二)
    负载均衡服务TCP端口健康检查成功,为什么在后端业务日志中出现网络连接异常信息?
    haproxy的丰富特性简介
    健康检查概述
    firewall防火墙常用操作
    gitlab修改默认端口
    vim脚本判断操作系统
  • 原文地址:https://www.cnblogs.com/yuzhongyu/p/15291293.html
Copyright © 2020-2023  润新知