浅析JaveScript中的Promise对象

浅析JaveScript中的Promise对象

前言

本文旨在简单讲解一下javascript中的Promise对象的概念,特性与简单的使用方法。并在文末会附上一份符合PromiseA+规范的Promise对象的完整实现。

注:本文中的相关概念均基于PromiseA+规范。

相关参考

JavaScript Promise迷你书 Promise/A+规范

正文

1.Promise简介

在了解javescript中的Promise实现之前有必要先了解一下Promise的概念。


什么是Promise?


关于Promise概念的解释,网上的各种资料众说纷纭,这里奉上笔者自己的理解。简单来说,Promise就是一套处理异步事件的方式和流程。promise在英文中的含义是约定,而针对异步事件特性的处理方式与这个含义非常吻合。


为什么要使用Promise?


一个异步事件不会立刻返回结果,这时我们就需要预先规定一些操作,等待异步事件返回结果后,再去使用某种方式让预先规定的操作执行。在javascript的习惯中,我们常用回调函数(callback)去实现上述过程。下面是一个简单的示例:

例1
let asyncFunc = function(callback){
    let num = 100;
    setTimeout(function(){
        num += 100;
        callback(num);
    },2000);
};

function foo(value){
    console.log(value);  //value => 200
}

asyncFunc (foo);

上面就是一个简单的异步操作处理过程,asyncFunc就是一个异步的函数,执行后通过setTimeout方法在2秒返回了一个值,而foo则是一个回调函数,通过传入异步函数并且在返回结果后被调用的方式获取异步操作的结果。这里的回调函数就如同一个事先的约定,在异步操作返回结果后立即被实现。

那么,既然js中已经有处理异步事件的方法,为何还要引入Promise这个新的方式呢?实际上,上面这段代码只是简单展示下回调函数的基础使用,而在真正的使用场景中,我们不得不面对各种十分复杂的局面。通常在一个异步操作返回结果后执行的回调中还要进行另一个异步操作,而同一个异步操作返回结果后要执行的回调函数可不止一个。数个异步操作与回调函数彼此嵌套,时刻挑战者维护和使用者的神经。下面是一个彼此嵌套的例子:

例2
ajax(url1,function(value1){
    foo(value1);
    bar();
});
function foo(value){
    ajax(url2,function(value2){
        do something..
        ajax(url3,function(value3){
            ...
        })
    });
}
function bar(){ do something.. };

上面的例子模拟了一个js中一个常用的异步操作:发送ajax请求数据。在url1请求的回调中使用了foo和bar两个函数,而foo中又发送了url2,url3的请求。。。这样数层嵌套下来,最终导致代码非常的不直观,维护起来难度也直线上升,形成常说的“回调地狱”。

了解了传统上js处理异步操作的复杂和困难后,我们不禁思索,是否有方法能够更加简洁,直观的去解决异步操作的种种问题?答案就是我们这篇文章的主角:Promise。


2. Promise的特性及使用

在PromiseA+规范中做出了这样定义:promise是一个包含了兼容Promise规范then方法的对象或函数,与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因。
这段定义有两个重点:1.Promise是一个对象或函数 2.它有一个then方法,能够获取prmose的最终结果。下面我们就来实际看一下Promise到底是如何处理异步事件的,我们将上面的例1使用Promise进行一下改写:

例3
let p = new Promise(function(resolve,reject){
    let value = 100;
    setTimeout(function(){
        value += 100;
        resolve(value);
    },2000);
});
p.then(function(value){
    console.log(value);      //value => 200
},function(err){
    do something...
});

初看之下其实并没有太大区别,但实际上Promise的威力在更复杂的场景下才能更好的发挥。我们先针对这个简单的例子来讲解下Promise的使用

首先通过 new 关键字实例化一个Promise对象,在这个对象中传入一个要执行异步操作的函数。这个函数包含两个形参:resolve和reject。这两个形参是Promise中定义的2个函数,分别在异步事件成功和失败时调用。例3中我们在2秒后调用了resolve函数,代表着异步事件成功,返回一个值。而在我们实例化Promise对象的同时,我们又调用了这个实例的then方法。then方法可以说是Promise方法中的核心,它即代表着Promise约定的这层含义,在then方法中接收2个函数作为参数,分别在异步事件成功时或失败时执行,并且两个函数的参数正是异步事件成功时返回的值或失败时原因。

