JavaScript:Promise的基础套路

关于Promise的基本内容,已经写过一篇文章。基本的会用,偶尔也用过,不过知识点比较多,并且代码在外面套了好几层,感觉也比较复杂,一直理不顺头绪。最近看了下面链接的几篇文章,感觉很不错,对于Promise基本概念的理解比以前要清晰一点了。
Promise的知识点很多,这里是一次从多到少的收敛过程,写了几点平时可能会用到的最基础的用法。

大白话讲解Promise(一)
这篇文章写得还是比较简单直接的,对于理解概念很有帮助,推荐好好看看。

廖雪峰的Promise
这篇文章对于概念的分解还是比较详细的,很不错。里面的例子直接copy到chrome的控制台会报错,不过简单修改一下就可以了。本文的例子基本上都是从这里简单修改来的。

适用Promise的三种场景?

  • 场景1: 级联调用,就是几个调用依次发生的场景。这个就是有名的回调地狱。通用的套路是,新建一个Promise,启动流程,其他的Promise,放在级联的then函数中。

  • 场景2:几个异步回调都成功,然后再进行下一步操作,比如图片比较大,分成几个小图下载,然后拼接,就是一个典型的场景。这里用Promise.all这个函数就很方便。

  • 场景3: 几个异步回调,只要有一个成功,就可以进行下一步。比如像主站和另外两个备用站点同时请求一张图片,只要有一个有相应,其他几个就可以放弃。这里用Promise.race这个函数就很方便。

Promise和回调地狱是什么关系?

  • 单个的回调函数不会形成回调地狱。串行的回调,也就是上面提到的场景1,层层嵌套,导致代码越套越深,结构复杂,才形成了回调地狱。

  • Promise并不是替代回调函数,而是对回调函数的一层封装。比如,有名的setTimeOut函数,Promise并不能替代它,只是对它进行了一层包装。

  • 当然,也不是简单的包装,最本质的变化,就是将对回调函数的调用,修改成了消息发送。将对结果的处理剥离出去,交给后续的对象处理。
    resolve(data); reject(error);这两个理解为消息发送函数更确切一点。一方面,将所处的Promise的状态由pending改为resolved或者reject。另一方面,生成一个新的Promise,并返回,同时将数据作为参数传递出去。

  • Promise包装的函数,同步和异步都是可以的,没有本质区别,按照一套思路去理解就可以了。同步的代码基本不需要包装,本来就简单,比如用Promise.resolve(初始值)发起一个流程。大多数情况,Promise包装的都是异步对象。本质是为了把层层嵌套异步回调代码,回调地狱 callback hell,转变为串行的链式调用。

  • Promise对象新建的时候,状态是Pending,可以理解为“正在进行中,需要等待”。
    resolve(data);一下,状态变成了Resolved,链式调用的控制权就转移到了下一级的then(data => {callback(data)})函数中。这里就是传统的回调函数执行的地方。
    reject(error);一下,状态变成了Rejected,链式调用的控制权就转移到了catch(error => { })函数中,一般建议放在最后面。这里就是集中处理错误的地方。
    所以,不要纠结同步还是异步,将重点放在Promise的对象的状态以及链式调用的控制权的转移上面。

Promise的编程范式:面向对象 or 函数式?

  • 创建Promise对象,需要用到new关键字,并且名字也一般叫做对象。不过,Promise并不是面向对象的编程,更多的还是函数式。范畴或者集合,用类来模拟。如果能够提供一个静态函数Promise.of来替代new关键字,函数式的味道就更浓厚一点。

  • 链式调用,比较方便,要做到这一点,每个函数,比如then,catch等,都返回Promise对象,叫范畴或者集合更确切一点。不过,每一个Promise都是不同的,这个符合函数式编程的习惯:生成新的对象,而不是改变对象本身。之间的联系主要是数据的传递,自身内部状态的变化。

Promise对异步调用的常用封装套路:

function promiseFunction(resolve, reject) {
    let timeOut = Math.random() * 2;
    let flag = (timeOut < 1);
    let callback = () => {
        if (flag) {
            console.log('success...');
            return resolve('data: 200 OK'); // 加个return是个好习惯
        } else {
            console.log('fail...');
            return reject('error: timeout in ' + timeOut + ' seconds.'); // 加个return是个好习惯
        }
    };
    // start process
    console.log('start async function ...');
    setTimeout(callback, (timeOut * 1000));
}

