之前在业务开发中使用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