来源:
在之前的前端Javascript的逻辑实现中,我们经常用到回调这个方式。简单的来说,就是当前请求的结果由回调函数处理。假如现在有一个请求用户ID的异步请求如下:
$.ajax({
url:"/get_user_id/",
type:'GET',
success:function(res){
console.log(res);
},
error:function(error){
console.log(error);
}
})
在上面的代码中,通过ajax异步获取到用户id之后,返回的数据交给success函数处理,这种可以理解为简单的回调处理。
然后现在假如我们需要对拿到的数据做二次异步请求处理,假如是用这个ID去获取用户的信息,则代码会变成下面这样:
$.ajax({
url:"/get_user_id/",
type:'GET',
success:function(res){
let userId = res;
$.ajax({
url:"/get_user_info/",
type:'GET',
success:function(res){
console.log("user_info:" , res);
},
error:function(error){
console.log(error);
}
})
},
error:function(error){
console.log(error);
}
})
由于获取用户信息之前我们需要获取到用户的ID,那么第二步的操作就必须等待第一步操作完成,则代码会变成上述那样。如果调用的层数越来越多呢?那这个代码的样式就会变得更加臃肿,俗称“圣诞树模式”。
解决:
1: 在实现上述功能的过程中,直接如此通过上次的回调结果执行下次的操作原则上是正确的,但是代码太过ugly,ES6针对类似情况,集合了新出的标准,即Promise
Promise,直译“承诺”。那也就是说Promise代表的是在未来发生的事情。首先我们用Promise实现上述的功能,先有个整体印象:
let userId = function(){
return new Promise(function(resolve, reject){
$.ajax({
url:"/get_user_id/",
type:'GET',
success:function(res){
resolve(res);
},
error:function(error){
reject(error);
}
})
});
}
userId().then(function(res){
//上一步回调成功
$.ajax({
url:"/get_user_info/",
type:"GET",
success:function(res){
console.log(res);
},
error:function(error){
console.log(error);
}
})
}, function(error){
console.log("read user_id failed!");
});
可以看到,Promise相比于普通回调的差异就是他对返回的回调操作进行了封装。在官方文档中介绍由:Promise加载异步操作有三种状态:Pending, Resolve,Reject。Pending可以转换为Resolve或者Reject,其余没有相关的转换过程。如果异步操作成功,则Pending变为Resolve,否则Reject(当然也可以投出错误)。
在上面的例子中,新建了Promise对象之后,promise根据回调的状态选择不同的返回值,然后回调的then方法会等待回调值的产生,根据不同的状态选择不同的回调函数:
then(function(), function())这两个function分别对应的是返回状态是Resolve和Reject的处理函数。所以在上面的程序中我们会在第一个函数中再次执行下一次异步操作。
整体的概念就是相比于之前的异步回调操作,Promise进行了封装操作,使得函数的执行更加符合人类的思维。
2: Promise神奇之处不止上面的操作,看另外一个例子,假如现在有A,B两个操作,C操作要等待A和B都完成之后才能执行,则一般的处理流程如下:
let ra = A()
let rb = B()
if(ra && rb){
C();
}
那么C的执行必须等到A,B两个函数都执行完成之后才能执行,则执行时间为A+B。Promise提供了All方法可以实现多个异步操作的同步管理。看样例:
var ra = new Promise(function (resolve) {
setTimeout(function () {
resolve("result of A");
}, 3000);
});
var rb = new Promise(function (resolve) {
setTimeout(function () {
resolve("result of B");
}, 1000);
});
Promise.all([ra, rb]).then(function (final_result) {
console.log(final_result);
});
上述代码的数据结果是["result of A","result of B"]
此时Promise.all会根据观测的多个promise对象的返回值进行操作,上述的返回结果决定于最长的返回时间(A)。即Promise.all会等到所有的promise都回调完成之后(都变成resolve或者有一个变为reject)才会执行对应的处理函数。
3:Promise还有一个操作Promise.race,它的操作对象依旧是多个promise对象,不过和all不同的是,race观测的对象只要有一个发生了状态改变,则就会将该状态改变返回给race。假如:
var p = Promise.race([p1, p2, p3]);
p.then(ret => { console.log(ret); })
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。然后会将返回值传递给p的回调函数。
官方文档有一个例子,做超时判断的:
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));
上述操作如果说那个fetch命令在5S内没有结束,那么整个操作就会结束。
深入探究
既然promise这么神奇,那么内部是怎么实现的呢?楼主比较菜,阅读了网上各大神的blog,简单的说一下,欢迎大家拍砖。
1:首先说呢,很多人说promise的实现类似于设计模式中的观察者模式,通过在函数中注册对应的操作函数,如果相关的数据发生了变化,则通知已经注册的函数进行对应的操作。
CP网上的例子讲吧(感觉类比的比较清楚😁):
最简单的如下:
function getUserId(){
return new MyPromise(function(resolve){
$.ajax({
url:'/users/getUserId',
type:'GET',
success:function(res) {
console.log(res);
resolve(res);
},
error:function(res){
console.log(res);
}
})
})
}
getUserId().then(function(id) {
console.log(id);
});
然后基本版的MyPromise如下:
function MyPromise(fn){
var value = null;
var callbacks = [];
this.then = function(func){
callbacks.push(func);
};
function resolve(value){
setTimeout(function(){
callbacks.forEach(function(cal){
cal(value);
});
},0);
}
fn(resolve);
}
一行一行解释来看:
当我们创建MyPromise对象的时候,传入的参数是一个函数function。然后我们在MyPromise的构造函数中看到
value:存储的要处理的变量
callbacks:存储的需要进行注册的回调函数
当new一个MyPromise的时候,我们传入function,然后我们给该函数传递一个函数参数resolve。然后呢,函数就去执行对应的异步操作,在这里就是ajax请求。然后当前的MyPromise对象执行then方法,then方法将当前的操作函数注册到我们的callbacks数组中,当前方法即为function(id) {console.log(id);}。当异步操作完成之后,mypromise对象执行对应的resolve方法,看函数定义,此时,执行的操作是,对当前已经注册的回调方法传入刚刚采集的数据value,然后依次对每个函数进行操作。然后呢就可以看到对应的输出了。
其实这种注册然后执行的模式确实挺像观察者模式的,通过在本身注册操作函数,当发生变化的时候,执行对应的函数。
解释:resolve函数里面的setTimeout操作,是为了一种情况设定的,即如果当前的异步操作变成了同步操作,那么就会直接执行resolve函数,则对应的执行函数就没有注册到回调函数数组中。所以就给resolve加上settimeout实现将当前操作移动到时间循环的最后执行,保证所有的方法等能够得道注册。
2:当我们执行了注册函数的操作之后,当前函数才可以被执行,如果函数异步操作已经完成,那么之后的调用then注册的函数也都没有执行了。反看promise的实现,可以通过不断的调用then方法不断的去执行注册函数。所以呢,在这一步就加入了状态的概念
看代码:
function MyPromise(fn){
var value = null;
var callbacks = [];
var state = 'pending';
this.then = function(func){
if(state === 'pending'){
callbacks.push(func);
return this;
}
func(value);
return this;
};
function resolve(newValue){
value = newValue;
state = 'fulfilled';
setTimeout(function(){
callbacks.forEach(function(cal){
cal(value);
});
},0);
}
fn(resolve);
}
可以看到对应的支持操作是如果当前已经resolve了,那么后续的then操作就会直接调用执行对应的回调函数。解决了前面提到的后续注册函数不能执行的问题。
3:继续学习网上大神的操作。然后假如then函数传递了一个新的promise进来,那么如何操作呢?ES6的promise的then函数如果传递了新的promise进来,那么后续的then操作则会等待当前的异步操作对象完成之后再次执行。
传入then的函数的参数是一个promise的情况是上一个回调函数resolve返回的是一个promise。所谓的链式操作吧。
然后链式操作的过程是上一个resolve之后开始下一个promise,所以重点就是把前后的promise进行连接
然后既然是新的promise那么我们就给他在执行then的时候返回一个新的promise对象回去。
function MyPromise(fn){
var value = null;
var callbacks = [];
var state = 'pending';
this.then = function(func){
return new MyPromise(function(resolve){
handle({
func:func,
resolve:resolve
});
});
};
function handle(callback){
if(state === "pending"){
callbacks.push(callback);
return;
}
if(!callback.resolve){
callback.resolve(value);
return;
}
var ret = callback.func(value);
callback.resolve(ret);
}
function resolve(newValue){
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve);
return;
}
}
state = 'fulfilled';
value = newValue;
setTimeout(function () {
callbacks.forEach(function (callback) {
handle(callback);
});
}, 0);
}
fn(resolve);
}
我本地执行大神的代码没有实现想要的效果,还是传送门http://blog.csdn.net/qq_22844483/article/details/73655738
网友评论