从实际应用中去理解Promise

在es6使用已经很普遍的今天,我再说Promise各种API及其原理的话也不过是锦上添花。Promise的重要性,我们不必多讲,相信大家都知道出现Promise的意义及其对js异步编程上的作用是多么巨大。不过,说真,要真正去理解使用这个对象,确实是有些困难,特别是对我这种理解新东西总是有困难症的人来说更甚。所以,我的办法就是重重复复地看相关的文章及api文档,以达到自己能使用及理解。在理解Promise之前,先来看一下异步的实现过程。

为什么要用Promise

在说Promise之前,我们得先来确定一下为什么要有这个对象。

原生JS实现一个简单的ajax

说到浏览器异步处理中,ajax可以说是最基本的异步编程方法了,可以简单实现为:

//此实现,只是简单实现,未实现兼容处理
var url = '<url>';
var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
    if (XHR.readyState == 4 && XHR.status == 200) {
        result = XHR.response;
        console.log(result);
    }
}

在ajax的原生实现中,利用了onreadystatechange事件,当该事件触发且符合一定条件时,才能拿到我们想要的数据。之后我们才能开始执行回调里面的代码。这看上去并没有什么麻烦的,但是如果这时,我们想有两个ajax请求,并且还得有先后顺序进执行。我们这时会这样做。

var url = '<url>';
var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
    if (XHR.readyState == 4 && XHR.status == 200) {
        result = XHR.response;
        console.log(result);
        // 在此重复上面的逻辑
        var url2 = '<url>';
        var result2;
        var XHR2 = new XMLHttpRequest();
        XHR2.open('GET', url, true);
        XHR2.send();
        XHR2.onreadystatechange = function() {
            if (XHR.readyState == 4 && XHR.status == 200) {
                result = XHR.response;
                console.log(result);
            }
        }
    }
}

当出现第三个ajax(甚至更多)仍然依赖上面的方法做,那将是一场灾难。这样的灾难,往往被称为** 回调地狱 **
因此,我们需要消除掉回调地狱这个问题。
当然,除了消除回调地狱之外,还一个非常重要的需求:** 为了我们的代码更加具有可读性与可维护性,我们需要将数据请求与数据处理逻辑区分开来 **。上面的写法,是完全没有区分开的,当数据变得复杂,处理逻辑就更加复杂,到时我们就没法维护我们写的代码了。
(为了代码简洁性,部分代码使用es6的写法)

函数调用栈

我们可以通过利用函数调用栈,将我们想要代码放到回调函数中,来解决回调地狱问题。函数调用栈这是一种什么概念呢?我们知道,js内置了一些公共的方法,如setTimeout与setInterval这两个函数,他们有一个特性,就是异步执行。当js执行到此类函数的时候,直接放到栈中(具体如何实现不做详解),当整个同步的js代码执行完成之后,再调用栈中的异步函数,当然这个调用栈也是按放入的顺序去一个一个执行。这样子我们就实现了异步处理按我们想要队列办法来实现。

function fn(){
    console.log('fn do!');
}
function fn1(fn){
    console.log('do 1!');
    fn && setTimeout(fn,0);
}
function fn2(fn){
    console.log('do 2!');
    fn && setTimeout(fn,0);
}
function fn3(fn){
    console.log('do 3!');
    fn && setTimeout(fn,0);
}
fn1(fn2(fn3(fn)));
// 输出结果为:
// do 3!
// do 2!
// do 1!
// fn do!

这种实现办法,虽然在一定程序上可以解决掉代码维护上的困难度,不过却影响到可读性。我们从调用的层叠性上看,令人搞不清顺序,从fn1到fn3的层层调用中,我们发现输出结果是先3然后再到1,最后才执行want函数,感觉虽然解决了代码回调地狱,却把顺序弄得不伦不类的。如果这队列上的回调更多,则会更加难以理解。

Promise的基础与使用

Promise的引入

