深度学习 Promise 对象

179 阅读7分钟

浅谈 Promise 对象

一、介绍

说到 Promise 相信很多前端工作者都不会陌生,但是很多人用它,却对其原理模糊不清,在深度学习了 Promise 原理后,决定浅谈一下这个神秘对象内部是如何实现的? 又解决了哪些问题?

Promise 规范有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的升级版 Promise/A+,而 ES6 中采用了 Promise/A+ 规范。说明对这套规范的认可度很高,本文也将围绕这套规范展开详说。

二、初识 Promise

Promise 的诞生是为了解决异步编程问题,最早由社区提出并实现,ES6 将其写进了语言标准,统一了用法,随之由原生推出了 Promise 对象。

阮神的ES6入门中说:Promise,简单来讲就是一个容器,里面保存着某种未来才会结束的事件,白话就是:一个处理异步操作的结果的容器;从语法层面来讲,Promise 是一个对象,它可以获取异步操作的消息。Promise 提供统一的API,各种异步操作都可以用同样的方法进行处理。

三、Promise 对象的两个特点

1.状态改变不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Fulfilled(已成功)和Rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这个状态。

2.状态一旦改变就不会再变,任何时候都可以得到这个状态的结果。而状态也只可能是从 Pending 变为 Fulfilled 或者从 Pending 变为 Rejected,之后的状态不会再改变,这个状态称为 Resolved(已定型)

四、Promise 使用场景

为了更接近开发中使用的场景,我们从一段代码开始分析 Promise 的作用。

//不使用Promise        
http.get('url', function (result) {
    //do something
    console.log(result.id);
});

//使用Promise
new Promise(function (resolve) {
    //异步请求
    http.get('url', function (result) {
        resolve(result.id)
    })
}).then(function (id) {
    //do something
    console.log(id);
})

对比来看似乎不使用 Promise 代码更简洁易懂,但是场景稍微复杂,我们就能看出一个很明显的问题,那就是回调地狱。

//不使用Promise        
http.get('url', function (id) {
    //do something
    http.get('getNameById', id, function (name) {
        //do something
        http.get('getCourseByName', name, function (course) {
            //dong something
            http.get('getDetailByCourse', function (Detail) {
                //do something
            })
        })
    })
});


//使用Promise
function getUserId(url) {
    return new Promise(function (resolve) {
        //异步请求
        http.get(url, function (id) {
            resolve(id)
        })
    })
}
getUserId('some_url').then(function (id) {
    //do something
    return getNameById(id); // getNameById 是和 getUserId 一样的Promise封装。下同
}).then(function (name) {
    //do something
    return getCourseByName(name);
}).then(function (course) {
    //do something
    return getDetailByCourse(course);
}).then(function (courseDetail) {
    //do something
});

其实细心的小伙伴已经发现了,Promise 也还是使用回调函数,只不过是把回调封装在了内部,使用上一直通过 then 方法的链式调用,使得多层的回调嵌套看起来变成了同一层的,书写上以及理解上会更直观和简洁一些。没有多层嵌套,变得利于阅读了一点。

来看一个基础版本:

//极简的实现
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
    }
    _resolve(value) {
        this.callbacks.forEach(fn => fn(value));
    }
}

//Promise应用
let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
}).then((tip) => {
    console.log(tip);
})

这个版本的逻辑就是:

调用 then 方法,将想要在 Promise 异步操作成功时执行的 onFulfilled 放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考;

创建 Promise 实例时传入的函数会被赋予一个函数类型的参数,即 resolve,它接收一个参数 value,代表异步操作返回的结果,当异步操作执行成功后,会调用resolve方法,这时候其实真正执行的操作是将 callbacks 队列中的回调一一执行;

首先 new Promise 时,传给 Promise 的函数设置定时器模拟异步的场景,接着调用 Promise 对象的 then 方法注册异步操作完成后的 onFulfilled,最后当异步操作完成时,调用 resolve(value), 执行 then 方法注册的 onFulfilled。

then 方法注册的 onFulfilled 是存在一个数组中,可见 then 方法可以调用多次,注册的多个onFulfilled 会在异步操作完成后根据添加的顺序依次执行。如下:

//then 的用法
let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('2s');
    }, 2000);
});

p.then(res => {
    console.log('then1', res);
});

p.then(res => {
    console.log('then2', res);
});

上例中,要先定义一个变量 p ,然后 p.then 两次。而规范中要求,then 方法应该能够链式调用。实现也简单,只需要在 then 中 return this 即可。如下所示:

//简单的实现+链式调用
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
        return this;  // Look
    }
    _resolve(value) {
        this.callbacks.forEach(fn => fn(value));
    }
}

let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('2s');
    }, 2000);
});

p.then(res => {
    console.log('then1', res);
});

p.then(res => {
    console.log('then2', res);
});

五、异常处理

异常通常是指在执行成功/失败回调时代码出错产生的错误,对于这类异常,我们使用 try-catch 来捕获错误,并将 Promise 设为 rejected 状态即可。

