什么是异步编程,为什么要使用异步编程呢?
1. Event Loop
介绍异步编程之前,先来介绍一下事件循环(Event Loop)吧!
我们都知道javascript是一门单线程语言,用来处理用户的交互,实现DOM的增删改等,一次事件循环只处理一个事件响应,使得脚本的执行相对连续,所以就有了事件队列,用来存储待执行的事件,那么事件队列中的事件是从哪里push进来的呢,这就是Event Loop的作用了,它的作用主要是将定时触发器线程,异步HTTP请求线程等满足特定条件下的回调函数push到事件队列中,等到javascript引擎空闲的时候去执行,这就是Event Loop的作用了。
简单来说,就是程序中设置有两个线程,一个负责程序本身的,被称为“主线程”,另一个负责主线程与其他线程(主要是各种I/O操作)的通信,被称为“Event Loop线程”,它属于一个程序结构,用来等待和发送消息事件。
2. 微任务和宏任务
介绍完Event Loop之后,来介绍一下微任务和宏任务。
javascript引擎的执行是有先后顺序的,它会先执行主线程中的任务,等待主线程中的任务执行完毕,就会在微任务队列中查找是否有微任务,有微任务就执行微任务,微任务执行完毕就会在宏任务队列中进行查找是否有宏任务,然后开始执行宏任务。
宏任务:事件队列中的每一个事件都是一个宏任务,例如主代码块,setTimeOut,setInterval,setImmediate等等
微任务:当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件,例如Promise.then就是一个典型的微任务,proness.nextTick等等。
微任务一般一次性执行完毕,宏任务一般需要多次事件循环才能执行完毕。
3. 异步编程的解决方案——回调函数
回调函数是最基本的实现异步编程的解决方案,在es5里面,我们通常使用的就是这种方式来达到异步编程的目的
假设有两个函数fn1,fn2,后者需要等待前者的执行结果,正常的情况下是这样:
fn1();
fn2();
但如果函数fn1是一个很耗时的任务,那么整个程序就会很慢,页面加载时间就会延长,所以就可以考虑使用回调函数的方式,将fn2作为fn1的回调函数
function fn1(callback){
setTimeOut(function(){
//fn1的任务执行代码
callback();
},100)
}
也就是相当于fn1(fn2)这样
这种方式比较简单,容易理解,把同步操作变成了异步操作,fn1不会阻塞程序运行,相当于先执行程序的主要逻辑,将耗时的任务推迟执行,但是这种方式会不利于代码的维护,各个函数嵌套,高度耦合,流程会比较混乱,如果代码过长会不易于理解阅读,而且每个任务只能指定一个回调函数。
4. 异步编程的解决方案——事件监听
事件监听主要是采用事件驱动的模式,任务的执行不取决于代码的顺序,而取决于某个事件是否发生
f1.on('done', f2);
1
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}
即f1若执行就会触发f2的执行,这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
5. 异步编程的解决方案——发布和订阅
假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
优点:该方法的性质与“事件监听”类似,但是明显优于后者,因为我们可以通过查看“消息中心”从而知道存在多少个信号,每个信号有多少订阅者,从而监听程序的运行。
举个例子吧:
//采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
jQuery.subscribe("done", f2); //f2向"信号中心"jQuery订阅"done"信号。
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
//jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
6. 异步编程的解决方案——Promise
重中之重来啦,前面介绍的三种方案除了回调函数其它两种基本没有使用过,哈哈哈哈,还好ES6为我们提供了一种新的异步编程实现方式——Promise,让我们可以快速高效的实现异步编程。
下面就先来介绍一下Promise吧!
- Promise的三种状态
Promise是一个对象,内部会存在一个异步操作,提供了统一的api来获取异步操作的结果,它有三种状态,分别是pending(等待)、resolved(已完成)、rejected(已拒绝),三种状态可以进行转变,可以从等待变成已完成,也可以从等待变成已拒绝,不可逆,一旦从一种状态变成另一种状态之后就是不可逆的了,不能再转变回去。
- Promise所传递的参数
Promise构造函数接收一个函数作为参数,函数的两个参数为resolve和reject,resolve为成功时调用,reject为失败时调用,两个方法会将异步操作结果通过参数传递出去。
- Promise中重要的API
promise.all([p1,p2,p3]):将几个promise对象打包放到一个数组里面,打包完还是promise对象,这种方法必须确保几个promise对象都为resolve状态,如果有一个对象为reject状态,则就会以该失败的结果进行接下来的回调
promise.race([p1,p2,p3]):这个方法其实和promise.all功能是一样的,唯一不同的地方在与,打包的几个promise对象,只要有一个为resolve状态就可以成功返回
- Promise处理错误的三种方式
A:then(resolve,reject),then方法中的第二个回调是失败的时候做的失败时候的事
promise遇到then,也就是resolve和reject的时候是异步的,所以try…catch不起作用
function f(val){
return new Promise((resolve,reject) => {
if(val){
resolve({ name:'小明' },100); //成功时也可以传递一个值,但需要注意的是只能传递一个参数,传两个的话第二个参数是拿不到的
}else{
reject('404'); //传递参数,错误的原因
}
});
}
f(false)
.then( (data, data2) => {
console.log(data2); //undefined
}, e => {
console.log(e); //404
})
//需要注意的是只能传递一个参数,如果传递了两个参数,第二个参数是拿不到的,data2会为undefined
f(true)
.then( (data,data2) => {
console.log(data2); //打印结果为undefined
},e => {
console.log(e);
})
B:使用catch进行捕获错误
function f(val) {
return new Promise((resolve, reject) => {
if (val) {
resolve({ name: "小明" });
} else {
reject("404");
}
});
}
f(true)
.then((data) => {
console.log(data); //{name:'小明'}
return f(false); //返回的promise是失败的话,后面的then对这个失败没有处理的话,就会继续往下走
})
.then(() => {
console.log("我永远不会被输出");
})
.then(
() => {},
(e) => console.log("失败");
) //
.catch((e) => {
//上面处理了错误的话,这个catch就不会运行了
console.log(e); //404
})
.then(() => {
//catch后面可以继续then,但是如果后面的then出错了,跟上一个catch就没有关系了
console.log(e);
return f(false);
})
.catch(); //如果最后一个catch有错误,会无限catch
C:finally方法进行捕获错误,不论成功还是失败,finally中的内容一定会执行,即使返回一个成功的promise,下面的finally也会执行,所以可以利用这个在finally中做一些收尾的工作。
function f(val){
return new Promise((resolve,reject) => {
if(val){
resolve({ name:'小明' });
}else{
reject('404');
}
});
}
f(true)
.then(data => {
console.log(data); //{name:'小明'}
return f(false);
})
.catch(e => {
console.log(e) //404
return f(false); //如果返回的是失败的promise,控制台最后一行会报错uncaught (in promise) 404
})
.finally( () => {
console.log(100) //100
})
- Promise的缺点
Promise的缺点在于:错误需要回调函数来进行捕获、一旦创建就会立即执行,不会中途停止、当处于等待状态的时候无法得知目前进展到哪一阶段,是刚刚开始,还是即将完成。
- Promise如何实现异步编程
它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
f1().then(f2);
function f1(){
var dfd = $.Deferred();
setTimeout(function () {
// f1的任务代码
dfd.resolve();
}, 500);
return dfd.promise;
}
这种方式可以指定多个回调,f1().then(f2).then(f3)…,回调函数变成了链式写法,程序的流程可以看得很清楚。
如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,不用担心是否错过了某个事件或信号。
7. 异步编程的解决方案——Async
Async也是ES6提供的一种解决方案。
async function fn(){return 'hello world'}
async会自动执行,await后面可以是Promise对象,也可以是原始数据类型,返回的是一个promise对象,return后的值会作为Promise对象then方法中成功回调函数中的参数
Async函数的特点:await只能放在async函数里面,语义化更强,await后面可以是Promise对象,也可以是数字,字符串,布尔值等,只要await语句后面Promise对象状态变为reject,那么整个async函数会终止执行。
Async函数处理错误:
可以使用try…catch进行捕获错误,因为await后面跟着的是Promise对象,当有异常的情况下会被Promise内部的catch捕获,而await就是一个then的语法糖,并不会捕获异常,那么就需要使用try…catch进行捕获错误了,并进行相应的逻辑处理
Async实现异步编程:
// 等待执行函数
function sleep(timeout) {
return new Promise((resolve) => {
setTimeout(resolve, timeout)
})
}
// 异步函数
async function test() {
console.log('test start')
await sleep(1000)
console.log('test end')
}
console.log('start')
test()
console.log('end')
JavaScript实现异步编程的方式就介绍这么多,总结了好几个小时,也属于跨天完成了,哈哈哈哈哈,个人觉得Promise实现异步编程更好一些,各有各的好处吧,掌握这些面对面试官的提问一定是没有问题的!