站住,你这个Promise!

前排占楼

个人开源项目 — Vchat 正式上线了,欢迎各位小哥哥小姐姐体验。如果觉得还行的话,记得给个star哟 _

言归正传,你经历过绝望吗?

image

众所周知,js是单线程异步机制的。这样就会导致很多异步处理会嵌套很多的回调函数,最为常见的就是ajax请求,我们需要等请求结果返回后再进行某些操作。如:

    function success(data, status) {
        console.log(data)
    }
    function fail(err, status) {
        console.log(err)
    }
    ajax({
        url: myUrl,
        type: 'get',
        dataType: 'json',
        timeout: 1000,
        success: success(data, status),
        fail: fail(err, status)
    })

乍一看还行啊,不够绝望啊,让绝望来的更猛烈一些吧!那么试想一下,如果还有多个请求依赖于上一个请求的返回值呢?五个?六个?代码就会变得非常冗余和不易维护。这种现象,我们一般亲切地称它为‘回调地狱’。现在解决回调地狱的手段有很多,比如非常方便的async/await、Promise等。

我们现在要讲的是Promise。在如今的前端面试中,Promise简直是考点般的存在啊,十个有九个会问。那么我们如何真正的弄懂Promise呢?俗话说的好,‘想要了解它,先要接近它,再慢慢地实现它’。自己实现一个Promise,不就什么都懂了。

image

其实网络上关于Promise的文章有很多,我也查阅了一些相关文章,文末有给出相关原文链接。所以本文侧重点是我在实现Promise过程中的思路以及个人的一些理解,有感兴趣的小伙伴可以一起交流。

如果用promise实现上面的ajax,大概是这个效果:

    ajax().success().fail();

何为 Promise

那么什么是Promise呢?

  1. Promise是为了解决异步编程的弊端,使你的代码更有条理、更清晰、更易维护。
  2. Promise是一个构造函数(或者类),接受一个函数作为参数,该函数接受resolve,reject两个参数。
  3. 它的内部有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),其中pending可以转化为fulfilled或者和rejected,但是不能逆向转化,成功和失败也不能相互转化。
  4. value、reason成功的参数和失败的错误信息。
  5. then方法,实现链式调用,类似于jq。

基本用法:

    let getInfo = new Promise((resolve, reject) => {
        setTimeout(_ => {
            let ran = Math.random();
            console.log(ran);
            if (ran > 0.5) {
                resolve('success');
            } else {
                reject('fail');
            }
        }, 200);
    });
    getInfo.then(r => {
        return r + ' ----> Vchat';
    }).then(r => {
        console.log(r);
    }).catch(err => {
        console.log(err);
    })
    // ran > 0.5输出 success ----> Vchat
    // ran <= 0.5输出 fail

先定个小目标,然后一步步实现它。

构建Promise

  • 基础构造

    首先需要了解一下基本原理。我第一次接触Promise的时候,还很懵懂(捂脸)。那会只知道这么写,不知道到底是个什么流程走向。下面,我们来看看最基本的实现:

    function Promise(Fn){
        let resolveCall = function() {console.log('我是默认的');}; // 定义为函数是为了防止没有then方法时报错
        this.then = (onFulfilled) => {
            resolveCall = onFulfilled;
        };
        function resolve(v){ // 将resolve的参数传给then中的回调
            resolveCall(v);
        }
        Fn(resolve);
    }
    new Promise((resolve, reject) => {
        setTimeout(_ => {
            resolve('success');
        }, 200)
    }).then(r => {
        console.log(r);
    });
    // success

这里需要注意的是,当我们new Promise 的时候Promise里的函数会直接执行。所以如果你想定义一个Promise以待后用,比如axios封装,需要用函数包装。比如这样:

    function myPromise() {
        return new Promise((resolve, reject) => {
            setTimeout(_ => {
                resolve('success');
            }, 200)
        });
    }
    // myPromise().then()

再回到上面,在new Promise 的时候会立即执行fn,遇到异步方法,于是先执行then中的方法,将 onFulfilled 存储到 resolveCall 中。异步时间到了后,执行 resolve,从而执行 resolveCall即储存的then方法。这是输出的是我们传入的‘success’

这里会有一个问题,如果 Promise 接受的方法不是异步的,则会导致 resolve 比 then 方法先执行。而此时 resolveCall 还没有被赋值,得不到我们想要的结果。所以要给resolve加上异步操作,从而保证then方法先执行。

    // 直接resolve
    new Promise((resolve, reject) => {
        resolve('success');
    }).then(r => {
        console.log(r); // 输出为 ‘我是默认的’,因为此时then方法还没有,then方法的回调没有赋值给resolveCall,执行的是默认定义的function() {}。
    });
    // 加上异步处理,保证then方法先执行
    function resolve(v){
        setTimeout(_ => {
            resolveCall(v);
        })
    }
  • 增加链式调用

    链式调用是Promise非常重要的一个特征,但是上面写的那个函数显然是不支持链式调用的,所以我们需要进行处理。

    function Promise(Fn){
        this.resolves = []; // 方便存储onFulfilled
        this.then = (onFulfilled) => {
            this.resolves.push(onFulfilled);
            return this;
        };
        let resolve = (value) =>{ // 改用箭头函数,这样不用担心this指针问题
            setTimeout(_ => {
                this.resolves.forEach(fn => fn(value));
            });
        };
        Fn(resolve);
    }