如果浏览器已经支持了原生的Promise对象,那我们可以将上面的函数都改写为Promse对象的方法。

function fn(){
    console.log('fn do!');
}
function fn1(fn){
    console.log('do 1!');
    return new Promise(function(resolve,reject){
        if(typeof fn == 'function') resolve(fn);
        else reject("TypeError:" + fn + "no a function.");
    });
}
function fn2(fn){
    console.log('do 2!');
    return new Promise(function(resolve,reject){
        if(typeof fn == 'function') resolve(fn);
        else reject("TypeError:" + fn + "no a function.");
    });
}
function fn3(fn){
    console.log('do 3!');
    return new Promise(function(resolve,reject){
        if(typeof fn == 'function') resolve(fn);
        else reject("TypeError:" + fn + "no a function.");
    });
}
fn1(fn)
.then(function(fn){
    return fn2(fn);
})
.then(function(fn){
    return fn3(fn);
})
.then(function(fn){
    fn();
});
// 执行结果为:
// do 1!
// do 2!
// do 3!
// fn do!

从上面的代码中,我们很清楚地看到,fn1是从1-3的顺序去执行,并且代码层次也可以分得非常清晰。这就是Promise的在实际中最基本的实现。

Promise对象

为了更好理解Promise,我们把其基础的概念进行解释一下:
一、Promise对象有三种状态,分别是:

  • padding:等待中,或进行中,表示还没有得到结果
  • resolved(Fulfilled): 已经完成,表示得到了我们想要的结果,可以继续往下执行
  • rejected: 也表示得到结果,但是由于结果并非我们所愿,因此拒绝执行
new Promise(function(resolve, reject) {
    if(true){ resolve() };
    if(false){ reject() };
})

二、Promise对象中的then方法,可以接收构造函数中处可以接收构造函数中处理的状态变化,并分别对应执行。then方法有2个参数,第一个函数接收resolved状态的执行,第二个参数接收reject状态的执行。

假设:var p = new Promise(resolve,reject)中有两参数,则p.then((data1) => {},(data2) => {}),此中data1是resolve执行处理的结果,而data2是reject执行处理的结果。

then方法也会返回一个Promise对象。因此我们就可以进行then的链式调用了。这也是解决回调的主要方式。比如可以这样写:

new Promise((resolve,reject) => {
    if(true){
        console.log('resolve');
        resolve();
    }else{
        console.log('reject');
        reject();
    }
}).then(() => {
    console.log('then 1');
}).then(() => {
    console.log('then 2');
})

我们在浏览器控制台上打印new Promise((resolve,reject) => {}),从原型中可以看到除了then还有一个catch。
then(null,() => {})就等同于catch(() => {})。从这个结构上来看,then函数其实也是接受两个参数的。我们可以猜想,在执行异步的队列过程中,每一步都是有返回成功与失败的情况,那么则在每一种情况下都可以执行相应的逻辑代码。

// 伪代码
new Promise((resolve,reject) => {})
    .then(() => {}, () => {}) //第一个回调是成功的,第二个是失败的
    .then(() => {}, () => {})
    ...

或者根据Promise的对象实例接口可以分开来写:

// 伪代码
new Promise((resolve,reject) => {})
    .then(() => {}) //成功的
    .catch(() => {}) //失败的
    .then(() => {})
    .catch(() => {})
    ...

三、Promise.all方法是等待所有Promise都执行完之后才执行的回调函数。比如,很多ajax在被按顺序地执行,当所有ajax都返回值了,这时才去执行all。
Promise.all接收一个Promise对象组成的数组作为参数,当这个数组所有的Promise对象状态都变成resolved或者rejected的时候,它才会去调用then方法。并且返回一个Promise对象。

const p1 = new Promise((resolve,reject) => {});
const p2 = new Promise((resolve,reject) => {});
const p3 = new Promise((resolve,reject) => {});
Promise.all([p1,p2,p3]).then(() => {
    console.log('run Promise all callback!');
})

