资讯专栏INFORMATION COLUMN

ES6-Promise

dackel / 2220人阅读

摘要:方法没有设置返回值。解决思路是,当遇到任务的返回值是一个或者,并且有自己的方法的时候,就将它当做是一个对象处理,等这个对象中的方法处理到的时候,把作为参数输出传递给后续的任务。

前段时间看到关于microTask的文章,《Tasks, microTasks, queues and schedules》,感觉有必要澄清一下。本篇里用setTimeout来实现的Promise,和浏览器原生的Promise是有本质区别的。多数时候感觉不到差异,但正如文章所说,如果不搞清楚microTasks,在实战中一旦遇到和这家伙有关的问题,真得会一点方向都没有。"Yeah, it"ll bite you in obscure places (ouch). "推荐一读。

前两天看到前端早读课的一篇“【第666期】剖析 Promise 内部机制”,从实现角度讲解promise底层实现原理的。看得我两天里寝食难安,脑袋疼胸口闷,严重怀疑脑洞太小。终于还是把逻辑理顺了。短短几十行代码威力如此惊人,逼得我把洪荒之力都用完了……
趁热打铁把自己的理解记录一下~

Promise使用场景

要弄清原理,必须要非常清楚Promise想实现什么。
比如现在有两件事情,第一件事情是要洗衣服,第二件事情是晾衣服。

function wash(){
    console.log("开始洗衣服...");
    setTimeout(()=>{
        console.log("洗完了!");
        return "一堆洗干净的衣服";
    }, 2000);
}

function hang(clothes){
    console.log("开始晾衣服...");
    /*...晾衣服中...*/
    console.log(clothes+"晾完了!");
}

晾衣服hang(clothes)是一定要等洗衣服wash()结束,然后接收到洗好了的衣服clothes以后,才能执行的。
类似这种有明确先后执行顺序,并且可能会有依赖关系的(没有前面执行完返回的结果后面就处理不下去)场景,就是Promise的用武之地。

Promise语法

先看下怎么用Promise来完成先洗衣服后晾干这两步的。

第一步:

var promise = new Promise(wash);
告诉Promise立刻执行wash开始洗衣服。

第二步:

promise.then(hang);
告诉Promise等wash结束以后执行hang开始晾衣服。

Promise用起来就是这么简单!这么清爽!
如果等晾干以后还要收衣服的话,就继续再后面加:

   promise.then(hang).then(pickup);   //pickup就是收衣服方法,等后面再实现

那么问题来了,promise怎么知道衣服啥时候洗完?
Promise规定,丢给Promise执行的方法,需要将一个方法(resolve)作为参数,在执行完成以后,将返回的结果传给这个resolve方法。(有点难说清楚,上代码试试)
这样就需要改写wash方法

function wash(resolve){
    console.log("开始洗衣服...");
    setTimeout(()=>{
        console.log("洗完了!");
        resolve("一堆洗干净的衣服");
    }, 2000);
}

resolve("一堆干净的衣服")会调用Promise里的resolve方法,Promise就会知道洗衣服wash操作已经成功完成了,可以接下去处理then后面的事情了。
把事情变得稍微复杂点试试。衣服洗完了要晾出去,晾完以后要等晒干,晒干了以后要收衣服。我们需要重写hang方法并新增dry和pickup方法

function hang(clothes){
    console.log("开始晾衣服...");
    /*...晾衣服中...*/
    console.log(clothes+"晾好了!");
    return "一堆晾好的衣服";
}

function dry(clothes){
    console.log("等衣服干...");
    /*...晾干中...*/
    console.log(clothes+"晾干了!");
    return "一堆晾干的衣服";
}

function pickup(clothes){
    console.log("开始收衣服...");
    /*...收衣服中...*/
    console.log(clothes+"收完了!");
}

对比之前的代码,发现多了一行return语句,将处理后得到的结果输出,作为参数传入接下去的要处理的方法。
准备好了wash,hang,dry,pickup四个方法后,执行一下看看:

var promise = new Promise(wash);
promise.then(hang).then(dry).then(pickup);

输出结果如下:

开始洗衣服...
洗完了,去晾干!
开始晾衣服...
一堆洗干净的衣服晾完了!
等衣服干...
一堆晾好的衣服晾干了!
开始收衣服...
一堆晾干了的衣服收完了!

再稍微复杂点儿试试~
上面输出结果可以看到,从’开始晾衣服...’到’一堆晾干了的衣服收完了!’几乎是同时输出的。每个动作都应该有段时间间隔才对呀~再改

function hang(clothes){
    console.log("开始晾衣服...");
    setTimeout(()=>{
        console.log(clothes+"晾完了!");
    }, 3000);
    return ("一堆晾好的衣服");
}

