什么是同步?什么是异步?
同步(synchronous)指的是任务从上往下依次执行的模式。比如:
A();
B();
C();
在这段代码中,A、B、C是三个不同的函数,每个函数都是一个不相关的任务。在同步模式下,计算机会先执行 A 任务,再执行 B 任务,最后执行 C 任务。在大部分情况,同步模式都没问题。但是如果 B 任务是一个耗时很长的网络请求,而 C 任务恰好是展现新页面,就会导致网页卡顿。
更好解决方案是,将 B 任务分成两个部分。一部分立即执行网络请求的任务,另一部分在请求回来后的执行任务。这种一部分立即执行,另一部分在未来执行的模式称为异步(asynchronous)。异步,也就是执行一个任务的同时,中间去执行其它的事件,最终再回来执行这个任务,不连续。
A();
// 在现在发送请求
ajax('url1',function B() {
// 在未来某个时刻执行
})
C();
// 执行顺序 A => C => B
实际上,JS 引擎并没有直接处理网络请求的任务,它只是调用了浏览器的网络请求接口,由浏览器发送网络请求并监听返回的数据。JavaScript 异步能力的本质是浏览器或 Node 的多线程能力。
异步的缺点: 不支持 try/catch。如下:
同步的代码:
try{
throw new Error('hello world');
}catch(err){
console.log(err);
}
// Error: hello world(…)
异步的代码:
try{
setTimout(function(){
throw new Error('hello world');
},0)
}catch(err){
console.log(err);
}
// ReferenceError: setTimout is not defined(…)
可见catch里面的代码并没有执行,也就是说try无法捕获异步里面的代码。
callback函数
上面提及的B任务中的未来执行的函数通常也叫 callback。
先了解一下callback的工作原理:
当我们把一个callback函数当作参数传到另一个函数中,我们只是传入了函数的定义,而并没有执行这个函数。(因为我们当作参数传入函数的时候并没有在定义之后加上()符号,因为函数名后面加上括号才代表要执行它了)。一个函数由于被传入了callback函数当作参数,它就可以随时执行这个传进来的callback函数。
callback函数其实是一个闭包,因此callback函数可以访问到包含它的函数的变量,甚至可以访问到全局变量。
虽然使用 callback可以解决阻塞的问题,但是也带来了一些其他问题。在最开始,我们的函数是从上往下书写的,也是从上往下执行的,这种“线性”模式非常符合我们的思维习惯,但是现在却被 callback 打断了。就上面提及的代码中,我们发现是跳过 B 任务先执行了 C任务的。这种异步“非线性”的代码会比同步“线性”的代码更难阅读,因此也更容易滋生 BUG。
A();
ajax('url1', function(){
B();
ajax('url2', function(){
C();
}
D();
});
E();
// A => E => B => D => C
从上面这段代码中,从上往下执行的顺序被 callback 打乱了。我们的阅读代码视线是A => B => C => D => E,但是执行顺序却是A => E => B => D => C,这也就是非线性代码带来的糟糕之处。
通过将ajax后面执行的任务提前,可以更容易看懂代码的执行顺序。虽然代码因为嵌套看起来不美观,但现在的执行顺序却是从上到下的“线性”方式。这种技巧在写多重嵌套的代码时,是非常有用的。
A();
E();
ajax('url1', function(){
B();
D();
ajax('url2', function(){
C();
}
});
// A => E => B => D => C
上一段代码只有处理了成功回调,并没处理异常回调。接下来,把异常处理回调加上,再来讨论代码“线性”执行的问题。
A();
ajax('url1', function(){
B();
ajax('url2', function(){
C();
},function(){
D();
});
},function(){
E();
});
加上异常处理回调后,url1的成功回调函数 B 和异常回调函数 E,被分开了。这种“非线性”的情况又出现了。
在 node 中,为了解决的异常回调导致的“非线性”的问题,制定了错误优先的策略。node 中 callback 的第一个参数,专门用于判断是否发生异常。
A();
get('url1', function(error){
if(error){
E();
}else {
B();
get('url2', function(error){
if(error){
D();
}else{
C();
}
});
}
});
到此,callback 引起的“非线性”问题基本得到解决。遗憾的是,使用 callback 嵌套,一层层if else和回调函数,一旦嵌套层数多起来,阅读起来不是很方便。此外,callback 一旦出现异常,只能在当前回调函数内部处理异常。
Promise
在 JavaScript 的异步进化史中,涌现出一系列解决 callback 弊端的库,而 Promise 成为了最终的胜者,并成功地被引入了 ES6 中。它将提供了一个更好的“线性”书写方式,并解决了异步异常只能在当前回调中被捕获的问题。
Promise 就像一个中介,它承诺会将一个可信任的异步结果返回。首先 Promise 和异步接口签订一个协议,成功时,调用resolve函数通知 Promise,异常时,调用reject通知 Promise。另一方面 Promise 和 callback 也签订一个协议,由 Promise 在将来返回可信任的值给then和catch中注册的 callback。
// 创建一个 Promise 实例(异步接口和 Promise 签订协议)
var promise = new Promise(function (resolve,reject) {
ajax('url',resolve,reject);
});
// 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议)
promise.then(function(value) {
// success
}).catch(function (error) {
// error
})
Promise 是个非常不错的中介,它只返回可信的信息给 callback。它对第三方异步库的结果进行了一些加工,保证了 callback 一定会被异步调用,且只会被调用一次。
var promise1 = new Promise(function (resolve) {
// 可能由于某些原因导致同步调用
resolve('B');
});
// promise依旧会异步执行
promise1.then(function(value){
console.log(value)
});
console.log('A');
// A B (先 A 后 B)
var promise2 = new Promise(function (resolve) {
// 成功回调被通知了2次
setTimeout(function(){
resolve();
},0)
});
// promise只会调用一次
promise2.then(function(){
console.log('A')
});
// A (只有一个)
var promise3 = new Promise(function (resolve,reject) {
// 成功回调先被通知,又通知了失败回调
setTimeout(function(){
resolve();
reject();
},0)
});
// promise只会调用成功回调
promise3.then(function(){
console.log('A')
}).catch(function(){
console.log('B')
});
// A(只有A)
介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。
var fetch = function(url){
// 返回一个新的 Promise 实例
return new Promise(function (resolve,reject) {
ajax(url,resolve,reject);
});
}
A();
fetch('url1').then(function(){
B();
// 返回一个新的 Promise 实例
return fetch('url2');
}).catch(function(){
// 异常的时候也可以返回一个新的 Promise 实例
return fetch('url2');
// 使用链式写法调用这个新的 Promise 实例的 then 方法
}).then(function() {
C();
// 继续返回一个新的 Promise 实例...
})
// A B C ...
如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。
Promise 解决的另外一个难点是 callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过catch来捕获之前未捕获的异常。
Promise 解决了 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接我们的代码和异步接口。
来源:CSDN
作者:小眼不叫粒透艾
链接:https://blog.csdn.net/weixin_40222512/article/details/82458463