其实,使用Promise对象来处理异步事件比起使用传统的回调函数的一个优点在于:Promise规范了处理异步事件的流程。我们不必再深入异步事件的内部,去分析种种状态变化后对应的回调究竟如何调用,也不必过多考虑异步事件内部发生错误时该如何捕获,我们只需要在合适的时候通知Promise返回成功或失败状态,剩下的事统统交给Promise去解决。

以上我们大致了解了Promise的处理流程,在详细讲解Promise对象中的方法之前有必要先了解一下Promise的状态概念。

一个Promise对象在实例化后可能拥有以下3种状态的其中之一:

Fulfilled - 当传入的异步事件成功返回值时的状态

Rejected - 当传入的异步事件失败或产生异常时的状态

Pending - 当传入的异步事件还没有结果返回时的状态


注意,任何时候Promise对象都只能处于以上其中状态的一种,当Promise对象处于Pending状态时,它可以转化成Fulfilled 或Rejected 状态,而当Promise对象处于Fulfilled 或Rejected状态时,它不能再转化成其他状态。

可以用一张图来直白的表示上面这段话

(图片取自Promise迷你书)

在了解了Promise的三种状态后 ,接下来可以详细了解下Promise对象的几个方法


resolve()


resolve方法是在一个Promise对象实例化时传入的任务函数的第一个参数,它的作用是让Promise进入“Fulfilled ”状态,resolve方法只接受一个参数,即异步事件的返回值value。


reject()


reject方法与resolve方法正好相反,它是在一个Promise对象实例化时传入的任务函数的第二个参数,它的作用是让Promise进入“Rejected”状态,reject方法同样只接受一个参数,即异步事件失败或异常的原因reason。


Promise.prototype.then()


then方法是Promise对象方法的重中之重,它是Promise实例的方法,用来注册Promise对象成功时执行的回调函数(onFulfilled)和失败时执行的回调函数(onRejected)。一个then方法的返回值仍然是一个Promsie对象。因此,then方法支持链式调用,也就是一个一个then方法的返回值可以继续调用then。而相链接的then方法中,在上一个then方法的onFulfilled或onRejected回调函数中通过 return value(reason)的方式,把这个结果作为下一个then中的回调函数的参数被接收。onFulfilled和onRejected函数的返回值可以是任何javascript值,甚至一个Promise对象的成功或失败时的回调函数可以返回一个新的Promise对象。这样的特性使得例2中那种复杂的异步事件嵌套的场景处理得以简化。下面是使用Promise来重写的例2:

例4
let p1 = new Promise(function(resolve,reject){
    ajax(url1,function(value1){
        resolve(value1);
    });
});

p1.then(function(value1){
    return new Promise(function(resolve,reject){
        ajax(url2,function(value2){
            do something..
            resolve(value2);
        });
    })
}).then(function(value2){
    return new Promise(function(resolve,reject){
        ajax(url3,function(value3){
            ...
        });
    })
});
p1.then(bar);
function bar(){do something...};

可以看出,使用Promise改写后的代码结构上更加清晰,它把层层嵌套的函数转化成链式的调用then方法的形式,这样可以非常清晰的看出事件间的关系和执行顺序,大大降低了日后代码使用和维护的难度。

关于then方法还有几点补充:

1. then方法中的onFulfilled和onRejected方法都是可以省略的。

2. 当一个Promise失败返回了reason,而then方法中没有定义onRejected函数时,这个reason会被链式调用的下一个then方法的onRejected方法接收。

3. 一个Promise实例可以调用多次then方法,这些then注册的onFulfilled和onRejected函数会按照注册的顺序执行。

Promise.prototype.catch()

catch方法是一个then方法的语法糖,它只接受一个失败处理函数onRejected,实际上等同于以下代码:

new Promsie.then(null,function(){
    do something...
})


Promise.all()


all方法是Promsie对象的静态方法,使用方式是 Promise.all()。all方法接收的参数为一个包含数个Promise对象实例的数组,并返回一个新的Promise实例。当数组中所有的Promse实例都返回结果后,将所有数组中的Promise实例的成功返回值传入一个数组,并将这个数组注入到all方法返回的新实例的then方法中。下面是一个all方法的使用实例:

例5
let promiseArr = [
    new Promise(function(resolve,reject){
        setTimeout(function(){
            resolve(100)
        },1000)
    }),
    new Promise(function(resolve,reject){
        setTimeout(function(){
            resolve(200)
        },500)
    })
]
Promise.all(promiseArr).then(function(valArr){
    console.log(valArr)     // valArr  => [100,200]
},function(err){
    do something...
})

all方法值得注意的有两点:

1.数组中所有promise实例都成功后的返回值,在valArr中的顺序是按照promiseArr 中promise实例的顺序来排列的。

2.当任何一个promise失败后,all方法直接将返回的Promise对象的状态变为Rejected,并调用then方法的onRejected函数,把失败的原因传递出来。


Promise.resolve()


Promsie对象本身存在一个resolve方法,它的作用是立刻返回一个状态为Fulfilled的Promise对象实例。如果你在这个resolve方法中传入的是一个Promise实例的话,那么resolve方法会保持这个Promise实例的状态,并根据它最后返回的状态来调用resolve方法返回的Promise实例then方法的onResolve或onRejected函数。

其实这个方法最常用的场景是讲一个普通的值转换成一个Promise实例。一般来说不是很常用。


Promise.reject()


与Promise.resolve()相反,它的作用是立刻返回一个状态为Rejected的Promise对象实例。实际上这个方法是一个语法糖,它等同于以下代码:

new Promise(function(resolve,reject){
    reject(reason);
})

以上就是一个ES6中的Promise对象中所包含的常用方法。

3. 一个符合PromiseA+规范的Promise对象的完整实现


想必看到这里的一些读者会不禁思考,Promise对象究竟是如何实现的呢?我个人参考了一些资料实现了一个符合PromiseA+规范的Promise对象,把源代码贴在下面,有兴趣的朋友可以参考一下,实际上代码本身并不是很多,各位看完以后可以尝试用自己的方式再实现一遍。同时附上一个测试工具,里面包含了几百个测试用例,用来测试我们自己写的Promise是否完美的符合PromiseA+规范。

Compliances tests for Promises/A+

使用的方法很简单

npm i -g promises-aplus-tests
 promises-aplus-tests Promise.js

安装后运行你的js文件就可以测试你的代码是否符合规范了。

下面就是我实现的Promise对象的代码


function MyPromise(task) {
    const _this = this;
    _this.status = 'pending';  //设定初始状态
    _this.value = undefined;
    _this.onFulfilledsList = [];  //onFulfilled函数序列
    _this.onRejectedsList = [];  //onRejected函数序列

    function resolve(value) {
        if (value instanceof MyPromise) {
            return value.then(resolve, reject);
        }
        //异步执行resolve或reject方法,保证代码的统一性和注册的回调函数按照正确的顺序执行
            if (_this.status === 'pending') {
                _this.status = 'fulfilled';
                _this.value = value;
                _this.onFulfilledsList.forEach(cb => cb(value))
            }
    }

    function reject(reason) {
            if (_this.status === 'pending') {
                _this.status = 'rejected';
                _this.reason = reason;
                _this.onRejectedsList.forEach(cb => cb(reason))
            }
    }

    try {
        task(resolve, reject);
    } catch (err) {
        throw new Error(err);
    }
}