function dry(clothes){
    console.log("等衣服干...");
    setTimeout(()=>{
        console.log(clothes+"晾干了!");
    }, 3000);
    return ("一堆晾干了的衣服");
}

function pickup(clothes){
    console.log("开始收衣服...");
    setTimeout(()=>{
        console.log(clothes+"收完了!");
    }, 3000)
}

执行一下看看……

开始洗衣服...
洗完了!
开始晾衣服...
等衣服干...
开始收衣服...
一堆洗干净的衣服晾完了!
一堆晾好的衣服晾干了!
一堆晾干了的衣服收完了!

问题比较明显:

第一.顺序乱了

第二.从’洗完了’到’开始收衣服...’同时输出

第三.最后三句话隔了3秒后同时输出

可以看出then执行的方法并不会等setTimeout执行完才去执行接下去的then中的方法,因为then执行的方法都是同步的。咋办呢?再改~

function hang(clothes){
    console.log("开始晾衣服...");
    return new Promise(resolve=>{
        setTimeout(()=>{
            console.log(clothes+"晾完了!");
            resolve("一堆晾好的衣服");
        }, 3000)
    });
}

function dry(clothes){
    console.log("等衣服干...");
    return new Promise(resolve=>{
        setTimeout(()=>{
            console.log(clothes+"晾干了!");
            resolve("一堆晾干了的衣服");
        }, 3000)
    });
}

function pickup(clothes){
    console.log("开始收衣服...");
    setTimeout(()=>{
        console.log(clothes+"收完了!");
    }, 3000)
}

hang和dry方法返回值改成了一个Promise对象。这里有点难理解,then传入的方法如果返回的是个Promise对象,那么再后面的then传入的方法就会等到这个Promise(实际上是传入Promise的方法)调用了resolve()为止,才会继续执行。

Promise原理

最头疼的部分来了,看看new Promise(wash).then(hang).then(dry).then(pickup)到底怎么实现的。
首先new Promise(wash):
实例化Promise并传入一个方法,这个方法就立刻开始执行了,所以Promise里会执行wash(resolve)方法;
wash方法中通过调用resolve(‘一堆洗干净的衣服’)通知Promise自己执行完了,所以Promise里会有一个resolve(_result_value)方法处理wash的返回结果;
再看then(hang):
then是Promise的实例方法,所以Promise里会有一个this.then = function(mission){...}实例方法。
再根据Promise的实现结果,即then后面的方法要等到wash中执行到resolve(‘一堆洗干净的衣服’)以后才能开始执行。实现方法就是在调用then(hang)的时候,不直接执行hang方法,而是把hang方法存起来,由resolve(_result_value)来触发。

初步实现
function myPromise(fn){
    const missions = [];//待执行队列
    var value = null;
    
    //执行传入的方法
    fn(resolve);
    
    //当传入的方法中调用resolve(value)时,异步执行mission
    function resolve(_return_value){
        value = _return_value;
        missions.forEach(mission=>{
            mission(value);
        });
    }

    //执行then方法时,将传入的方法加入missions,等待resolve触发。
    this.then = function(mission){
        missions.push(mission);
    }
}

同时修改一下wash方法

function wash(resolve){
    console.log("开始洗衣服...");
    console.log("洗完了!");
    resolve("一堆洗干净的衣服");
}

执行new myPromise(wash).then(hang);
输出结果:

开始洗衣服...
洗完了!

晾衣服动作没执行~
来看一下发生了什么
new myPromise(wash)触发执行wash(resolve)方法=>wash(resolve)触发执行resolve(‘一堆洗干净的衣服’)=>resolve(_return_value)执行mission(value)……等下,还没执行then(hang)之前missions里还没任务呢!
所以需要改下resolve方法

function resolve(_return_value){
    value = _return_value;
    setTimeout(()=>{
        missions.forEach(mission=>{
            mission(value);
        })
    }, 0);
}

在执行一下new myPromise(wash).then(hang);结果就对了。

增加状态控制

如果我们想这样使用:

var promise = new myPromise(wash);
setTimeout(()=>{
    promise.then(hang)
}, 1000)

开始洗衣服以后干别的事情去了,过一段时间回来如果洗完了就直接晾衣服,没洗完就接着等待。
因为hang也是个异步操作,会延迟到mission(value)之后才执行,所以此时myPromise又没法正常工作了。
解决办法是给myPromise增加一个状态state。当没有触发resovle(_return_value)时,状态处在pending处理中;当触发了resovle(_return_value)时,状态置为fulfilled已处理。而this.then = function(mission){...}在处理前先对状态做个判断,pending时将mission插入missions任务队列,fulfilled时就直接执行mission(value)。