function asyncFunction () {
    let promise = new Promise(promiseFunction);
    return promise;
}
  • 对callback的改造:
    一般的callback,应该定义对结果的处理过程以及出错的处理过程。Promise剥离了这些具体的处理过程,改成了发消息。成功就发送resolve(data);失败就发送reject(error);
    这里要注意的一点是,resolve(data);reject(error);,这两个消息函数至少要用一个,当然多用是没关系的,否则流程就启动不了。
    resolve(data);reject(error);之后,流程就交给后面的then或者catch来处理了,这之后的代码都不会执行。所以resolve(data);reject(error);前面加个return,可以更加明确这种意图,是个好习惯

  • 对流程函数的封装:
    一般的异步过程都分为两步:在主线程发起异步过程,然后主线程就去做其他事情了;具体的工作一般在工作者线程中执行。工作完成后,调用callback,通知主线程,让主线程拿着结果做想做的事。
    Promise把发起异步过程,(这里用setTimeout函数模拟),这个步骤封装在一个函数中,(就是Promise构造函数的executor参数),这个函数格式固定,参数是resolve, reject,这里用一个名字promiseFunction把他列出来。

  • 函数式编程范式的封装:
    函数式编程一般会简化为范畴或者集合的操作,数据和函数都包裹在一个集合容器中。
    这里用Promise类的对象来模拟,这也是导致误认为面向对象编程的原因。以固定套路的函数(resolve, reject)作为参数,通过Promise构造函数,用new关键字,得到了一个promise对象,完成封装。
    所以,Promise对象在构建过程中,异步流程就已经发起了,Promise对象的状态就是pending===这个也是参考文章大白话讲解Promise(一)中提到的注意点
    如果不resolve或者reject一下,(throw error跟reject是同一个意思),Promise对象就一直pending,这个链式调用就一直停着,动不了。

  • 接口函数的封装:
    这层封装是从软件工程的角度,方便使用者使用的角度来做的。
    函数式编程用来完成跟界面和业务无关的具体功能是比较好的,操作的也是集合。但是一般来说,业务层用面向对象的模式进行设计的,调用函数式编程的集合不是很方便。所以,封装成功能型的函数,(也就是上面的asyncFunction函数),用起来就比较顺手了。
    当然,把生成的Promise对象return出去,是为了方便链式调用。

在实际使用中,可以写得简洁一些,上面的代码可以精简如下:

function asyncFunction () {
    return new Promise(function(resolve, reject) {
        let timeOut = Math.random() * 2;
        let flag = (timeOut < 1);
        // start process
        console.log('start async function ...');
        setTimeout(() => {
            if (flag) {
                console.log('success...');
               return  resolve('data: 200 OK'); // 加个return是个好习惯
            } else {
                console.log('fail...');
                return reject('error: timeout in ' + timeOut + ' seconds.'); // 加个return是个好习惯
            }
        }, (timeOut * 1000));
    });
}

Promise简单使用的套路:

  • 所谓简单使用,就考虑最简单的异步调用,(a)发起流程,等结果;(b)成功,处理结果;(c)失败,报错

  • Promise.prototype.then(callback)就是用来处理成功结果的回调函数,具体的处理过程在这里定义。
    then函数的第二个参数可以用来处理出错结果,不过一般都不用。在这里处理错误是一种很差的方法。
    then函数会返回一个Promise对象。这个前面已经提过,这个Promise对象是then函数内部新建的,和流程发起的那个Promise对象是不一样的。

  • then函数一般建议写同步过程,这里是执行以往回调函数功能的地方。在流程最后,把接收到的datareturn回去是个好习惯,万一后面还有其他的then要用,数据data就可以顺着节点传一下,不至于中断。
    return data; 和 return Promise.resolve(data);是等价的,内部估计会装换一下。所以本质上还是return了一个Promise对象
    如果是异步过程,建议新建一个Promise对象包装一下,再return,这样就形成了串行依赖关系。
    如果什么都不return,那么内部会新建一个没有值的Promise对象,相当于return Promise.resolve(undefined);;所以这种情况,链式调用还可以继续,但是参数传递会中断。