四、Promise.race方法则是在一个Promise对象数组中,只要有一个Promise的状态变成resolved或者rejected,也就是说只要有一个执行完成,就可以调用其then方法了。
同样Promise.race与是接收一个Promise数组作为参数,并返回一个Promise对象。

const p1 = new Promise((resolve,reject) => {});
const p2 = new Promise((resolve,reject) => {});
const p3 = new Promise((resolve,reject) => {});
Promise.race([p1,p2,p3]).then(() => {
    console.log('run Promise all callback!');
})

Promise中的数据传递

Promise的then会执行之后会返回一个Promise对象,而then方法的两个可选参数是两个回调函数,这两个回调函数都有一个可选参数,实际上,这个参数就是从上一个Promise处理后resolved或者rejected的结果值。

new Promise(function(resolve,reject) {
    if(true) resolve('resolve 0');
    else reject('reject 0');
}).then(function(data){
    console.log(data);
    return "resolve 1";
},function(data){
    console.log(data);
    return "reject 1";
}).then(function(data){
    console.log(data);
    return "resolve 2";
},function(data){
    console.log(data);
    return "reject 2";
}).then(function(data){
    console.log(data);
    return "resolve 3";
},function(data){
    console.log(data);
    return "reject 3";
}).then(function(data){
    console.log(data);
},function(data){
    console.log(data);
});

// 打印的结果为:
// resolve 0
// resolve 1
// resolve 2
// resolve 3

从上面的结果来看,可以知道,每一个then方法的返回值,都是下一then方法中的回调函数中的参数值。这样内中的值就可以按着我们想要的顺序一步一步地向下传。

Promise在实际中的应用

Promise的实际应用是非常广泛的,常见的异步编程中,基本上都可以使用Promise来实现。

应用Promise封装ajax

在文章开始的时候,我们做过一个简便的ajax实际,现在我们再利用Promise来实现其过程。

// 简略实现ajax
function ajax(url,type){
    return new Promise((resolve,reject) => {
        var XHR = new XHRHttpRequest();
        XHR.open(type,url,true);
        XHR.send();
        XHR.onreadystatechange = () => {
            if(XHR.readyState == 4) {
                if(XHR.status == 200) {
                    try {
                        let response = JSON.parse(XHR.responseText);
                        resolve(response);
                    } catch(e) {
                        reject(e);
                    }
                } else {
                    reject(new Error(XHR.statusText));
                }
            }
        }
    });
}

为了健壮性,处理了很多可能出现的异常,总之,就是正确的返回结果,就resolve一下,错误的返回结果,就reject一下。并且利用上面的参数传递的方式,将正确结果或者错误信息通过他们的参数传递出来。
然后在调用的时候,可以这样使用:

// 伪代码
ajax(<url>,<type>).then(response => console.log(response), error => console.log(error));

现在所有的库几乎都将ajax请求利用Promise进行了封装,因此我们在使用jQuery等库中的ajax请求时,都可以利用Promise来让我们的代码更加优雅和简单。这也是Promise最常用的一个场景,因此我们一定要非常非常熟悉它,这样才能在应用的时候更加灵活。

所以现在的jquery中的$.ajax实际上是可以这样子调用的:

$.ajax(<url>).done(callback).error(callback);

总结

Promise在实际应用中是非常广泛的,常见的ajax封装,图片异步加载,一些js插件的封装也可以通过异步的实现办法。真正去理解它,还需要不段地练习以相多多查看api文档。这样才能更好利用Promise为你做更多的事。
写此文我参考了一些别人的资料,并添加了一些自己的见解,七凑八凑而成。由于我的知识积累有限,有很多地方可能解释得也是不太清楚。在此给一下参考作者的文章:前端进阶:透彻掌握 Promise 的使用,读这篇就够了

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

推荐阅读更多精彩内容