JavaScript:Promise简介

基本认识

  • Promise提供了一种异步过程的不同写法。回调函数是层层嵌套的,而Promise,类似于函数式的...链式调用。

  • Promise是一种容器,是对异步过程的一种封装。是对异步过程的一种新的写法。在此之前,异步过程一般都是通过回调函数的方式来写的。

同步过程也可以用Promise封装;不过纯粹是多此一举,没有必要。当然有时为了保持链式调用不中断,对同步过程也用Promise包一下也是可以的。比如,Promise(10)。

  • 对于多个异步过程级联的情况,在书写风格上,回调函数是一层层往里套,而Promise是横向链式调用。相对来讲,Promise的链式调用更符合人的习惯,看上去更简洁。

  • 回调函数的实现比较简单,只要函数能够作为另外一个函数的参数就可以了。
    Promise实现相对比较复杂,以前的话一般要引入第三方的库。ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。

  • 回调函数倾向于把异步过程的发起以及结果处理集中在一处;而Promise倾向于将异步过程的发起和结果处理分开来,处理结果通过消息传递。回调函数是一种集中式思维,对于简单的异步过程有优势。Promise是一种分而治之的思想,对于相对复杂的链式调用比较有优势。

  • 回调函数是一种逆向思维。发起者定义结果处理函数,比如successCallback或者failCallback,以函数参数的方式传给异步处理函数。异步过程结束后,由被调用的异步处理函数来回调。
    Promise是一种正向思维。可以理解为一种消息发送。在构建Promise对象的过程中自动发起异步过程。异步过程结束后,发出消息,后续的then或者catch接收消息,处理结果。

同步异步

  • 同步:等待结果处理完成,然后返回。
function syncProcess() {
    console.log('log1');
    console.log('log2');
    console.log('log3');
}
syncProcess();
// log1
// log2
// log3
  • 异步:不会等待结果处理完成,直接返回;当有结果的时候,再来处理。
function asyncProcess() {
    console.log('log1');
    setTimeout(() => {
        console.log('log2');
    }, 0); // 异步,就算是0秒也不会等
    console.log('log3');
}
asyncProcess();
// log1
// log3
// log2

级联的例子

需求:输入一个数,经过加2,乘10,减200三个异步过程(放延时函数中模拟),输出结果。
每个过程都有10%的概率失败。
提供一个有效区间,比如10 ~ 10000,一旦超出这个范围,抛出异常,当然也可以出错退出。

采用回调函数的方式,代码大概是这样子的:

const Min = 10;
const Max = 10000;

function isSucces() {
    const temp = Math.random() * 10;
    if ((Math.random() * 100) < 90) {
        return true;
    } else {
        return false;
    }
}

function add2(intputValue, successCallback, failCallback) {
    setTimeout(() => {
        if (!isSucces()) {
            failCallback(`add2 计算过程出现异常`);
            return;
        }
        const outputVaulue = intputValue + 2;
        if (outputVaulue < Min) {
            failCallback(`add2 结果${outputVaulue}超出下限${Min}`);
            return;
        }
        if (outputVaulue > Max) {
            failCallback(`add2 结果${outputVaulue}超出上限${Max}`);
            return;
        }
        successCallback(outputVaulue);
    }, 500);
}

function multiply10(intputValue, successCallback, failCallback) {
    setTimeout(() => {
        if (!isSucces()) {
            failCallback(`multiply10 计算过程出现异常`);
            return;
        }
        const outputVaulue = intputValue * 10;
        if (outputVaulue < Min) {
            failCallback(`multiply10 结果${outputVaulue}超出下限${Min}`);
            return;
        }
        if (outputVaulue > Max) {
            failCallback(`multiply10 结果${outputVaulue}超出上限${Max}`);
            return;
        }
        successCallback(outputVaulue);
    }, 1500);
}