可以看到,这里将接收then回调的方法改为了Promise的属性resolves,而且是数组。这是因为如果有多个then,依次push到数组的方式才能存储,否则后面的then会将之前保存的覆盖掉。这样等到resolve被调用的时候,依次执行resolves中的函数就可以了。这样可以进行简单的链式调用。

    new Promise((resolve, reject) => {
        resolve('success');
    }).then(r => {
        console.log(r); // success
    }).then(r => {
        console.log(r); // success
    });

但是我们会有这样的需求, 某一个then链想自己return一个参数供后面的then使用,如:

    then(r => {
        console.log(r);
        return r + ' ---> Vchat';
    }).then();

要做到这一步,需要再加一个处理。

    let resolve = (value) =>{
        setTimeout(_ => {
            // 每次执行then的回调时判断一下是否有返回值,有的话更新value
            this.resolves.forEach(fn => value = fn(value) || value);
        });
    };
  • 增加状态

    我们在文章开始说了Promise的三种状态以及成功和失败的参数,现在我们需要体现在自己写的实例里面。

    function Promise(Fn){
        this.resolves = [];
        this.status = 'PENDING'; // 初始为'PENDING'状态
        this.value;
        this.then = (onFulfilled) => {
            if (this.status === 'PENDING') { // 如果是'PENDING',则储存到数组中
                this.resolves.push(onFulfilled);
            } else if (this.status === 'FULFILLED') { // 如果是'FULFILLED',则立即执行回调
                console.log('isFULFILLED');
                onFulfilled(this.value);
            }
            return this;
        };
        let resolve = (value) =>{
            if (this.status === 'PENDING') { // 'PENDING' 状态才执行resolve操作
                setTimeout(_ => {
                    //状态转换为FULFILLED
                    //执行then时保存到resolves里的回调
                    //如果回调有返回值,更新当前value
                    this.status = 'FULFILLED';
                    this.resolves.forEach(fn => value = fn(value) || value);
                    this.value = value;
                });
            }
        };
        Fn(resolve);
    }

这里可能会有同学觉得困惑,我们通过一个例子来说明增加的这些处理到底有什么用。

    let getInfo = new Promise((resolve, reject) => {
        resolve('success');
    }).then(_ => {
        console.log('hahah');
    });
    setTimeout(_ => { 
        getInfor.then(r => {
            console.log(r); // success
        })
    }, 200);

在resolve函数中,判断了'PENDING' 状态才执行setTimeout方法,并且在执行时更改了状态为'FULFILLED'。这时,如果运行这个例子,只会输出一个‘success’,因为接下来的异步方法调用时状态已经被改为‘FULFILLED’,所以不会再次执行。

这种情况要想它可以执行,就需要用到then方法里的判断,如果状态是'FULFILLED',则立即执行回调。此时的传参是在resolve执行时保存的this.value。这样就符合Promise的状态原则,PENDING不可逆,FULFILLED和REJECTED不能相互转化。

  • 增加失败处理

    可能有同学发现我一直没有处理reject,不用着急。reject和resolve流程是一样的,需要一个reason做为失败的信息返回。在链式调用中,只要有一处出现了reject,后续的resolve都不应该执行,而是直接返回reject。

    this.reason;
    this.rejects = [];
     // 接收失败的onRejected函数
    if (this.status === 'PENDING') {
        this.rejects.push(onRejected);
    }
     // 如果状态是'REJECTED',则立即执行onRejected。
    if (this.status === 'REJECTED') {
        onRejected(this.reason);
    }
    // reject方法
    let reject = (reason) =>{
        if (this.status === 'PENDING') {
            setTimeout(_ => {
                //状态转换为REJECTED
                //执行then时保存到rejects里的回调
                //如果回调有返回值,更新当前reason
                this.status = 'REJECTED';
                this.rejects.forEach(fn => reason = fn(reason) || reason);
                this.reason = reason;
            });
        }
    };
    // 执行Fn出错直接reject
    try {
        Fn(resolve, reject);
    }
    catch(err) {
        reject(err);
    }

在执行储存then中的回调函数那一步有一个细节一直没有处理,那就是判断是否有onFulfilled或者onRejected方法,因为是允许不要其中一个的。现在如果then中缺少某个回调,会直接push进undefined,如果执行的话就会出错,所以要先判断一下是否是函数。

    this.then = (onFulfilled, onRejected) => {
        // 判断是否是函数,是函数则执行
        function success (value) {
            return typeof onFulfilled === 'function' && onFulfilled(value) || value;
        }
        function erro (reason) {
            return typeof onRejected === 'function' && onRejected(reason) || reason;
        }
        // 下面的处理也要换成新定义的函数
        if (this.status === 'PENDING') {
            this.resolves.push(success);
            this.rejects.push(erro);
        } else if (this.status === 'FULFILLED') {
            success(this.value);
        } else if (this.status === 'REJECTED') {
            erro(this.reason);
        }
        return this;
    };

