用ES6实现一个简单易懂的Promise(遵循Promise/A+ 规范并附详解注释)

一.Promise的含义和意义

1.什么是Promise
Promise是抽象异步处理对象以及对其进行各种操作的组件,其实Promise就是一个对象,用来传递异步操作的消息,它不是某门语言特有的属性,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象,Promise对象有以下两个特点:

1.对象的状态不受外界影响
2.一旦状态改变,就不会再变,任何时候都可以得到这个结果

Promise也以下缺点:

1.无法取消Promise,一旦新建它就会立即执行,无法中途取消。
2.如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
3.当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

关于Promise的详细介绍和用法,可以参考JavaScript Promise迷你书

2.为什么要在js中使用Promise
ES6新增了Promise这个特性的意义在于,以往在js中处理异步操作通常是使用回调函数和事件,而有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。拿node.js读取文件举例子,基于JavaScript的异步处理,以往都是想下面这样利用回调函数:

var fs = require('fs');
fs.readFile('demo.txt', 'utf8', function (err, data) {
          if (err) throw err;
         console.log(data);
});

而使用Promise可以这样写:

var fs = require('fs');
function readFile(fileName) {
    return new Promise(function(resolve, reject) {
        fs.readFile(fileName, function (err, data) {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}
readFile(('demo.txt').then(
    function(data) {
        console.log(data);
    }, 
    function(err) {
        throw err;
    }   
);

这样的结构就比较清晰了,有同学看到这要问了,要是有多重嵌套怎么办,来看下面这个例子,假如我们有多个延时任务要处理,在js中便使用setTimeout来实现,在以往就是js中往往是这样写:

var taskFun = function() {   
    setTimeout(function() {
               // do timeoutTask1
              console.log("do timeoutTask1");
        setTimeout(function() {
                   // do timeoutTask2
                  console.log("do timeoutTask2");
            setTimeout(function() {
                      // dotimeoutTask3
                     console.log("do timeoutTask3");
            }, 3000);
        }, 1000); 
    }, 2000);
}
 taskFun();

这样写嵌套了多层回调结构,如果业务逻辑再复杂一点,就会进入到所谓的回调地狱,那么如果用Promise可以这样来写:

new Promise(function(resolve, reject) {
    console.log("start timeoutTask1");
    setTimeout(resolve, 3000);
}).then(function() {
    // do timeoutTask1
    console.log("do timeoutTask1");
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask2");
        setTimeout(resolve, 1000);
    });
}).then(function() {
    // do timeoutTask1
    console.log("do timeoutTask2");
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask3");
        setTimeout(resolve, 2000);
    });
}).then(function() {
    // do timeoutTask1
    console.log("do timeoutTask3");
});

我们还可以用Promise这样写,把每个任务提炼成单独函数,让代码看起来更加优雅直观:

function timeoutTask1() {
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask1");
        setTimeout(resolve, 3000);
    });
}

function timeoutTask2() {
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask2");
        setTimeout(resolve, 1000);
    });
}

function timeoutTask3() {
    return new Promise(function(resolve, reject) {
        console.log("start timeoutTask3");
        setTimeout(resolve, 2000);
    });
}

timeoutTask1()
    .then(function() {
        // do timeoutTask1
        console.log("do timeoutTask1");
    })
    .then(timeoutTask2)
    .then(function() {
        // do timeoutTask2
        console.log("do timeoutTask2");
    })
    .then(timeoutTask3)
    .then(function() {
        // do timeoutTask2
        console.log("do timeoutTask3");
    });

执行的顺序为:


运行结果

二.用ES6自己实现一个遵循Promise/A+规范的Promise

Promise/A+是Promise的一个主流规范,浏览器,node和JS库依据此规范来实现相应的功能,以此规范来实现一个Promise也可以叫做实现一个Promise/A+。具体内容可参考Promise/A+规范

1.类和构造器的构建
Promise 的参数是一个函数 task,把内部定义 resolve 和reject方法作为参数传到 task中,调用 task。当异步操作成功后会调用 resolve 方法,然后就会执行 then 中注册的回调函数,失败是调用reject方法。