then函数中一般不建议放异步过程,这样做会增加理解的难度。下面这篇文章中就有这样的例子:
Promise.prototype.then()

  • Promise.prototype.catch()本质上是.then(null, rejection)的别名,这里是集中处理错误的地方,一般放在链式调用所有then的后面。这样可以捕获流程中的所有错误,包括主流程的以及后续then中出现的错误。

  • 再简单的过程,(一般是异步过程,同步过程也一样,比如直通),也用函数包一下,(对外的接口统一为函数,把Promise对象隐藏起来),至少给一个then,最后跟一个catch。将原来一个整体的异步调用(流程发起,成功,失败)转化成了3级的链式调用,代码结构清晰很多。
    比如上面通过Promise封装好的异步函数,典型的使用套路如下:

asyncFunction().then((data) => {
    console.log(data);
    return data; // 把数据往下传,是个好习惯
    }).catch((error) => {
    console.log(error);
});

场景1: 级联调用使用的套路:

  • 这就是著名的回调地狱,callback hell,采用Promise包装之后,可以改为简洁的链式调用。其实就是用级联的then来体现这种级联的调用关系。
    job1.then(job2).then(job3).catch(handleError);
    其中,job1、job2和job3都是封装了Promise对象的函数

  • 注意,这里的job1、job2和job3都要求是一个参数的函数。因为,不论是resolve,还是reject,传值的参数个数都只有一个。这个可以联想到函数式编程中的柯里化,每次只传一个参数,简单直接。
    如果想传个参数怎么办呢?将所有参数包装成一个对象就可以了,resolve和reject都是可以传递对象的,只是个数规定为一个而已。不过运算的时候需要解析参数,再传值的时候需要重新组装参数,相对就麻烦一点了。

  • 静态函数Promise.resolve(data)可以快捷地返回一个Promise对象,一般可以用在链式调用的开头,提供初始值。

  • 一般来说,在新建的Promise对象中发起异步流程,resolve(data)消息发出之后,then(data => { callback(data) })接收到数据data,将原先的callback在这里执行就好了。现在这里不放回调代码,而是return一个新的Promise对象,形成一个依赖链。

下面这个例子,就是先用Promise包装了一个异步过程,(乘10的函数);以及一个同步过程,(加100的函数);用随机数的方式,模拟过程失败的情况。然后通过then函数级联的方式定义依赖过程。最后用catch捕捉过程中遇到的错误。

Step1:用Promise封装过程

// input*10的计算结果; setTimeout模拟异步过程;
function multiply10(input) {
    return new Promise(function (resolve, reject) {
        let temp = Math.random() * 1.2;
        let flag = (temp < 1);
        console.log('calculating ' + input + ' x ' + 10 + '...');
        setTimeout(() => {
            if (flag) {
                return resolve(input * 10);
            } else {
                return reject('multiply error:' + temp);
            }
        }, 500);
    });
}

// input+100的计算结果;同步过程
function add100(input) {
    return new Promise(function (resolve, reject) {
        let temp = Math.random() * 1.2;
        let flag = (temp < 1);
        console.log('calculating ' + input + ' + ' + 100 + '...');
        if (flag) {
            return resolve(input + 100);
        } else {
            return reject('add error:' + temp);
        }
    });
}

Step2:用then函数级联的方式定义依赖过程:

// 结果是3300,或者报错
Promise.resolve(32).then(multiply10).then(multiply10).then(add100).then(data => {
    console.log('Got value: ' + data);
    return data;
}).catch(error => {
    console.log(error);
});

// 结果是1160,或者报错
Promise.resolve(6).then(add100).then(multiply10).then(add100).then(data => {
    console.log('Got value: ' + data);
    return data;
}).catch(error => {
    console.log(error);
});

// ... ... 还能写出很多的组合情况
  • 一般情况then(data => { callback(data) })函数的主要工作是接收数据,然后执行原来的回调函数。
    这里一般放同步代码;如果是异步代码,就像上面那样,可以新建一个Promise对象并返回,形成一个调用链。

  • 如果既有同步的回调代码需要执行,又有异步的过程需要包装链接,怎么办呢?比如上面的例子,增加显示中间过程的功能。
    可以考虑用两个级联的then函数分别来做这两件事。