function resolvePromise(promise2, x, resolve, reject) {
    if (x === promise2) {
        return reject(new TypeError('循环引用'));
    }
    //如果返回的是一个thenable对象,即一个拥有then方法的对象,那么使用它的then方法去获得它的最终返回值。目的是为了兼容其他Promise库
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        let then, called;
        try {
            then = x.then;
            if (typeof then === 'function') {
                then.call(x, function (newx) {
                    if (called) return;   //防止重复调用
                    called = true;
                    resolvePromise(promise2, newx, resolve, reject);
                }, function (err) {
                    if (called) return;
                    called = true;
                    return reject(err);
                });
            } else {
                resolve(x);
            }
        } catch (err) {
            if (called) return;
            called = true;
            reject(err);
        }
    } else {
        resolve(x);
    }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
    const _this = this;
    let promise2;
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (data) {
        return data;
    };
    onRejected = typeof onRejected === 'function' ? onRejected : function (data) {
        throw data;
    };
    //为了支持同步代码,当then方法注册的时候如果Promise的状态已经改变,那么立即执行对应的函数
    if (_this.status === 'fulfilled') {
        promise2 = new MyPromise(function (resolve, reject) {
            setTimeout(function () {
                let x;
                try {
                    x = onFulfilled(_this.value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (err) {
                    reject(err);
                }
            })
        })
    }
    if (_this.status === 'rejected') {
        promise2 = new MyPromise(function (resolve, reject) {
            setTimeout(function () {
                let x;
                try {
                    x = onRejected(_this.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (err) {
                    reject(err);
                }
            })
        })
    }
    if (_this.status === 'pending') {
        promise2 = new MyPromise(function (resolve, reject) {
            _this.onFulfilledsList.push(function (value) {
                setTimeout(function () {
                    let x;
                    try {
                        x = onFulfilled(value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (err) {
                        reject(err);
                    }
                });
            });
            _this.onRejectedsList.push(function (reason) {
                setTimeout(function () {
                    try {
                        let x = onRejected(reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (err) {
                        reject(err);
                    }
                });
            });
        })
    }
    return promise2;  //返回一个新的Promise实例,以便支持链式调用
};

MyPromise.prototype.catch = function (onRejected) {
    this.then(null, onRejected);
};

MyPromise.all = function (someValue) {
    let resolveValArr = [];
    let count = promiseLen = 0;
    let promise2;
    promise2 = new MyPromise(function (resolve, reject) {
        let iNow = 0;
        try {
            for (let item of someValue) {
                if (item !== null && typeof item === "object") {
                    try {
                        let then = item.then;
                        let index = iNow;
                        if (typeof then === 'function') {
                            promiseLen++;
                            then.call(item, function (value) {
                                resolveValArr[index] = value;
                                if (++count === promiseLen) {
                                    resolve(resolveValArr)
                                }
                            }, function (err) {
                                reject(err);
                            });
                        }
                    } catch (err) {
                        resolveValArr[iNow] = item;
                    }
                } else {
                    resolveValArr[iNow] = item;
                }
                iNow++;
            }
            if (iNow === 0) {
                return resolve(someValue);
            }
            if (promiseLen === 0) {
                return resolve(resolveValArr);
            }
        } catch (err) {
            reject(new TypeError('无法遍历的类型!'));
        }
    });
    return promise2;
};


MyPromise.race = function (someValue) {
    let promise2;
    promise2 = new MyPromise(function (resolve, reject) {
        let iNow = 0;
        try {
            for (let item of someValue) {
                if (item !== null && typeof item === "object") {
                    try {
                        let then = item.then;
                        then.call(item, function (value) {
                            resolve(value);
                        }, function (err) {
                            reject(err);
                        });
                    } catch (err) {
                        resolve(item);
                        break;
                    }
                } else {
                    resolve(item);
                    break;
                }
                iNow++;
            }
            if (iNow === 0) {
                return resolve(someValue);
            }
        } catch (err) {
            reject(new TypeError('无法遍历的类型!'));
        }
    });
    return promise2;
};
MyPromise.resolve = function (value) {
    let promise2;
    if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
        promise2 = new MyPromise(function (resolve, reject) {
            try {
                let then = value.then;
                if (typeof value.then === 'function') {
                    then.call(value, function (data) {
                        resolve(data);
                    }, reject);
                } else {
                    resolve(value);
                }
            } catch (err) {
                reject(err);
            }
        })
    } else {
        promise2 = new MyPromise(function (resolve) {
            resolve(value);
        })
    }
    return promise2;
};
MyPromise.reject = function (reason) {
    return new MyPromise(function (resolve, reject) {
        reject(reason);
    })
};
module.exports = MyPromise;

//这是为了让代码能够测试而开放的接口,详见promises-aplus-tests中的相关描述
MyPromise.deferred = MyPromise.defer = function () {
    let deferred = {};
    deferred.promise = new MyPromise(function (resolve, reject) {
        deferred.resolve = resolve;
        deferred.reject = reject;
    });
    return deferred
};


尾声

本文参考了很多资料,如果你看到其他文章有类似的观点非常正常,不过笔者尽量使用了自己的理解去阐述Promise的相关知识。如果你发现本文中有哪些疏漏,欢迎发私信给我进行斧正。同时也可以在下面留言给我,我会一一查看,尽量回复。

编辑于 2018-01-15