class Promise {
    constructor(task) {
        let self = this; //缓存this
        self.status = 'pending'; //默认状态为pending
        self.value = undefined;  //存放着此promise的结果
        self.onResolvedCallbacks = [];  //存放着所有成功的回调函数
        self.onRejectedCallbacks = [];   //存放着所有的失败的回调函数

        // 调用resolve方法可以把promise状态变成成功态
        function resolve(value) {
            if (value instanceof Promise) {
                return value.then(resolve, reject)
            }
            setTimeout(() => { // 异步执行所有的回调函数
                // 如果当前状态是初始态(pending),则转成成功态
                // 此处这个写判断的原因是因为resolved和rejected两个状态只能由pending转化而来,两者不能相互转化
                if (self.status == 'pending') {
                    self.value = value;
                    self.status = 'resolved';
                    self.onResolvedCallbacks.forEach(item => item(self.value));
                }
            });

        }

        // 调用reject方法可以把当前的promise状态变成失败态
        function reject(value) {
            setTimeout(() => {
                if (self.status == 'pending') {
                    self.value = value;
                    self.status = 'rejected';
                    self.onRejectedCallbacks.forEach(item => item(value));
                }
            });
        }

        // 立即执行传入的任务
        try {
            task(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
}

代码思路与要点:

  • self = this, 不用担心this指向突然改变问题。
  • 每个 Promise 存在三个互斥状态:pending、fulfilled、rejected。
  • Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
  • 创建 Promise 对象同时,调用其 task, 并传入 resolve和reject 方法,当 task 的异步操作执行成功后,就会调用 resolve,也就是执行 Promise .onResolvedCallbacks 数组中的回调,执行失败时同理。
  • resolve和reject 方法 接收一个参数value,即异步操作返回的结果,方便传值。

2.Promise.prototype.then链式支持

 /**
     * onFulfilled成功的回调,onReject失败的回调
     * 原型链方法
 */
    then(onFulfilled, onRejected) {
        let self = this;
        // 当调用时没有写函数给它一个默认函数值
        onFulfilled = isFunction(onFulfilled) ? onFulfilled : value => value;
        onRejected = isFunction(onRejected) ? onRejected : value => {
            throw value
        };
        let promise2;
        if (self.status == 'resolved') {
            promise2 = new Promise((resolve, reject) => {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            });
        }
        if (self.status == 'rejected') {
            promise2 = new Promise((resolve, reject) => {
                setTimeout(() => {
                    try {
                        let x = onRejected(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            });
        }
        if (self.status == 'pending') {
            promise2 = new Promise((resolve, reject) => {
                self.onResolvedCallbacks.push(value => {
                    try {
                        let x = onFulfilled(value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
                self.onRejectedCallbacks.push(value => {
                    try {
                        let x = onRejected(value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            });
        }
        return promise2;
    }

代码思路与要点:

  • 调用 then 方法,将成功回调放入 promise.onResolvedCallbacks 数组;失败回调放入 promise.onRejectedCallbacks 数组
  • 返回一个 Promise 实例 promise2,方便链式调用
  • then方法中的 return promise2 实现了链式调用
  • 如果传入的是一个不包含异步操作的函数,resolve就会先于 then 执行,即 promise.onResolvedCallbacks 是一个空数组,为了解决这个问题,在 resolve 函数中添加 setTimeout,将 resolve 中执行回调的逻辑放置到 JS 任务队列末尾;reject函数同理。

3.静态方法Promise.resolve

 static resolve(value) {
        return new Promise((resolve, reject) => {
            if (typeof value !== null && typeof value === 'object' && isFunction(value.then)) {
                value.then();
            } else {
                resolve(value);
            }
        })
    }

静态方法Promise.resolve(value) 可以认为是 new Promise() 方法的快捷方式。

比如 Promise.resolve(666); 可以认为是以下代码的语法糖。

new Promise(function(resolve){
    resolve(666);
});

4.静态方法Promise.reject

 static reject(err) {
        return new Promise((resolve, reject) => {
            reject(err);
        })
    }

Promise.reject(err)是和 Promise.resolve(value) 类似的静态方法,是 new Promise() 方法的快捷方式。

比如 Promise.reject(new Error("出错了")) 就是下面代码的语法糖形式。

new Promise(function(resolve,reject){
    reject(new Error("出错了"));
});

4.静态方法Promise.all

 /**
     * all方法,可以传入多个promise,全部执行完后会将结果以数组的方式返回,如果有一个失败就返回失败
     * 静态方法为类自己的方法,不在原型链上
     */
    static all(promises) {
        return new Promise((resolve, reject) => {
            let result = []; // all方法最终返回的结果
            let count = 0; // 完成的数量
            for (let i = 0; i < promises.length; i++) {
                promises[i].then(data => {
                    result[i] = data;
                    if (++count == promises.length) {
                        resolve(result);
                    }
                }, err => {
                    reject(err);
                });
            }
        });
    }

Promise.all 接收一个 promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用 .then 方法。当全部为resolve时返回一个全部的resolve执行结果数组,只要有一个不为resolve状态,直接返回这个状态的执行失败结果。

5.静态方法Promise.race

/**
     * race方法,可以传入多个promise,返回的是第一个执行完的resolve的结果,如果有一个失败就返回失败
     *  静态方法为类自己的方法,不在原型链上
*/
    static race(promises) {
        return new Promise((resolve, reject) => {
            for (let i = 0; i < promises.length; i++) {
                promises[i].then(data => {
                    resolve(data);
                },err => {
                    reject(err);
                });
            }
        });
    }

Promise.racePromise.all 相类似,它同样接收一个数组,race的意思是竞赛,顾名思义只要是竞赛就有唯一的那个第一名,所以它与all最大的不同是只要该数组中的任意一个 Promise 对象的状态发生变化(无论是 resolve 还是 reject)该方法都会返回,所以它只输出某一个最先执行的状态结果,而不是像all一样在全部为resolve状态时返回的是一个数组。只需在Promise.all 方法基础上修改一下就可实现race。

三.总结

源代码
以上是对几个主要方法的介绍,还有些没有介绍完全,可以参考源代码,源码文件里包含了一个测试文件夹以及es5的版本源码,后续会奉上更为详尽的解释。另外可以通过安装一个插件来对实现的promise进行规范测试。

npm(cnpm) i -g promises-aplus-tests
promises-aplus-tests es6Promise.js
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,698评论 1 56
  • Promiese 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,语法上说,Pr...
    雨飞飞雨阅读 3,348评论 0 19
  • 本文适用的读者 本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,...
    HZ充电大喵阅读 7,293评论 6 19
  • Promise的含义:   Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和...
    呼呼哥阅读 2,161评论 0 16
  • 为期一周的中层领导能力提升培训,今天就要结束了,我和其他学员一样,感悟颇多,收获满满。 首先,"充电"饱满...
    随冲阅读 784评论 0 0