理解ES6的Promise

来源:

在之前的前端Javascript的逻辑实现中,我们经常用到回调这个方式。简单的来说,就是当前请求的结果由回调函数处理。假如现在有一个请求用户ID的异步请求如下:

$.ajax({
  url:"/get_user_id/",
  type:'GET',
  success:function(res){
      console.log(res);
  },
  error:function(error){
      console.log(error);
  }
})

在上面的代码中,通过ajax异步获取到用户id之后,返回的数据交给success函数处理,这种可以理解为简单的回调处理。
然后现在假如我们需要对拿到的数据做二次异步请求处理,假如是用这个ID去获取用户的信息,则代码会变成下面这样:

$.ajax({
  url:"/get_user_id/",
  type:'GET',
  success:function(res){
      let userId = res;
      $.ajax({
          url:"/get_user_info/",
          type:'GET',
          success:function(res){
              console.log("user_info:" , res);
          },
          error:function(error){
              console.log(error);
          }
      })
  },
  error:function(error){
      console.log(error);
  }
})

由于获取用户信息之前我们需要获取到用户的ID,那么第二步的操作就必须等待第一步操作完成,则代码会变成上述那样。如果调用的层数越来越多呢?那这个代码的样式就会变得更加臃肿,俗称“圣诞树模式”。

解决:

1: 在实现上述功能的过程中,直接如此通过上次的回调结果执行下次的操作原则上是正确的,但是代码太过ugly,ES6针对类似情况,集合了新出的标准,即Promise
Promise,直译“承诺”。那也就是说Promise代表的是在未来发生的事情。首先我们用Promise实现上述的功能,先有个整体印象:

let userId = function(){
    return new Promise(function(resolve, reject){
        $.ajax({
            url:"/get_user_id/",
             type:'GET',
             success:function(res){
                 resolve(res);
             },
             error:function(error){
                reject(error);
             }
        })
    });
}
userId().then(function(res){
    //上一步回调成功
    $.ajax({
      url:"/get_user_info/",
      type:"GET",
      success:function(res){
        console.log(res);
      },
      error:function(error){
        console.log(error);
      }
    })
}, function(error){
    console.log("read user_id failed!");
});

可以看到,Promise相比于普通回调的差异就是他对返回的回调操作进行了封装。在官方文档中介绍由:Promise加载异步操作有三种状态:Pending, Resolve,Reject。Pending可以转换为Resolve或者Reject,其余没有相关的转换过程。如果异步操作成功,则Pending变为Resolve,否则Reject(当然也可以投出错误)。
在上面的例子中,新建了Promise对象之后,promise根据回调的状态选择不同的返回值,然后回调的then方法会等待回调值的产生,根据不同的状态选择不同的回调函数:
then(function(), function())这两个function分别对应的是返回状态是Resolve和Reject的处理函数。所以在上面的程序中我们会在第一个函数中再次执行下一次异步操作。
整体的概念就是相比于之前的异步回调操作,Promise进行了封装操作,使得函数的执行更加符合人类的思维。
2: Promise神奇之处不止上面的操作,看另外一个例子,假如现在有A,B两个操作,C操作要等待A和B都完成之后才能执行,则一般的处理流程如下:

let ra = A()
let rb = B()
if(ra && rb){
  C();
}

那么C的执行必须等到A,B两个函数都执行完成之后才能执行,则执行时间为A+B。Promise提供了All方法可以实现多个异步操作的同步管理。看样例:

var ra = new Promise(function (resolve) {
    setTimeout(function () {
        resolve("result of A");
    }, 3000);
});
var rb = new Promise(function (resolve) {
    setTimeout(function () {
        resolve("result of B");
    }, 1000);
});

Promise.all([ra, rb]).then(function (final_result) {
    console.log(final_result); 
});

上述代码的数据结果是["result of A","result of B"]
此时Promise.all会根据观测的多个promise对象的返回值进行操作,上述的返回结果决定于最长的返回时间(A)。即Promise.all会等到所有的promise都回调完成之后(都变成resolve或者有一个变为reject)才会执行对应的处理函数。
3:Promise还有一个操作Promise.race,它的操作对象依旧是多个promise对象,不过和all不同的是,race观测的对象只要有一个发生了状态改变,则就会将该状态改变返回给race。假如:

var p = Promise.race([p1, p2, p3]);
p.then(ret => { console.log(ret); })

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。然后会将返回值传递给p的回调函数。
官方文档有一个例子,做超时判断的:

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));

上述操作如果说那个fetch命令在5S内没有结束,那么整个操作就会结束。

深入探究

