理解promise

之前在业务开发中使用promise时,大部分情况都是声明一个返回promise实例的函数,调用时在then方法里传入一个包含resolve,reject的回调函数,像下面这样:

        function promiseFn () {
            return new Promise((resolve, reject)=>{
                //异步函数 ...
                if (data) {
                    resolve(data)
                } else {
                    reject()
                }
            }) 
        }

        promiseFn.then((data)=>{
            // ...
        }, (err)=>{
            // ...
        })

但是对于以下几点一直没有好好理解:
1、相对于回调函数,promise的优势是什么
2、then方法的链式调用
3、如何理解 “一旦状态改变,就不会在变,任何时候都可以得到这个结果”

后来读到了一篇关于promise的文章,JavaScript Promise:简介,对promise的理解又有了更深的体会,记录一下。

一、与回调函数的比较

1、捕获错误
这里的捕获错误指的是处理异步操作的函数promiseFn发生了未知错误,例如某个变量未定义,这个时候promise.catch可以捕获到这个错误,如果是回调函数需要使用try ... catch

2、回调函数的嵌套
假设有这样一个场景,想在页面中打印一篇文章,文章的每个段落都是通过接口获取,所有段落的请求地址,又是通过另一个接口下发,模拟请求数据如下:

        let data = {
            story: ['title1', 'title2', 'title3', 'title4', 'title5', 'title6'],
            title1: '第一段段落,第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落',
            title2: '第二段段落,第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落',
            title3: '第三段段落,第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落',
            title4: '第四段段落,第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落',
            title5: '第五段段落,第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落',
            title6: '第六段段落,第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落'
        }
        function getData (key, callBack) {
            // ...根据key值,异步获取段落数据, 不同的key值,返回对应的段落数据
            setTimeout(()=>{
                //content模拟异步获取数据成功
                callBack(data[key])
            }, 1000)
            
        }

我们用“key”代替请求的url,setTimeout代替请求的异步行为,由于文章是有顺序的,所以我们要按照顺序请求,即第一段请求回来后,再请求第二段,第二段请求成功后,再请求第三段...

        getData('story', (data)=>{
            getData(data[0], (content)=>{
                console.log(content)
                getData(data[1], (content)=>{
                    console.log(content)
                    // 以此类推,一直到获取到最后一个段落
                    //... getcontent(data[n], (content)=>{
                    //})
                })
            })
        })

这种地狱式的回调应该不是我们大家所接受的,promise却能很好的解决这种嵌套问题

二、then方法的链式调用

在学习promise的时候我们大家都知道,then()会返回一个新的promise实例,套用在我们这个例子中

        let data = {
            story: ['title1', 'title2', 'title3', 'title4', 'title5', 'title6'],
            title1: '第一段段落,第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落',
            title2: '第二段段落,第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落',
            title3: '第三段段落,第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落',
            title4: '第四段段落,第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落',
            title5: '第五段段落,第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落',
            title6: '第六段段落,第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落'
        }
        let time = {
            story: 1000,
            title1: 3000,
            title2: 2000,
            title3: 1000,
            title4: 4000,
            title5: 5000,
            title6: 6000,
        }
        var getData = function (key) {
            return new Promise((resolve, reject) => {
                setTimeout(()=>{
                    resolve(data[key])
                }, time[key])   
            })
        }
        getData('story').then((titleLists)=>{
            // 1秒后,第一个then方法返回 一个数组 ['title1', 'title2', 'title3', 'title4', 'title5', 'title6']
            return titleLists
        })
        .then((titleLists)=>{
            // 1秒后,第二个then方法的回调拿到第一个then方法返回的titleLists
            return getData(titleLists[0]).then((content)=>{
                console.log(content)
            })
        })

then方法的resolve回调可以返回一个基本类型的值,也可以返回一个promise实例
1、return 一个基本类型的数据
上面代码中,第一个then方法返回的promise实例会拿到自己resolve回调返回的数据(titleLists数组),作为入参传递给自己的回调函数
2、return 一个promsie对象
当回调函数返回一个新的promise实例时

        getData('story').then((titleLists)=>{
            //第一个then方法返回 一个promise实例
            return getData(titleLists[0])
        })
        .then((content)=>{
            // 4秒后 第二个then方法的回调会拿到新promise实例状态改变后返回的数据
            console.log(content)
        })