一个then用来执行同步的回调函数。这里要注意将要传递的data return出去,不然,整个链式调用参数传递会中断。

.then(data => { 
    callbacek(data);
    return data;   // 这里要把接收到的data传出去,不然整个调用链的参数传递会断掉。
})

一个then用来包装异步过程的,并把这个新建的Promise return出去,形成异步过程依赖链。

.then(data => { 
    return new Promise(function(resolve, reject) {
        let flag = ((Math.random() * 2) < 1);  // demo flag
        let newData = data + 1; // demo data 
        setTimeout(() => { // demo async function
            if (flag) {
                resolve(newData);
            } else {
                reject(new Error('error message'));
            }
        }, 10);
    });
})
  • 上面的新需求可以按照下面的套路简单实现:
// 结果是9880,或者报错
Promise.resolve(888).then(add100).then(data => {
    console.log('add100之后的结果为:' + data);
    return data;
}).then(multiply10).then(data => {
    console.log('multiply10之后的结果为:' + data);
    return data;
}).then(data => {
    console.log('Got value: ' + data);
    return data; // 这里是最后了,不return data对流程没影响。不过谁知道以后会不会加新的节点,return一下还是好的。
}).catch(error => {
    console.log(error);
});

场景2: 几个异步回调都成功,然后再进行下一步操作
场景3: 几个异步回调,只要有一个成功,就可以进行下一步

  • 这两种的实现方式很类似,可以按照一种套路模式

  • 只考虑异步过程,不考虑同步过程

  • 这里用到了两个静态函数,分别是Promise.all(),(场景2);Promise.race(),(场景3);

  • 这两个函数的参数都是一个数组,数组的成员是Promise对象。

  • 后面跟一个then和catch,就像是普通的使用场景。

  • Promise.all()成功时,传递过来的是一个结果数组;失败时,传递过来的是出错对应的值。

  • 这里没有链式调用,一长串的数据传递,所以这里的函数的参数个数没有限制。不过,统一为一个是比较好的习惯。就算没有参数,给个空对象也可以,万一以后要传呢

  • 大白话讲解Promise(一)
    Promise.all(),「谁跑的慢,以谁为准执行回调」;
    Promise.race(),「谁跑的快,以谁为准执行回调」;
    这个表述还是形象而准确的。

function asyncFunction1(data = null) {
    return new Promise(function(resolve, reject) {
        let temp = Math.random() * 2;
        let flag = (temp < 1);
        // start process
        console.log('start asyncfunction1 ...');
        setTimeout(() => {
            if (flag) {
                if (data) {
                    return resolve(data);
                } else {
                    return resolve('success: asyncfunction1===');
                }
            } else {
                return reject(`fail:asyncfunction1; temp:${temp}`);
            }
        }, 500);
    });
}

function asyncFunction2(data = null) {
    return new Promise(function(resolve, reject) {
        let temp = Math.random() * 2;
        let flag = (temp < 1);
        // start process
        console.log('start asyncfunction2 ...');
        setTimeout(() => {
            if (flag) {
                if (!data) {
                    return resolve(data);
                } else {
                    return resolve('success: asyncfunction2');
                }
            } else {
                return reject(`fail:asyncfunction2; temp:${temp}`);
            }
        }, 500);
    });
}

// 这里传过来的是成功结果的数组
Promise.all([asyncFunction1(), asyncFunction2()]).then(array => {
    console.log(JSON.stringify(array));
    return array; // 这里传递的是数组,比较特殊
}).catch(error => {
    console.log(error);
});

// 结果是success: asyncfunction1;跑得比较快
Promise.race([asyncFunction1(), asyncFunction2()]).then(data => {
    console.log(data);
    return data;
}).catch(error => {
    console.log(error);
});

done、finally、success、fail等其他内容呢?

  • 这些一些框架提供的便利方法,当然,如果有需要,也可以自己实现。

  • 上面这些是基本的使用套路,简单直接。一个基础应用加三个典型场景,可以应付平时大多数的异步过程。

  • 当然,Promise还有很多高级而灵活的用法。下面推荐几篇文章,里面的内容很丰富。

Promise 对象(阮一峰)

JavaScript Promise迷你书(中文版)

Promise MDN

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