既然promise这么神奇,那么内部是怎么实现的呢?楼主比较菜,阅读了网上各大神的blog,简单的说一下,欢迎大家拍砖。
1:首先说呢,很多人说promise的实现类似于设计模式中的观察者模式,通过在函数中注册对应的操作函数,如果相关的数据发生了变化,则通知已经注册的函数进行对应的操作。
CP网上的例子讲吧(感觉类比的比较清楚😁):
最简单的如下:

    function getUserId(){
        return new MyPromise(function(resolve){
            $.ajax({
                url:'/users/getUserId',
                type:'GET',
                success:function(res) {
                    console.log(res);
                    resolve(res);
                },
                error:function(res){
                    console.log(res);
                }
            })
        })
    }
    getUserId().then(function(id) {
        console.log(id);
    });

然后基本版的MyPromise如下:

    function MyPromise(fn){
        var value = null;
        var callbacks = [];
        this.then = function(func){
            callbacks.push(func);
        };
        function resolve(value){
            setTimeout(function(){
                callbacks.forEach(function(cal){
                    cal(value);
                });
            },0);
        }
        fn(resolve);
    }

一行一行解释来看:
当我们创建MyPromise对象的时候,传入的参数是一个函数function。然后我们在MyPromise的构造函数中看到
value:存储的要处理的变量
callbacks:存储的需要进行注册的回调函数
当new一个MyPromise的时候,我们传入function,然后我们给该函数传递一个函数参数resolve。然后呢,函数就去执行对应的异步操作,在这里就是ajax请求。然后当前的MyPromise对象执行then方法,then方法将当前的操作函数注册到我们的callbacks数组中,当前方法即为function(id) {console.log(id);}。当异步操作完成之后,mypromise对象执行对应的resolve方法,看函数定义,此时,执行的操作是,对当前已经注册的回调方法传入刚刚采集的数据value,然后依次对每个函数进行操作。然后呢就可以看到对应的输出了。
其实这种注册然后执行的模式确实挺像观察者模式的,通过在本身注册操作函数,当发生变化的时候,执行对应的函数。
解释:resolve函数里面的setTimeout操作,是为了一种情况设定的,即如果当前的异步操作变成了同步操作,那么就会直接执行resolve函数,则对应的执行函数就没有注册到回调函数数组中。所以就给resolve加上settimeout实现将当前操作移动到时间循环的最后执行,保证所有的方法等能够得道注册。

2:当我们执行了注册函数的操作之后,当前函数才可以被执行,如果函数异步操作已经完成,那么之后的调用then注册的函数也都没有执行了。反看promise的实现,可以通过不断的调用then方法不断的去执行注册函数。所以呢,在这一步就加入了状态的概念
看代码:

function MyPromise(fn){
    var value = null;
    var callbacks = [];
    var state = 'pending';
    this.then = function(func){
        if(state === 'pending'){
            callbacks.push(func);
            return this;
        }
        func(value);
        return this;
    };
    function resolve(newValue){
        value = newValue;
        state = 'fulfilled';
        setTimeout(function(){
            callbacks.forEach(function(cal){
                cal(value);
            });
        },0);
    }
    fn(resolve);
}

可以看到对应的支持操作是如果当前已经resolve了,那么后续的then操作就会直接调用执行对应的回调函数。解决了前面提到的后续注册函数不能执行的问题。
3:继续学习网上大神的操作。然后假如then函数传递了一个新的promise进来,那么如何操作呢?ES6的promise的then函数如果传递了新的promise进来,那么后续的then操作则会等待当前的异步操作对象完成之后再次执行。
传入then的函数的参数是一个promise的情况是上一个回调函数resolve返回的是一个promise。所谓的链式操作吧。
然后链式操作的过程是上一个resolve之后开始下一个promise,所以重点就是把前后的promise进行连接
然后既然是新的promise那么我们就给他在执行then的时候返回一个新的promise对象回去。

function MyPromise(fn){
    var value = null;
    var callbacks = [];
    var state = 'pending';
    this.then = function(func){
        return new MyPromise(function(resolve){
            handle({
                func:func,
                resolve:resolve
            });
        });
    };
    function handle(callback){
        if(state === "pending"){
            callbacks.push(callback);
            return;
        }
        if(!callback.resolve){
            callback.resolve(value);
            return;
        }
        var ret = callback.func(value);
        callback.resolve(ret);
    }
    function resolve(newValue){
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }
    fn(resolve);
}

我本地执行大神的代码没有实现想要的效果,还是传送门http://blog.csdn.net/qq_22844483/article/details/73655738

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

推荐阅读更多精彩内容