写在前面
异步编程对Javascript语言非常重要,在Javascript的发展道路上,异步编程的方法也是一直在不断更新。关于这方面的知识,网上已经有很多成熟的教程和讲解,我将对这些教程进行整理和归纳,整理出异步JS异步编程的几种解决方法。
Javascript的异步执行
Javascript的执行环境是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和超市等待付款一样,前面的人还没有结账,后面的人就只能等着。这种模式的坏处很明显,就是如果有一个任务耗时很长,就会拖延整个程序的执行。为了解决这个问题,Javascript将任务的执行模式分为两种:同步(synchronous)和异步(Asynchronous)。
- 同步模式就是前面提到的这种,后一个任务等待前一个任务结束再执行,程序执行的顺序与任务排列的顺序是一致的,同步的。
- 异步模式,通俗点说,就是前面排队的人告诉后面排队的一个准确时间,这样后面的人就可以利用这段时间去干些别的事情。异步模式可以通过回调函数实现,假设我们需要进行一个耗时的数据请求,在对外部数据发送请求后,程序的执行权将会交给别的任务,一直等到外部数据返回以后,系统才会通知执行回调函数,而回调函数中通常会包括对获取的数据的处理方法。所以,在这个模式下,程序的执行顺序和任务的排列顺序是不一致的,异步的。
Promise
Promise是异步编程的一种解决方案,ES6原生提供了Promise对象。
特点
- Promise对象有三种状态:Pending(进行中),Resolved(已完成),Rejected(已失败),只有异步操作的结果可以决定当前是哪种状态。
- Promise的状态改变可能有两种情况:pending->Resolved或者pending->Rejected,一旦发生,状态就会保持不变。
基本用法
- Promise对象是一个构造函数,用来生成Promise实例。
- Promise构造函数接受一个函数作为参数,该函数的两个参数分别为resolve和reject,他们是两个函数,由JS引擎提供,不用自己部署。
- resolve函数在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
- reject函数在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
- then方法可以接受两个回调函数作为参数,第一个回调函数时Promise对象的状态变为resolved时候调用,第二个是Promise对象状态变为reject时调用,第二个函数可选。
- 如果Promise对象状态变为reject时,会调用catch方法指定的回调函数处理这个错误,所以,除了定义then的第二个参数以外,也可以定义catch方法的回调函数来处理Promise抛出的错误。
例1 (简单例子)
function test(resolve,reject){
var timeout=Math.random()*2;
console.log('设置timeout为'+timeout+'秒');
setTimeout(function () {
if(timeout<1){
console.log('call resolve()');
resolve(" resolved");
}else {
console.log('call reject()');
reject(" rejected");
}
}, timeout*1000);
}
//写法1
var p1=new Promise(test);
p1.then(function(result){
console.log("成功"+result);
},function(result){
console.log("失败"+result);
});
//写法2
var p1=new Promise(test);
var p2=p1.then(function(result){
console.log("成功"+result);
});
var p3=p2.catch(function(result){
console.log("失败"+result);
})
/*
输出:
设置timeout为1.3983493377635763秒
call reject()
失败 rejected
或者:
设置timeout为0.8257771710631485秒
call resolve()
成功 resolved
*/
这个例子中p1是promise实例,方法一和方法二所表达的意思是一样的,只是写法不同,方法1用的就是上面说过的给then传递两个回调函数作为参数的写法。方法二只将then用于函数执行成功的情况,而使用catch来处理函数执行不成功的情况。一般倾向于使用catch方法定义reject状态的回调函数。
上述的test函数执行成功的情况下,即随机数小于1的情况下,我们将调用resolve(' resolved')
,在执行失败的情况下,将调用reject(" rejected")
。变量p1是一个Promise对象,负责执行test函数。当test函数执行成功时,then方法指定的回调函数,将在当前脚本所有同步任务执行完后执行,也就是过了随机数产生的秒数以后执行这里的console.log("成功"+result)
。相同的,当test函数执行失败时,catch方法中指定的回调函数也会在test中的所有任务执行完后执行。
上面提到的console.log("成功"+result)
中的result其实就是等于" resolved",因为如果resolve函数和reject函数带有参数,那么它们的参数就会被传递给回调函数。通常情况下,reject函数的参数会使Error对象的实例,表示抛出的错误;resolve函数的参数可能是一个正常值,也有可能是另一个Promise实例,表示异步操作的结果可能是另一个异步操作。接下来就举一个执行若干个异步任务的例子.
例2 (若干个异步任务)
function add(input){
return new Promise(function(resolve,reject){
var result=input+input;
console.log("add");
setTimeout(function(){
if(result<1000){
resolve(result);
}else {
reject(result);
}
},500);
})
}
function multiply(input){
return new Promise(function(resolve,reject){
var result=input*input;
console.log("multiply");
setTimeout(resolve,500,result);
})
}
var p=new Promise(function(resolve,result){
console.log("start new promise");
resolve(10);
})
p.then(add).then(multiply).then(add).then(add).then(function(result){
console.log("小于1000"+result);
},function(result){
console.log("大于1000"+result);
});
//输出:大于1000,1600
上面这段代码首先定义了add和multiply,然后在add()函数中定义了成功和失败条件,就是当数字小于1000的时候未成功,大于1000的时候为失败。这里最重要的点是add和multiply都会返回一个Promise对象,所以在第一个Promise对象p运行结束后,会开始调用函数add,并将10传递给函数add当作input,此时multiply方法数就会等待add返回的这个新的promise对象发生变化,依次类推,最后的一个then方法指定的回调函数就会等待最后一个add返回的新的promise对象状态发生变化,如果变为Promise对象的状态为成功,就会调用console.log("小于1000"+result);
,反之调用console.log("大于1000"+result);
例3 (all和race)
all方法将用于多个Promise实例,包装成一个新的Promise实例。
var p=Promise.all([p1,p2,p3]);
在使用all的情况下,p的状态由p1,p2,p3决定:
- 只有p1,p2,p3的状态都变成Resolved,p的状态才会变成Resolved。
- p1,p2,p3中只要有一个被Rejected,p的状态就变成了Rejected。
var p1=new Promise(function(resolve,reject){
setTimeout(resolve,1000,"p1 finish in 1s");
});
var p2=new Promise(function(resolve,reject){
setTimeout(resolve,2000,"p2 finish in 2s");
});
Promise.all([p1,p2]).then(function(result){
console.log("end"+result);
});
//直到2s后'p1 finish in 1s'和‘p2 finish in 2s’会被同时输出
上例中,因为只有p1,p2都为Resolved,由它们包装成的新的Promise实例的状态才会变为Resolved,所以直到2秒以后,p1和p2的返回值才会被同时传递给新的promise实例的回调函数。
race方法和all类似,同样将多个Promise实例包装成一个新的Promise实例。
var p=Promise.race([p1,p2,p3]);
在使用race的情况下,只要p1,p2,p3之中有一个率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。
var p1=new Promise(function(resolve,reject){
setTimeout(resolve,1000,"p1 finish in 1s");
});
var p2=new Promise(function(resolve,reject){
setTimeout(resolve,2000,"p2 finish in 2s");
});
Promise.race([p1,p2]).then(function(result){
console.log("end"+result);
});
//1秒后'p1 finish in 1s'和‘p2 finish in 2s’就会被同时输出
上例中因为p1执行的较快,它的状态会率先变为Resolved,所以p1的返回值,就传递给了新的promise实例的回调函数。
Promise应用于Ajax
使用Promise简化Ajax异步处理:
function ajax(method,url,data){
var request=new XMLHttpRequest();
return new Promise(function(resolve,reject{
request.onreadystatechange=function(){
if(request.readyState==4){
if(request.status==200){
resolve(request.responseText);
}else {
reject(request.status);
}
}
});
request.open(method,url);
request.send(data);
})
}
var p=ajax('POST',someUrl);
p.then(function(text){
alert(text);//成功获取到数据
}).catch(function(status){
alert(status); //请求数据失败,获得相应代码
});
这里的method可以是'GET'或者'POST',url是php或者asp文件的地址。
总结
关于Promise的大部分知识已经在本文涵盖到,不得不说廖雪峰和阮一峰老师的讲解很全面,但也许有些同学对于ES6并不熟悉,直接看阮一峰的Promise这章会有点吃力,所以笔者尽可能地在他们的基础上解释地更加细致一些,当然,想要全面地了解Promise的所有内容,还是要静下心来阅读ES6入门这本书。这是JS异步的第一篇,接下来仍会介绍别的JS异步的解决方案。
网友评论