function resolve(_return_value){
    state = "fulfilled";
    ...//省略其他未改动代码
}

this.then = function(mission){
    if(state === "fulfilled"){
        mission(value);
    }else{
        missions.push(mission);
    }
}
任务链处理

目前执行new myPromise(wash).then(hang).then(dry).then(pickup)会报错。then方法没有设置返回值。稍微调整下代码

this.then = function(mission){
    if(state === "fulfilled"){
        mission(value);
    }else{
        missions.push(mission);
    }
    return this;
}

简化hang, dry, pickup方法

function hang(clothes){
    console.log("开始晾衣服...");
    console.log(clothes+"晾完了!");
    return ("一堆晾好的衣服");
}

function dry(clothes) {
    console.log("等衣服干...");
    console.log(clothes + "晾干了!");
    return ("一堆晾干了的衣服");
}

function pickup(clothes){
    console.log("开始收衣服...");
    console.log(clothes+"收完了!");
}

执行new myPromise(wash).then(hang).then(dry).then(pickup),错是不报了。但是then之间没有正常传递返回的值。clothes始终是“一堆洗干净的衣服”。
myPromise中的value是在执行resolve(_return_value)时赋值的。一个myPromise对象只有一个初始任务(这里是wash),初始任务就执行了一次resovle(‘一堆洗干净的衣服’)。而所有的then方法返回的都是同一个myPromise对象,所以value指向的都是同一个值。
解决思路是,每次调用then方法后,返回一个新的myPromise对象 new myPromise(fn);在fn中执行then方法中要执行的操作。

this.then = function(mission){
    function fn(resolve){
        if(state === "pending"){
            missions.push(mission)
        }else{
            const result = mission(value);
            resolve(result);//关键!
        }
    }
    
    return new myPromise(fn);
}

当触发mission(value)时,将返回的结果作为result执行resolve(result),这就将result传递给了下一个myPromise。
then直接触发mission(value)执行的操作和resolve(_result_value)是一样的,所以resolve也要调整

function resolve(_return_value){
    value = _return_value;
    state = "fulfilled";
    setTimeout(()=>{
        missions.forEach(mission=>{
            const result = mission(value);
            resolve(result); //死循环
        })
    }, 0);
}

执行下?死循环!resolve中应该调用的是then创建的新myPromise的resolve方法,而不是他本身。所以then方法必须把自己创建的myPromise的resolve传递出来。

var next_resolve = null;//保存then生成的下一个myPromise的resolve方法
this.then = function(mission){
    function fn(resolve){
        next_resolve = resolve;
        if(state === "pending"){
            missions.push(mission)
        }else{
            const result = mission(value);
            resolve(result);
        }
    }

    return new myPromise(fn);
}

function resolve(_return_value){
    value = _return_value;
    state = "fulfilled";
    setTimeout(()=>{
        missions.forEach(mission=>{
            const result = mission(value);
            next_resolve(result); 
        })
    }, 0);
}

执行下代码看看结果吧~

Promise对象传递

最后一个问题~(最后一关逻辑有点绕)
还记得之前用Promise对象来作为hang()和dry()的返回值的场景吗?hang()返回Promise对象后,Promise中resolve(‘一堆晾好的衣服’)被执行后,才会将’一堆晾好的衣服’作为参数传递给dry(clothes)方法并开始执行。
这种情况下,用前面写好的myPromise执行一下,又不对了。dry()和pickup()的形参变成了一个new myPromise,结果肯定出错嘛。
解决思路是,当遇到任务的返回值是一个object或者function,并且有自己的then方法的时候,就将它当做是一个Promise对象处理,等这个Promise对象中的方法处理到resolve(_return_result)的时候,把_return_result作为参数输出传递给后续的任务。
重写then和resolve

this.then = function(mission){
    var fn = function(resolve){
        next_resolve = resolve;
        if(state === "pending"){
            missions.push(mission)
        }else{
            const result = mission(value);
            if(result && (typeof result == "object" || typeof result == "function")){
                if(result.then && typeof result.then == "function"){
                    result.then(next_resolve);//关键!
                }
            }else{
                next_resolve(result);
            }
        }
    }

    return new myPromise(fn);
}

function resolve(_return_value){
    value = _return_value;
    state = "fulfilled";
    setTimeout(()=>{
        missions.forEach(mission=>{
            const result = mission(value);
            if(result && (typeof result == "object" || typeof result == "function")){
                if(result.then && typeof result.then == "function"){
                    result.then(next_resolve);//关键!
                }
            }else{
                next_resolve(result);
            }
        })
    }, 0);
}