function minus200(intputValue, successCallback, failCallback) {
    setTimeout(() => {
        if (!isSucces()) {
            failCallback(`minus200 计算过程出现异常`);
            return;
        }
        const outputVaulue = intputValue - 200;
        if (outputVaulue < Min) {
            failCallback(`minus200 结果${outputVaulue}超出下限${Min}`);
            return;
        }
        if (outputVaulue > Max) {
            failCallback(`minus200 结果${outputVaulue}超出上限${Max}`);
            return;
        }
        successCallback(outputVaulue);
    }, 1000);
}

function cascade(inputValue) {
    console.log(`input value: ${inputValue}`);
    add2(inputValue, (add2Vaulue) => {
        multiply10(add2Vaulue, (multiply10Value) => {
            minus200(multiply10Value, (minus200Value) => {
                console.log(`output value: ${minus200Value}`);
            }, (error) => {
                console.log(error); 
            });
        }, (error) => {
            console.log(error); 
        });
    }, (error) => {
        console.log(error);
    });
}

cascade(22);
// input value: 22
// output value: 40 ;  (22 + 2) * 10 - 200 = 40

如果采用Promise包装,代码大概是这样子的:

const Min = 10;
const Max = 10000;

function isSucces() {
    const temp = Math.random() * 10;
    if ((Math.random() * 100) < 90) {
        return true;
    } else {
        return false;
    }
}

function add2(intputValue) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (!isSucces()) {
                reject(`add2 计算过程出现异常`);
                return;
            }
            const outputVaulue = intputValue + 2;
            if (outputVaulue < Min) {
                reject(`add2 结果${outputVaulue}超出下限${Min}`);
                return;
            }
            if (outputVaulue > Max) {
                reject(`add2 结果${outputVaulue}超出上限${Max}`);
                return;
            }
            resolve(outputVaulue);
        }, 500);
    });
}

function multiply10(intputValue) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (!isSucces()) {
                reject(`multiply10 计算过程出现异常`);
                return;
            }
            const outputVaulue = intputValue * 10;
            if (outputVaulue < Min) {
                reject(`multiply10 结果${outputVaulue}超出下限${Min}`);
                return;
            }
            if (outputVaulue > Max) {
                reject(`multiply10 结果${outputVaulue}超出上限${Max}`);
                return;
            }
            resolve(outputVaulue);
        }, 1500);
    });   
}

function minus200(intputValue) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (!isSucces()) {
                reject(`minus200 计算过程出现异常`);
                return;
            }
            const outputVaulue = intputValue - 200;
            if (outputVaulue < Min) {
                reject(`minus200 结果${outputVaulue}超出下限${Min}`);
                return;
            }
            if (outputVaulue > Max) {
                reject(`minus200 结果${outputVaulue}超出上限${Max}`);
                return;
            }
            resolve(outputVaulue);
        }, 1000);
    });
}

function cascade(inputValue) {
    console.log(`input value: ${inputValue}`);
    add2(inputValue).then(multiply10).then(minus200).then((value) => {
        console.log(`output value: ${value}`);
    }).catch(error => {
        console.log(error); 
    });
}

cascade(22);
// input value: 22
// output value: 40 ;  (22 + 2) * 10 - 200 = 40
  • 通过比较上面两份代码,大部分是相同的。

  • 对于每个异步过程,都用一个Promise对象包装一下。大致的结构一般是这样的,外面再包一层容器函数是为了方便传参数:

function container(param) {
    return new Promise(function(resolve, reject) {
        // ... some code
    
        if (/* 异步操作成功 */){
          resolve(value);
        } else {
          reject(error);
        }
    });
}
  • 相对于回调函数,原本用successCallback的地方用resolve代替,原本用failCallback的地方用reject代替。注意,错误不要用throw,用reject代替

  • 差别大的地方是调用,就是上面的cascade函数。通过比较,可以看出,Promise的方式更符合人的认知习惯,看上去更加简洁高效。
    在这种多个异步过程级联的场景下,Promise很有优势。这个就是经常说的,Promise解决异步调用中的“回调地狱”问题。

  • catch放在最后,集中处理错误,这是一个好习惯,也大大简化了编码。

