问题
假设我们有一个需求:1. 获取用户所在的城市;2. 根据城市获取天气;3. 根据天气获取出行建议。那我们的代码应该是这样的
getCity(url1, function(){
getWeather(url2, function(weather){
getSuggestion(url3, function(suggestion){
console.log(suggestion)
})
})
})
这就是典型的异步 callback 『回调地狱』,代码层层嵌套可读性很差。关于异步的解决方式可参考这篇文章 Node.js异步漫谈
使用 Promise 是解决上述问题的一种方式,这里我们不去讲如何去使用内置的 Promise,而是带大家手把手写一个 Promise。
思路
我们希望有一个工具,能让我们使用下面的的写法来实现上述功能
promise.then(getCity)
.then(getWeather)
.then(getSuggestion)
整理下思路:
- Tool 是一个对象
- Tool 有 then 这个方法
- 执行 then 方法返回的应该还是 Tool 对象
function Promise(){}
Promise.prototype.then = function(fn){
//todo...
return this
}
var promise = new Promise()
那如何实现异步操作序列执行呢?关键思路如下:
在 promise 对象内容维护一个数组,当执行 promise.then(getCity) .then(getWeather) .then(getSuggestion) 时把这几个函数依次放入数组中。注意此时这些函数并没有执行。
执行promise.resolve()时,会从数组中拿出一个函数去执行。函数执行的过程中在异步操作的结果到来后会再次自动调用 promise.resolve(),触发下一个函数的取出并执行,下一个函数结果到来后再次自动调用promise.resolve() ......,这样就实现了异步链式执行。和原子弹爆炸原理类似。
所以需要对原来的异步函数做一点小小的改动,在数据到来的地方,加一个promise.resolve,用于启动后续函数的执行
function getCity(){
var xhr = new XMLHttpRequest()
xhr.open(url, 'get', true)
xhr.onload = function(){
if (this.status == 200) {
promise.resolve(xhr.responseText) //注意这里的promise.resolve
}
}
xhr.send()
}
现在我们就能实现一个简易的 Promise 了,这里我们先暂不考虑特殊情况:
function Promise(){
this.callbacks = []
}
Promise.prototype.then = function(fn){
this.callbacks.push(fn) //调用 then 时把函数放入数组
return this //返回当前对象供链式调用
}
Promise.prototype.resolve = function(data){
var fn = this.callbacks.shift() //当调用resolve时拿出一个函数
fn&&fn(data) //执行这个函数,并且把resolve的参数做参数
}
var promise = new Promise()
promise.then(getCity)
.then(getWeather)
.then(getSuggestion)
promise.resolve() //启动
function getCity(){
setTimeout(function(){
promise.resolve('杭州')
}, 1000)
}
function getWeather(city){
setTimeout(function(){
promise.resolve(city + ' 晴天')
}, 1000)
}
function getSuggestion(weather){
setTimeout(function(){
console.log(weather + ' 天气不错,可携女友与狗出行')
}, 1000)
}
当然,如果觉得promise.resolve 单独启动一次看起来不舒服,也可以这样执行
getCity()
.then(getWeather)
.then(getSuggestion)
function getCity(){
setTimeout(function(){
promise.resolve('杭州')
}, 1000)
return promise //注意这里
}
实现
到此为止我们已经写了一个简单的 Promise,甚至能满足很大一部分使用需求。但有个问题,每次异步操作可能存在失败的情况,而上面的代码并没有异步函数的失败处理。下面考虑异步的失败处理,原理和上面类似,可以阅读代码动手做个测试
class Promise {
constructor (){
this.callbacks = []
this.oncatch = null
}
reject(result){
this.complete('reject', result)
}
resolve(result){
this.complete('resolve', result)
}
complete(type, result){
if(type==='reject' && this.oncatch){
this.callbacks = []
this.oncatch(result)
}else if(this.callbacks[0]) {
var handlerObj = this.callbacks.shift()
if(handlerObj[type]){
handlerObj[type](result)
}
}
}
then(onsuccess, onfail){
this.callbacks.push({
resolve: onsuccess,
reject: onfail
})
return this
}
catch(onfail){
this.oncatch = onfail
return this
}
}
var promise = new Promise()
fn1().then(fn2, onfn1error)
.then(fn3, onfn2error)
.catch(onerror)
function fn1(){
setTimeout(function(){
if(Math.random()>0.5){
promise.resolve('杭州')
}else{
promise.reject('fn1 error')
}
})
return promise
}
总结
现在我们已经手写了一个 Promise, 当然和浏览器内置对象Promise原理有些差异, 但至少『达到』类似的目的了
加微信号: astak10或者长按识别下方二维码进入前端技术交流群 ,暗号:写代码啦
每日一题,每周资源推荐,精彩博客推荐,工作、笔试、面试经验交流解答,免费直播课,群友轻分享... ,数不尽的福利免费送
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。