有必要解释一下result.then(next_resolve)。result此时是一个Promise对象(取名resultPromise),我们需要在resultPromise对象方法执行到resolve(_result_value)时,获取到_result_value并传递给next_resolve(result)执行。
而then方法正是干这事儿的:将next_resolve放进resultPromise的执行队列missions里,resultPromise执行resolve(_result_value),state状态变为fulfilled,触发执行next_resolve(_result_value)。后面就是next_promise状态变为fulfilled,触发执行接下去的mission...

最后把公共代码提取出来整理一下:

function myPromise(fn){
    const missions = [];  //待执行队列
    var value = null;
    var state = "pending";
    var next_resolve = null;

    //执行传入的方法
    fn(resolve);

    //当传入的方法中调用resolve(value)时,异步执行mission
    function resolve(_return_value){
        value = _return_value;
        state = "fulfilled";
        setTimeout(()=>{
            missions.forEach(mission=>{
                handle(mission);
            })
        }, 0);
    }

    //执行then方法时,将传入的方法加入missions,等待resolve触发。
    this.then = function(mission){
        var fn = function(resolve){
            next_resolve = resolve;
            if(state === "pending"){
                missions.push(mission)
            }else{
                handle(mission);
            }
        }
        return new myPromise(fn);
    }

    function handle(mission){
        const result = mission(value);
        //当处理结果为Promise对象时,将next_resolve推入待执行队列
        if(result && (typeof result == "object" || typeof result == "function")){
            if(result.then && typeof result.then == "function"){
                result.then(next_resolve);
            }
        }else{
            next_resolve(result);
        }
    }
}

代码和前端早读课的“剖析 Promise 内部机制”稍有区别,但个人觉得这样写逻辑更清晰一些。至于promise的reject部分,偷懒省略啦~ 当是留点思考空间咯~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87856.html

相关文章

  • 浅析es6-promise源码

    摘要:主要逻辑本质上还是回调函数那一套。通过的判断完成异步和同步的区分。 主要逻辑: 本质上还是回调函数那一套。通过_subscribers的判断完成异步和同步的区分。通过 resolve,reject -> publish -> invokeCallback -> resolve,reject的递归和下一条then的parent是上一条的child来完成then链的流转 同步情况...

    fox_soyoung 评论0 收藏0
  • es6-promise

    摘要:是什么可以理解为一个承诺,如果调用,返回一个承诺给,然后就可以在写计划的时候这么写,当返回结果的时候,就执行方案,如果没有返回要的结果,就执行方案。这样一来,所有的潜在风险就都在的可控范围之内了。 promise是什么 Promise可以理解为一个承诺,如果A调用B,B返回一个承诺给A,然后A就可以在写计划的时候这么写,当B返回结果的时候,A就执行方案1,如果B没有返回A要的结果,A就...

    jackwang 评论0 收藏0
  • 前端临床手扎——简单易用的fetch

    摘要:如题,新增的真的简单易用,感觉现在这一个支持完全可行。虽然兼容性问题还是存在,但是打上后就基本解决了。来自使用简单使用这里说明一下,必须配合一起使用,这会得到更佳效果。 如题,es6 新增的fetch真的简单易用,感觉现在这一个支持完全可行。 showImg(https://segmentfault.com/img/bVGlRy?w=995&h=631); 虽然兼容性问题还是存在,但是...

    xingqiba 评论0 收藏0
  • 在微信小程序使用音乐api的方法,以及微信小程序播放背景音乐失败的解决方案汇总

    摘要:下一步准备使用网易云代替音乐。已经开发新的网易云代替音乐了,需要的可以看看这篇文章为微信小程序开发的网易云音乐库 项目要做一个可以为日记添加音乐的小程序,所以要用到音乐api,参考了一些文章后我们封装了一个qq音乐api库(完成了动态token获取,音乐搜索,音乐专辑图片,音乐名称,歌手名称,播放),有需要的可以到Github自提。 小程序qq音乐api库Gihub地址https://...

    Sleepy 评论0 收藏0
  • turnjs fabricjs canvas 翻书

    摘要:最近做了一个翻书效果的项目来总结一下实现过程和遇到的一些问题供自己以后快速解决问题希望也能帮到同样遇到此类问题的同学如果有更好的方法希望你能分享给我地址插件问题都是些自己觉的比较难解决的比较片面如有其他疑问可以留言交流或者当你从官网下载 最近做了一个翻书效果的项目, 来总结一下实现过程和遇到的一些问题, 供自己以后快速解决问题, 希望也能帮到同样遇到此类问题的同学, 如果有更好的方法,...

    mylxsw 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<