因为reject回调执行时和resolve基本一样,所以稍微优化一下部分代码。

    if(this.status === 'PENDING') {
        let transition = (status, val) => {
            setTimeout(_ => {
                this.status = status;
                let f = status === 'FULFILLED',
                    queue = this[f ? 'resolves' : 'rejects'];
                queue.forEach(fn => val = fn(val) || val);
                this[f ? 'value' : 'reason'] = val;
            });
        };
        function resolve(value) {
            transition('FULFILLED', value);
        }
        function reject(reason) {
            transition('REJECTED', reason);
        }
    }
  • 串行 Promise

    假设有多个ajax请求串联调用,即下一个需要上一个的返回值作为参数,并且要return一个新的Promise捕捉错误。这样我们现在的写法就不能实现了。

    我的理解是之前的then返回的一直是this,但是如果某一个then方法出错了,就无法跳出循环、抛出异常。而且原则上一个Promise,只要状态改变成‘FULFILLED’或者‘REJECTED’就不允许再次改变。

    之前的例子可以执行是因为没有在then中做异常的处理,即没有reject,只是传递了数据。所以如果要做到每一步都可以独立的抛出异常,从而终止后面的方法执行,还需要再次改造,我们需要每个then方法中return一个新的Promise。

    // 把then方法放到原型上,这样在new一个新的Promise时会去引用prototype的then方法,而不是再复制一份。
    Promise.prototype.then = function(onFulfilled, onRejected) {
        let promise = this;
        return new Promise((resolve, reject) => {
            function success (value) {
                let val = typeof onFulfilled === 'function' && onFulfilled(value) || value;
                resolve(val); // 执行完这个then方法的onFulfilled以后,resolve下一个then方法
            }
            function erro (reason) {
                let rea = typeof onRejected === 'function' && onRejected(reason) || reason;
                reject(rea); // 同resolve
            }
            if (promise.status === 'PENDING') {
                promise.resolves.push(success);
                promise.rejects.push(erro);
            } else if (promise.status === 'FULFILLED') {
                success(promise.value);
            } else if (promise.status === 'REJECTED') {
                erro(promise.reason);
            }
        });
    };

在成功的函数中还需要做一个处理,用以支持在then的回调函数(onFulfilled)中return的Promise。如果onFulfilled方法return的是一个Promise,则直接执行它的then方法。如果成功了,就继续执行后面的then链,失败了直接调用reject。

    function success(value) {
        let val = typeof onFulfilled === 'function' && onFulfilled(value) || value;
        if(val && typeof val['then'] === 'function'){ // 判断是否有then方法
            val.then(function(value){ // 如果返回的是Promise 则直接执行得到结果后再调用后面的then方法
                resolve(value);
            },function(reason){
                reject(reason);
            });
        }else{
            resolve(val);
        }
    }

找个例子测试一下

    function getInfo(success, fail) {
        return new Promise((resolve, reject) => {
            setTimeout(_ => {
                let ran = Math.random();
                console.log(success, ran);
                if (ran > 0.5) {
                    resolve(success);
                } else {
                    reject(fail);
                }
            }, 200);
        })
    }
    getInfo('Vchat', 'fail').then(res => {
        console.log(res);
        return getInfo('可以线上预览了', 'erro');
    }, rej => {
        console.log(rej);
    }).then(res => {
        console.log(res);
    }, rej => {
        console.log(rej);
    });
    // 输出
    // Vchat 0.8914818954810422
    // Vchat
    // 可以线上预览了 0.03702367800412443
    // erro

总结

到这里,Promise的主要功能基本上都实现了。还有很多实用的扩展,我们也可以添加。
比如 catch可以看做then的一个语法糖,只有onRejected回调的then方法。其它Promise的方法,比如.all、.race 等等,感兴趣的小伙伴可以自己实现一下。另外,文中如有不对之处,还请指出_

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

相关文章

交流群

本群是Vchat前端交流群,欢迎各种技术交流,期待你的加入

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

推荐阅读更多精彩内容

  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,698评论 1 56
  • title: promise总结 总结在前 前言 下文类似 Promise#then、Promise#resolv...
    JyLie阅读 12,219评论 1 21
  • 你不知道JS:异步 第三章:Promises 在第二章,我们指出了采用回调来表达异步和管理并发时的两种主要不足:缺...
    purple_force阅读 2,047评论 0 4
  • 前有沙滩后有浪,而我还好没有死在沙滩上! 一 2016年的国庆节,我在上海参加了DISC课程学习。现场有00后的初...
    1路向前阅读 198评论 0 0