function handle(callback){
  if(state === 'pending'){ 
  callbacks.push(callback) 
   return; 
   
   const cb = state === 'fulfilled' ? callback.onFulfilled:callback.onRejected; 
   const next = state === 'fulfilled'? callback.resolve:callback.reject;

    if(!cb){ 
    next(value) return; 
    } try { 
    const ret = cb(value) next(ret) 
    } catch (e) {
    callback.reject(e);
    }
}

更多是习惯注册 catch 方法来处理错误,例:

new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ test: 1 })
}, 1000)
}).then((data) => {
console.log('result1', data)
//dosomething
return test()
}).catch((ex) => {
console.log('error', ex)
})

实际上,错误也好,异常也罢,最终都是通过reject实现的。也就是说可以通过 then 中的错误回调来处理。所以我们可以增加这样的一个 catch 方法:

function Promise(fn){
... 
this.then = function (onFulfilled,onRejected){
return new Promise((resolve, reject)=>{
       handle({
       onFulfilled,
       onRejected,
       resolve,
       reject 
          }) 
       })
      }
this.catch = function (onError){ this.then(null,onError)
}
... 
}

六、resolve 方法和 reject 方法

实际应用中,我们可以使用 Promise.resolve 和 Promise.reject 方法,用于将于将非 Promise 实例包装为 Promise 实例。如下例子:

Promise.resolve({name:'winty'})
Promise.reject({name:'winty'})
// 等价于
new Promise(resolve => resolve({name:'winty'}))
new Promise((resolve,reject) => reject({name:'winty'}))

这些情况下,Promise.resolve 的入参可能有以下几种情况:

  • 无参数 [直接返回一个resolved状态的 Promise 对象]
  • 普通数据对象 [直接返回一个resolved状态的 Promise 对象]
  • 一个Promise实例 [直接返回当前实例]
  • 一个thenable对象(thenable对象指的是具有then方法的对象) [转为 Promise 对象,并立即执行thenable对象的then方法。]

基于以上几点,我们可以实现一个 Promise.resolve 方法如下:

function Promise(fn){ 
        ...
        this.resolve = function (value){
            if (value && value instanceof Promise) {
                return value;
            } else if (value && typeof value === 'object' && typeof value.then === 'function'){
                let then = value.then;
                return new Promise(resolve => {
                    then(resolve);
                });
            } else if (value) {
                return new Promise(resolve => resolve(value));
            } else {
                return new Promise(resolve => resolve());
            }
        }
        ...
    }

Promise.reject与Promise.resolve类似,区别在于Promise.reject始终返回一个状态的rejected的Promise实例,而Promise.resolve的参数如果是一个Promise实例的话,返回的是参数对应的Promise实例,所以状态不一 定。

因此,reject 的实现就简单多了,如下:

function Promise(fn){ 
        ...
        this.reject = function (value){
            return new Promise(function(resolve, reject) {
                reject(value);
            });
        }
        ...
    }

入参是一个 Promise 的实例数组,然后注册一个 then 方法,然后是数组中的 Promise 实例的状态都转为 fulfilled 之后则执行 then 方法。这里主要就是一个计数逻辑,每当一个 Promise 的状态变为 fulfilled 之后就保存该实例返回的数据,然后将计数减一,当计数器变为 0 时,代表数组中所有 Promise 实例都执行完毕。

function Promise(fn){ 
        ...
        this.all = function (arr){
            var args = Array.prototype.slice.call(arr);
            return new Promise(function(resolve, reject) {
                if(args.length === 0) return resolve([]);
                var remaining = args.length;

                function res(i, val) {
                    try {
                        if(val && (typeof val === 'object' || typeof val === 'function')) {
                            var then = val.then;
                            if(typeof then === 'function') {
                                then.call(val, function(val) {
                                    res(i, val);
                                }, reject);
                                return;
                            }
                        }
                        args[i] = val;
                        if(--remaining === 0) {
                            resolve(args);
                        }
                    } catch(ex) {
                        reject(ex);
                    }
                }
                for(var i = 0; i < args.length; i++) {
                    res(i, args[i]);
                }
            });
        }
        ...
    }

七、Promise.all()方法

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

const p = Promise.all([p1, p2, p3]);

面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1、p2、p3决定,分成两种情况。

  • 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
let promise = Promise.all([1, 2, 3]);
promise.then(value => {
 console.log(value); // [1,2,3]
});
console.log(promise);

情景一:promise结果

image.png

let p2 = Promise.reject(2);
let promise = Promise.all([1, p2, 3]);
promise
 .then(value => {
 console.log(value);
})
 .catch(err => {
 console.log(err); // 2
});
console.log(promise);

情景二:promise结果

image.png

如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法

const p1 = new Promise(resolve => {
 resolve("hello");
})
.then(result => result)
.catch(e => e);
 
const p2 = new Promise(() => {
 throw new Error("报错了");
})
.then(result => result)
.catch(e => e); // p2实际上是catch返回的promise实例
Promise.all([p1, p2])
 .then(result => console.log(result))
 .catch(e => console.log(e));
Promise.race()

总结

看似复杂的Promise背后其实推敲也能分析出其中的原理,分析每一个步的执行过程,想一下它存在的作用,解决什么问题,用于什么场景,最关键的点就是要理解 then 函数 ,它负责注册回调,真正的执行是在Promise的状态被改变之后,而当reslove的入参是一个Promise时,想要链式调用起来,就必须调用其 then 方法 then.call 将上一个Promise方法注入其回调数组中。