一. js 异步流程的由来
众所周知,Javascript 语言的执行环境是单线程(single thread),作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM.若以多线程的方式操作这些 DOM,则可能出现操作的冲突.假设有两个线程同时操作一个 DOM 元素,线程 1 要求浏览器删除 DOM,而线程 2 却要求修改 DOM 样式,这时浏览器就无法决定采用哪个线程的操作.当然,我们可以为浏览器引入 "锁" 的机制来解决这些冲突,但这会大大提高复杂性,所以 JavaScript 从诞生开始就选择了单线程执行.
而单线程就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务.因为 javascript 设计之初是为浏览器设计的 GUI 编程语言,GUI 编程的特性之一是保证 UI 线程一定不能阻塞,否则性能不好,可能会界面卡死,因为 JavaScript 是单线程的,有一个致命问题是在某一时刻内只能执行特定的一个任务,并且会阻塞其它任务执行,为了解决这个问题,Javascript 语言将任务的执行模式分成同步(Synchronous)和异步(Asynchronous),在遇到类似 I/O 等耗时的任务时 js 会采用异步操作,而此时异步操作不进入主线程,而进入 "任务队列",只有 "任务队列" 通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行,这时就不会阻塞其它任务执,而这种模式称为 js 的事件循环机制 (Event Loop).
同步:调用者发出调用后,在没有得到结果之前,该调用就不返回.后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的,同步的,具有同步关系的一组任务相互发送的信息称为消息或事件.
异步:调用者发出调用后不会立刻得到结果,该调用就返回了.每一个任务有一个或多个回调函数(callback), 前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的,异步的,线程就是实现异步的一个方式,异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情.
阻塞:指调用结果返回之前,调用者会进入阻塞状态等待.只有在得到结果之后才会返回.
非阻塞:指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回.
事件循环机制:
(1)所有同步任务都在主线程上执行,形成一个 执行栈 (execution context stack).
(2)主线程之外,还存在一个 "任务队列"(task queue).只要异步任务有了运行结果,就在 "任务队列" 之中放置一个事件.
(3)一旦 "执行栈" 中的所有同步任务执行完毕,系统就会读取 "任务队列",看看里面有哪些事件.那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行.
(4)主线程不断重复上面的第三步,形成一个事件的循环.
事件循环机制示意图
阻塞非阻塞和同步异步的主要区别在于前者是相对于调用者来说,后者是相对于被调用者来说.举个栗子,把 js 比作一个老公的话,有一天上班的时候老公在微信约她老婆今天晚上去吃饭,如果老婆看到消息后马上同意或者拒绝,对老婆来说这就是同步(老公的消息被老婆返回了,同时也得到了结果),如果老婆看到消息后回复说我晚上可能会加班还不确定,过段时间确定了我再来发条消息通知你结果(可以理解为回调函数),对老婆来说这就是异步(老公的消息被老婆返回了,但是还没得到结果,需要等待).而在老婆还没有给出最终通知结果时(不管是同步回复还是异步回复),如果此时老公打开另一个微信窗口约小三明天晚上去吃饭,此时对老公来说就是非阻塞的,而如果老公在老婆没有最终通知结果之前一直在那等着而没干其他事情,对老公来说这就是阻塞的.显而易见,在这里老公是调用者,老婆是被调用者.
还是上面那个栗子,如果老婆说要过段时间才能通知老公最后结果(也就是异步的时候),此时老公也不能在老婆通知前什么都不干就待在那里,老公没有分身,也就是说老公不是多线程的,他会把这个异步事件先搁置(也就是放到任务队列里) ,作为单线程的他只能亲自去处理其他事情(主线程中处理执行栈),等老婆通知后再来处理这件事情(把这个异步事件从任务队列中取回来在主线程中执行).所以当 js 采用异步模式的时候 js 就是非阻塞了,这也就是为什么说 node.js 是非阻塞异步 I/O 了,因为异步和事件循环机制的特性使它是非阻塞的.
二. js 为什么要演进异步流程控制
"异步模式" 非常重要.在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是 Ajax 操作.在服务器端,"异步模式" 甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有 http 请求,服务器性能会急剧下降,很快就会失去响应.最早异步模式采用的是回调函数的方法,但是这种方法不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱,而且每个任务只能指定一个回调函数,这样就很容易陷入回调地狱,所以异步流程控制模式慢慢衍生出许多方式,下面主要来介绍这些方式有哪些.
三. js 异步流程控制的几种主要方式
1. 回调函数
有两个任务函数 taskFun1 和 taskFun2,如果按同步方式写
taskFun1();
taskFun2();
taskFun1() 如果是一个很耗时的任务,会严重阻塞 taskFun2() 的执行,用回调函数可以这样写:
function taskFun1(callbackFun){
setTimeout(function () {
// do something
callbackFun();
}, 3000);
}
taskFun1(taskFun2);
优点:简单,容易理解和部署,
缺点:不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱,而且每个任务只能指定一个回调函数.
2. 事件监听
另一种思路是采用事件驱动模式.任务的执行不取决于代码的顺序,而取决于某个事件是否发生.
taskFun1.on("event", taskFun2);
function taskFun1() { setTimeout(function() { // taskFun1的任务代码
taskFun1.trigger('event');
},
2000);
}
/* taskFun1.trigger('event')表示执行完成后,立即触发事件,从而开始执行taskFun2.*/
优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,耦合度很低,有利于实现模块化
缺点:整个程序都要变成事件驱动型,事件不能得到流程控制,运行流程会变得很不清晰.
3. 发布 / 订阅
上一节的 "事件",完全可以理解成 "信号".假定,存在一个 "信号中心",某个任务执行完成,就向信号中心 "发布"(publish)一个信号,其他任务可以向信号中心 "订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行.这就叫做 "发布 / 订阅模式",又称 "观察者模式".
element.subscribe("event", taskFun2);
function taskFun1() {
setTimeout(function() { // taskFun1的任务代码
element.publish("event");
},
2000);
}
优点:可以完全掌握事件被订阅的次数,以及订阅者的信息,管理起来特别方便.
4.Promise 对象
关于 Promises 的具体介绍和实现,可以参考 用 ES6 实现一个简单易懂的 Promise
比如平时我们常用的 axios 插件就是采用了 promise 模式:
axios.get('./demo.txt')
.then(function(response){
console.log(response);
})
.catch(function(err){
console.log(err);
});
而实现的机制就是 promise 把成功和失败分别代理到 resolved 和 rejected .
var promise = new Promise(function(resolve, reject) {
// 异步操作的代码
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
优点:回调函数变成了链式写法,程序的流程可以看得很清楚,可以实现许多强大的功能,同时还可以捕获到 catch 异常.
缺点:写法和理解起来都相对费劲
5.Generator 与 co 相结合
与 promise 不同的是,Generator 设计的初衷并不是为了来控制异步流程的,这种写法是 express 和 koa 框架的作者拿 Generator 与 co 相结合的一种写法,由于 generator 是一个状态机,所以需要手动调用 next 才能执行,node 框架的作者开发了 co 模块,可以自动执行 generator,可以理解为一种 geek 写法.
function readFile(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename, 'utf8',
function(err, data) {
err ? reject(err) : resolve(data);
});
})
}
function * read() {
console.log('开始');
let a = yield readFile('1.txt');
console.log(a);
let b = yield readFile('2.txt');
console.log(b);
let c = yield readFile('3.txt');
console.log(c);
return c;
}
co(read).then(function(data) {
console.log(data);
});
优点:可以用同步的方式编写异步代码
缺点:不够直观,没有语义化
6.await,async
await,async 是 ES7 引入了的关键字,async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,实质上是 generator+promise 的语法糖
*async function read(){
//await后面必须跟一个promise,
let a = await readFile('./1.txt');
console.log(a);
let b = await readFile('./2.txt');
console.log(b);
let c = await readFile('./3.txt');
console.log(c);
return 'ok';
}*/
优点:相比于之前的方式有很好的语义,实现也比较简单,被认为是目前最优的异步流程控制模式.
来源: http://www.jianshu.com/p/e4594d6dffd6