简单调用的例子

简化上面的需求,只要求加2就可以了。前面大部分的代码都差不多,就是最后的调用有点差别。

回调函数的调用,大致是这样的:

function single(inputValue) {
    console.log(`input value: ${inputValue}`);
    add2(inputValue, (outputVaulue) => {
        console.log(`output value: ${outputVaulue}`);
    }, (error) => {
        console.log(error);
    });
}

single(22);
// input value: 22
// output value: 24;    22 + 2 = 24

Promise的调用,大致是这样的:

function single(inputValue) {
    console.log(`input value: ${inputValue}`);
    add2(inputValue).then((value) => {
        console.log(`output value: ${value}`);
    }).catch(error => {
        console.log(error); 
    });
}

single(22);
// input value: 22
// output value: 24;    22 + 2 = 24
  • 在这种简单调用的场景下,回调函数和Promise调用代码的复杂度基本上是差不多的。

  • 回调函数的代码更集中。successCallbackfailCallback都在调用函数的参数位置。

  • Promise方式的调用和结果处理是解耦的,是一种分而治之的思想。异步调用过程在Promise对象创建的时候自动就开始了。
    异步过程结束后,成功的结果就交给then处理,这里做的事情就相当于successCallback。;失败的结果就交给catch处理,这里做的事情就相当于failCallback

  • Promise的状态在任何时候都是确定的,状态变化是单向的,不可逆的。一开始是Pending,异步过程结束时,成功就是Fulfilled;失败就是Rejected

promise-states.png

几个异步过程都成功,再执行下一步的例子

  • 这种场景在平时还是会遇到的。比如,将一张大图分成几张小图下载,小图都下载完后,才能进入下一步的拼接工作。

  • 又比如,用户登录之后,要下载数据,又要进行校验,只有这些过程都成功之后,才能进入下一步。

  • 接上面的需求:就是三个操作一起开始,由于multiply10耗时最长,15秒,那么总的执行时间和这个差不多。

  • 这种场景,有点像比谁跑得慢,最后的结果由最慢的决定。

  • Promise提供了一个类方法all,专门用来应付这种场景。当然,和其他不同的是,这种情况,传递的成功数据是一个数组,而不是一个简单的值。

function all(inputValue) {
    console.log(`input value: ${inputValue}`);
    console.time("all");

    Promise.all([add2(inputValue), multiply10(inputValue), minus200(inputValue)])
    .then(values => {     // 这里传过来的是结果的数组
        values.map(item => {
            console.log(`output value: ${item}`);
        });
        console.timeEnd("all");
    })
    .catch(error => {
        console.log(error);
        console.timeEnd("all");
    });
}

all(222);
// input value: 222
// output value: 224; 222 + 2 = 224
// output value: 2220; 222 * 10 = 2220
// output value: 22; 222 - 200 = 22
// all: 1502.470947265625ms    ; 15秒左右,由最慢的multiply10决定
  • 有了all这个函数帮助,跟简单场景基本上差不多。但是如果采用回调函数的方式就比较麻烦了。

几个异步过程有一个成功,就执行下一步的例子

  • 这种场景在现实中也会遇到,比如连接多个主站,只要有一个连上,就登上了,其他的自然就可以丢弃。

  • 这种场景,有点像比谁跑得快,最后的结果由最快的决定。

  • 接上面的需求,执行时间大约5s左右,由最快的add2决定,另外两个耗时长的不会执行。

  • Promise提供了一个类方法race,专门用来应付这种场景。如果用回调函数来实现,还是比较麻烦的。

function race(inputValue) {
    console.log(`input value: ${inputValue}`);
    console.time("race");

    Promise.race([add2(inputValue), multiply10(inputValue), minus200(inputValue)])
    .then(value => {     
        console.log(`output value: ${value}`);
        console.timeEnd("race");
    })
    .catch(error => {
        console.log(error);
        console.timeEnd("race");
    });
}

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

推荐阅读更多精彩内容