同步与异步
- 通常,代码是由上而下依次执行的。如果有多个任务,就必须排队,前一个任务完成,后一个任务才能执行。这种连续的执行模式就叫做同步。
a();
b();
c();
复制代码
上面代码中,a、b、c是三个不同的函数,每个函数都是一个不相关的任务。在同步模式会先执行 a 任务,再执行 b 任务,最后执行 c 任务。当b任务是一个耗时很长的请求时,而c任务是展现新页面时,就会导致网页卡顿。
- 所谓异步,就是一个任务不是连续完成的。比如,有一个读取文件处理的任务,任务的第一段的向操作系统发出请求,要求读取文件,然后程序执行其他任务,等到操作系统返回文件,再去处理文件。这种不连续的执行模式就叫做异步。
a();
//立即发送请求
ajax('url',(b)=>{
//请求回来执行
});
c();
复制代码
上面代码中,就是将b任务分成了两部分。一部分立即执行,另一部分再请求回来后执行。也就解决了上面的问题。
总结: 同步就是大家排队工作,异步就是大家同时工作。
异步的解决方案
1、CallBack
CallBack,即回调函数,回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。即异步操作执行完成后触发执行的函数。
//当请求完成时就会触发回调函数
$.get('url',callback);
复制代码
回调可以完成异步操作,但用过 jquery 的 PE 都对下面的代码折磨过。
$.ajax({
url:"type",
data:1,
success:function(a){
url:"list",
data:a,
success:function(b){
$.ajax({
url:"content",
data:b,
success:function(c){
console.log(c);
}
})
}
}
})
复制代码
上面代码就是传说中的回调金字塔,又叫回调地狱。这里还只是单纯的嵌套代码,如若再加上业务代码,代码可读性可想而知。自己开发起来还好,如果这是别人的代码,你要改其中一部分足以让人疯掉。
2、事件发布订阅
我们想读取两个文件时,希望这两个文件都被读取完后,拿到结果。我们可以通过 node 中的 EventEmitter 类来实现,它有两个核心方法,一个是 on(表示注册监听事件),一个是emit(表示发射事件)。
let fs=require('fs');
let EventEmitter=require('event');
let eve=new EventEmitter();
let arr=[];//存储读取内容
//监听数据获取成功事件,然后调用回调函数
eve.on('ready',function(d){
arr.push(d);
if(arr.length==2){
//两个文件的数据
console.log(arr);
}
});
fs.readFile('./a.txt','utf8',function(err,data){
eve.emit('ready',data);
});
fs.readFile('./b.txt','utf8',function(err,data){
eve.emit('ready',data);
});
复制代码
请求 a.txt 和 b.txt 文件数据,当成功后发布ready事件。on 订阅了 ready 事件,当监听到触发的时候,on 方法执行。
哨兵变量
let fs=require('fs');
function after(times,callback){
let arr=[];
return function(d){
arr.push(d);
if(arr.length==times){
callback(arr);
}
}
}
//2是一个哨兵变量,将读取文件数据成功后执行的方法作为回调函数传给after方法
let out=after(2,function(data){
console.log(data);
})
fs.readFile('./a.txt','utf8',function(err,data){
out(data);
});
fs.readFile('./b.txt','utf8',function(err,data){
out(data);
});
复制代码
上面代码 after 方法执行时传入的 2 相当于一个哨兵变量,需要读取几个文件的数据就传几。将需要读取的文件数量,和读取全部文件成功后的方法作为回调函数传入 after。out 为 after 执行后返回的函数,每次获取文件成功后执行 out 方法可以后去到最终全部文件的数据。
不使用回调地狱遇到的问题是:不知道读取文件的函数什么时候执行完。只有当全部读取完成后才能执行需要文件数据的方法。
3、Generator函数
Generator 函数要用*号来标识,yield 关键字表示暂停执行的标记。Generator 函数是一个状态机,封装了多个内部状态。调用一次 next 就会继续向下执行,返回的结果是一个迭代器,所以 Generator 函数还是一个遍历器对象生成函数。
function* read(){
let a=yield '123';
console.log(a);
let b=yield 4;
console.log(b);
}
let it = read();
console.log(it.next('321')); // {value:'123',done:false}
console.log(it.next(1)); // 1 {value:4,done:false}
console.log(it.next(2)); // 2 {value:2,done:true}
console.log(it.next(3)); // {value:undefined,done:true}
复制代码
上面代码可以看出,调用 Generator 函数后,返回的不是函数运行的结果,而是一个指向内部状态的指针对象,也就是遍历器对象。必须调用遍历器对象的 next 方法,让指针移动的下一个状态。内部指针就会从函数开始或上次定下来的地方开始执行,直到遇到下一个 yield 语句或 return 语句为止。value 属性表示当前的内部状态值,是 yield 语句后面那个表达值;done 属性是一个布尔值,表示是否遍历结束。
4、Promise
在 JavaScript 的异步发展史中,出现了一系列解决 callback 弊端的库,而 promise 成为了胜者,并成功地被加入了ES6标准。Promise 函数接受一个函数作为参数,该函数有两个参数 resolve 和 reject。promise 就像一个中介,而它只返回可信的信息给 callback,所以 callback 一定会被异步调用,且只会被调用一次。
let p=new Promise((resolve,reject)=>{
//to do...
if(/*异步操作成功*/){
resolve(value);
}else{
reject(err);
}
});
p.then((value)=>{
//success
},(err)=>{
//failure
})
复制代码
这样 Promise 就解决了回调地狱的问题,比如我们连续读取多个文件时,写法如下:
let fs=require('fs');
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(err,data){
if(err) reject(err);
resolve(data);
})
}
}
read('a.txt').then((data)=>{
console.log(data);
}).then(()=>{
return read('b.txt');
}).then((data)=>{
console.log(data);
}).catch((err)=>{
console.log(err);
})
复制代码
如此不断的返回一个新的 Promise,这种不断的链式调用,就摆脱了callback回调地狱的问题和异步代码非线性执行的问题。
Promise 还解决了 callback 只能捕获当前错误异常。Promise 和 callback 不同,Promise 代理着所有的 callback 的报错,可以由 Promise 统一处理。所以,可以通过catch来捕获之前未捕获的异常。
Promise解决了callback的回调地狱问题,但Promise并没有摆脱callback。所以,有没有更好的写法呢?
5、Async Await
async函数是ES7中的一个新特性,它结合了Promise,让我们摆脱callback的束缚,直接用类同步的线性方式写异步函数,使得异步操作变得更加方便。
let fs=require('fs');
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(err,data){
if(err) reject(err);
resolve(data);
})
}
}
async function r(){
let a=await read('a.txt');
let b=await read('b.txt');
return a+b;
}
r().then((data)=>{
console.log(data);
});
复制代码
至此,异步的 await 函数已经可以让我们满意。目前使用 Babel 已经支持 ES7 异步函数的转码了,大家可以在自己的项目中开始尝试。以后会不会出现更优秀的方案?以我们广大程序群体的创造力,相信一定会有的。
JavaScript异步调用的发展历程就到这里了,如果您觉得文章有用,可以打赏个咖啡钱(⊙﹏⊙)!
原文发布时间为:2018年06月27日
原文作者:afan
本文来源: 掘金 如需转载请联系原作者