上面代码中,第一个then方法的resolve回调返回的是一个promise实例,第二个then方法会等待这个新的promise实例状态改变后再执行。

总之,我对这里的理解是,then方法返回的promise实例的执行状态,依赖它前一个promise的执行状态,前一个promise状态改变后,当前的promise开始执行;如果前一个promise回调中 返回了一个新的promise实例,那么当前的promise实例又会依赖这个新的promise实例,一直等待最终返回的promise实例状态改变后,才开始执行。

通过这个例子再来理解下“一旦状态改变,就不会在变,任何时候都可以得到这个结果”这句话

三、状态改变后,任何时候都可以得到这个结果

先来看下面这段代码

        let storyPromise = null
        function getContent (index) {
            storyPromise = storyPromise || getData('story')
            
            return storyPromise.then((titleLists)=>{
                return  getData(titleLists[index])
            })
        }
        getContent(0).then((content)=>{
            // 4秒后执行,1秒获取story数据,3秒获取第一段落
            console.log(content)
            return getContent(1)
        })
        .then((content)=>{
            //6秒后执行 4秒获取第一段落,2秒获取第二段落,没有重新获取story
            console.log(content)
        })

获取第一段段落时,还没有获取到titleLists,先获取titleLists,再获取第一段段落,总共用时4秒;当获取第二段段落时,由于storyPromise这个状态已经改变,执行storyPromise的then方法时,会直接返回结果,即立刻执行resolve,没有再次请求story,所以2秒后打印第二段段落。

四、使用promise

下面我们用promise实现打印文章的功能
getData('story')会返回一个数组,循环遍历这个数组去请求段落数据

        getData('story').then((titleLists)=>{
            titleLists.forEach((title)=>{
                getData(title).then(content=>console.log(content))
            })
        })

这种方式有一个问题就是,所有的段落请求是在同一时间发出的,哪个请求用时短就会先打印哪个段落,所以第三段先打印,我们希望的是按顺序打印,要先打印第一段,第二段,第三段...
所以,需要将每次循环获取段落的行为,变为一个promise,代码如下:

        var sequence = Promise.resolve();
        getData('story').then((titleLists) => {
            titleLists.forEach((title)=>{
                sequence = sequence.then(()=>{
                    return getData(title)
                }).then((content)=>{
                    console.log(content)
                })
            })
        })

这里通过循环连续给sequence赋值6次,每次赋值都是一个promise,且都是前一个循环返回的promise,这样就将获取六段内容的行为串起来了,4秒后打印第一段,6秒后打印第二段...

可以用reduce的优化下,省掉了声明sequence

        getData('story').then((titleLists)=>{
            titleLists.reduce((sequence, title) => {
                return sequence.then(()=>{
                    return getData(title)
                }).then((content)=>{
                    console.log(content)
                })
            }, Promise.resolve())
        })

这种写法有一个问题就是所有请求都是串行的,等第六段下载完成需要22秒了,这里还可以怎么优化呢?

可以将获取所有段落的请求同时发出,由于“一旦状态改变,就不会在变,任何时候都可以得到这个结果”,所以,只对打印段落做一个promise的串行处理即可,请求段落提前执行,代码如下:

        getData('story').then((titleLists)=>{
            titleLists.map(getData).reduce((sequence, titlePromise) => {
                return sequence.then(()=>{
                    return titlePromise
                }).then((content)=>{
                    console.log(content)
                })
            }, Promise.resolve())
        })

这里先将所有获取段落的请求变为一个promise数组,再调用reduce,这样的好处是在对sequence赋值的时候,获取“title”的请求可能已经执行完了(即 titlePromise的状态已经改变),优化后,第六段下载完成只需要6秒。

原文讲的更加详情 JavaScript Promise:简介

关于promise的定义、用法推荐阮一峰老师的文章 es6 promise

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