我们都知道Javascript是「单线程」的,所谓「单线程」就是指一次只能完成一个任务,如果有多个任务,则必须排队,等上一个任务完成后,才能轮到下一个任务执行。
但是这样的模式就有一个问题了,如果上一个任务的执行时间很长,甚至造成死循环的话,那下一个任务是不是要等到天荒地老?
为了解决这个问题,于是乎Javascript就将任务的执行模式分为了:「同步模式」和「异步模式」。
「同步模式」,即上述的模式,一个任务执行完再执行下一个,程序的执行是有顺序的。
「异步模式」则是每一个任务都有一个或者多个「回调函数」,前一个任务执行完,不是执行下一个任务,而是执行「回调函数」,后一个任务也不是等前面那个任务执行完才开始执行,所以程序的执行顺序与任务的排列顺序不是一致的。比如在耗时很长的任务中,我们就需要使用异步,最好的例子就是Ajax操作,因为执行环境是单线程的,如果同步执行所有http请求,那么服务器性能会大幅下降,很快失去响应。
一.回调函数
这是处理异步的最基本的方式,所谓「回调函数」,就是把一个函数A传给函数B调用,则函数A就是「回调函数」。
假如有两个函数f1和f2(f2等待f1的执行结果),如果f1是需要花费很长时间,可以考虑把f2写成f1的回调函数。
function f1(callback) {
setTimeout(() => {
// f1的任务代码
f2();
}, 1000)
}
f1(f2);
采用这种方式,我们就可以把同步操作变成异步操作,f1不会堵塞程序,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单,容易理解和部署,不过缺点是不利于代码维护,高耦合,流程混乱,并且每个任务只能有一个回调函数。
所说回调的几种写法:
(1)具体回调写法
function getInfo(callback) {
callback.call(null, 'name: Jason');
}
function userInfo(info) {
console.log(info)
}
getInfo(userInfo)
(2)匿名回调写法
function getInfo(callback) {
callback.call(null, 'name: Jason');
}
getInfo.call(null, function(info) {
console.log(info)
})
(3)回调地狱
getInfo(function(info) {
console.log(info);
saveInfo(info, function() {
getAnotherInfo(function(anotherInfo) {
console.log(anoterInfo)
saveInfo(function() {});
})
})
})
像这种多层匿名函数的嵌套,就会让人很难理解,这种多层嵌套称为「回调地狱」。
二. 事件监听
另一种思路是采取事件驱动的模式,任务的执行不取决于代码顺序,而取决于这个事件是否发生。
f1.done('done', f2);
上面代码的意思就是在f1完成后在调用f2。
我们对上面代码改写。
function f1() {
setTimeout(function() {
// f1代码
f1.trigger('done');
},1000);
}
上述代码是指当f1完成后trigger('done'),触发done事件,从而开始执行f2。
这种方法比较容易理解,可以绑定多个事件,一个事件可以指定多个回调函数,去耦合,有利于代码模块化。缺点是整个程序编程事件驱动,运行流程不清晰。
三. 发布/订阅模式
上一节的「事件」我们可以理解为「信号」。我们假设有一个「信号中心」,来把控这些信号,当一个任务完成后,向「信号中心」“发布”一个信号,其他任务可以向这个「信号中心」“订阅”这个信号,从而知道自己的任务什么时候开始执行。
首先我们用f2向「Jquery订阅中心」订阅“done”信号。
jQuery.subscribe("done", f2);
然后对f1进行改写:
function f1() {
setTimeout(function() {
// f1代码
jQuery.publish('done');
}, 1000)
}
jQuery.publish('done')的意思就是在f1执行完“发布”一个“done”信号,从而引发f2的执行。
此外我们还可以取消“订阅”。
jQuery.unsubscribe("done", f2);
这种方式跟「事件监听」方式类似,但是优于后者,因为我们可以看到「消息中心」,有多少「信号」。
四. Promise
简单来说,Promise思想就是在每一个异步任务后返回一个Promise对象,该对象有个一个then方法,允许有一个回调函数。
// f1和f2可以写成
f1.then(f2)
使用Promise我们解决了上述什么问题呢?
(1)解决了不知道该如何使用回调函数
promise前面是操作,.then里面就是对上一步操作的结果进行处理,then的第一个参数是上一步操作成功后调用的回调函数,第二个参数是上一步操作失败后调用的操作。
f1.then(function() {}, function() {})
(2)解决了回调地狱
我们可以通过不断的then来比较清晰地看出逻辑,不存在“嵌套”一说。
f1.then(function() {}, function() {}).then(function() {}, function() {})
我们使用Promise来解决一下上面的回调地狱问题。
function getInfo() {
return new Promise((resolve, reject) => {
console.log('第一次获取信息');
resolve('name: Jason');
});
}
function printInfo(info) {
return new Promise((resolve, reject) => {
console.log(info);
resolve();
})
}
function getAnotherInfo() {
return new Promise((resolve, reject) => {
console.log('第二次获取信息')
resolve('name: Jack');
})
}
getInfo().then(printInfo)
.then(getAnotherInfo)
.then(printInfo)
// 第一次获取信息
// name: Jason
// 第二次获取信息
// name: Jack
.then里第一个参数,是成功函数(resolve),这个函数的参数就是上一个resolve中传入的实参
function f1() {
return new Promise((resolve, reject) => {
resolve('我是f1中的实参')
})
}
function f2(data) {
console.log(data);
}
f1().then(f2);
log结果
同样的,.then后面的第二个参数是失败函数,失败函数的参数也是由上一个reject函数
传入的实参。
如果不给resolve和reject传值,那么你通过then得到的参数就是undefined。
function f1() {
return new Promise((resolve, reject) => {
resolve(); // 我们不传参
})
}
function f2(data) {
console.log(data);
}
f1().then(f2);
log结果
如果我们想用多个then操作,那么我们就要在每一个resolve或者reject中传入参数。就像我们小时候玩的「击鼓传花」的游戏一样。
function f1() {
return new Promise((resolve, reject) => {
resolve('这是f1的实参');
})
}
function f2(data) {
return new Promise((resolve, reject) => {
resolve(data)
})
}
function f3(data) {
return new Promise((resolve, reject) => {
console.log(data)
resolve(data)
})
}
f1().then(f2).then(f3);
log结果
上述代码,f1传入的实参「这是f1的实参」可以通过then传到f3,然后在f3中log出来。
then里面不管调用的是成功函数(resolve)还是失败函数(reject),下一个then都会调用成功函数,除非在reject函数里直接reject。
function f1(name) {
return new Promise((resolve, reject) => {
if(name === "Jason") {
resolve('success');
} else {
reject('fail')
}
})
}
function f2(data) {
return new Promise((resolve, reject) => {
console.log('这里是f2')
})
}
function f3(data) {
console.log('这里是f3')
}
function f4(data) {
console.log('这里是f4')
return new Promise((resolve, reject) => {
resolve()
})
}
f1('Jack').then(f2, f3).then(f4);
log结果
如图,第一个.then进入f3这个失败函数后,没有返回任何东西,浏览器就默认处理完毕了,然后就直接到了第二个.then的f4这个成功函数了。那么如何让浏览器知道我们还没有搞定这个失败,不让下一个.then的成功函数不执行呢?
我们在第一个.then的失败函数f3增加reject。
function f1(name) {
return new Promise((resolve, reject) => {
if(name === "Jason") {
resolve('success');
} else {
reject('fail')
}
})
}
function f2(data) {
return new Promise((resolve, reject) => {
console.log('这里是f2')
})
}
function f3(data) {
console.log('这里是f3')
// 返回reject
return new Promise((resolve, reject) => {
reject()
})
}
function f4(data) {
console.log('这里是f4')
return new Promise((resolve, reject) => {
resolve()
})
}
f1('Jack').then(f2, f3).then(f4);
还有一个知识点,就是promise里面是「同步」的,.then里面才是「异步」的。
function f1() {
return new Promise((resolve, reject) => {
console.log(1)
})
};
f1();
console.log(2)
promise里的同步
function f1() {
return new Promise((resolve, reject) => {
resolve(1)
})
};
function f2(data) {
console.log(data);
}
f1().then(f2);
console.log(2)
.then中异步
五.await
上面我们使用promise,通过then来拿到成功或者失败的信息,但是then里面还是有回调,有啥方式可以不是用回调呢?
我们可以使用「await」。
await操作符用于等待一个Promise对象。它只能在异步函数async function中使用
function f1(name) {
return new Promise((resolve, reject) => {
if(name === 'Jason') {
resolve('success!');
} else {
reject('fail!!')
}
})
}
async function f2() {
let result = await f1('Jason');
console.log(result)
}
f2();
image.png
上述代码中,在f2函数中,我们用await得到了f1中promise对象执行成功函数传入的参数“success”。同样await也可以获得失败函数传入的参数。我们把两者整合,应该这么写。
function f1(name) {
return new Promise((resolve, reject) => {
if(name === 'Jason') {
resolve('success!');
} else {
reject('fail!!')
}
})
}
async function f2() {
try {
let result = await f1('Jack'); // 当前是Jack输出“fail”,如果为Jason则输出“success”
console.log(result)
}catch(error) {
console.log(error)
}
}
f2();
如果await后不是一个promise对象,await 会把该值转换为已正常处理的Promise,然后等待其处理结果。
async function f2() {
var y = await 20;
console.log(y); // 20
}
f2();
参考:
网友评论