异步编程背景:
JavaScript的执行环境是“单线程”,即一次只能执行一个任务,如果有多个任务,就需要排队,前一个任务完成,再执行后一个任务。
问题:这种执行模式实现简单,但是只要有一个任务耗时较长,会导致后面的任务必须排队等待,会拖延整个程序的执行。
假如我们的主线程里,充斥着用户事件、ajax任务等高耗时的操作,这种情况下页面的卡顿甚至卡死将是不可避免的。
解决方案:为解决这种问题,JavaScript将任务的执行模式分为两种:同步Synchronous和异步Asynchronous。
同步模式
"同步模式"就是后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。异步模式
"异步模式"则完全不同,任务的执行顺序不必遵循排列顺序。前一个任务就算没执行完,也没关系,先执行下一个任务就好,等前一个任务的执行结果出来时,再把它临时穿插进来执行。所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务。
异步编程解决方案:
1. 回调函数
ES2015之前,回调是异步最常见、最基本的实现手段。
console.log('test');
var a = 1;
function fun01(callback){
setTimeout(function(){
console.log('fun01执行......'+a);
a++;
callback();
}, 1000)
}
function fun02(){ //fun02的执行依赖fun01
console.log('fun02执行......'+a);
}
fun01(fun02);
/**
test
fun01执行......1
fun02执行......2
*/
fun02的执行依赖于fun01。我们把fun02写成fun01的回调函数。采用这种方式,把同步操作变成异步操作。
回调函数用的最多的地方其实是在Node环境下,我们难免需要和引擎外部的环境有一些交流:比如说我要利用网络模块发起请求、或者要对外部文件进行读写等等。这些任务都是异步的,我们通过回调的形式来实现它们。
// -- 异步读取文件
fs.readFile(filePath,'utf8',function(err,data){
if(err) {
throw err;
}
console.log(data);// 输出文件内容
});
const https = require('https');
// 发起网络请求
https.get('目标接口', (res) => {
console.log(data)
}).on("error", (err) => {
console.log("Error: " + err.message);
});
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱,而且每个任务只能指定一个回调函数。
当回调只有一层的时候,看起来感觉没什么问题。但是一旦回调函数嵌套的层级变多了之后,代码的可读性和可维护性将面临严峻的挑战,比如下面这种深不见底的回调地狱:
func1(function (resultA) {
func2(resultA, function (resultB) {
func3(resultB, function (resultC) {
func4(resultC, function (resultD) {
func5(resultD, function (resultE) {
func6(resultE, function (resultF) {
console.log(resultF);
...
// 无尽的回调
});
});
});
});
});
});
这样写代码非常糟糕,它会带来很多问题,最直接的就是:可读性和可维护性被破坏。
2. 事件监听模式
比如给目标DOM绑定一个监听函数,使用最多的addEventListener
:
document.getElementById('#myDiv').addEventListener('click', function (e) {
console.log('我被点击了')
}, false);
通过给 id 为 myDiv 的一个元素绑定了点击事件的监听函数,我们把任务的执行时机推迟到了点击这个动作发生时。此时,任务的执行顺序与代码的编写顺序无关,只与点击事件有没有被触发有关。
采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
const events = require('events');
console.log('test');
var a = 1;
function fun01(){
setTimeout(function(){
console.log('fun01执行......'+a);
a++;
fun02.emit('execute');
}, 1000)
}
var fun02 = new events.EventEmitter();
fun02.addListener('execute', function(){
console.log('fun02执行......'+a);
});
fun01();
/**
test
fun01执行......1
fun02执行......2
*/
利用Node自身提供的events模块,创建fun02事件对象,并为其绑定执行事件execute
,在fun01执行时触发fun02的execute
事件。
事件监听的方法优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
3. 发布/订阅模式
假设存在一个“信号中心”,某个任务执行完成就像信号中心“发布publish”一个信号,其它任务可以向信号中心“订阅subscribe”这个信号,从而知道什么时候开始执行。
const events = require('events');
console.log('test');
var emitter = new events(); //信号中心
emitter.on('done', fun02); //fun02向信号中心订阅"done"信号
var a = 1;
function fun01(){
setTimeout(function(){
console.log('fun01执行......'+a);
a++;
emitter.emit('done');//向信息中心publish“done”这个信号,从而引发fun02的执行
}, 1000)
}
function fun02(){ //fun02的执行依赖fun01
console.log('fun02执行......'+a);
}
fun01();
/**
test
fun01执行......1
fun02执行......2
*/
利用Node自身提供的events模块,将事件done与回调函数fun2相关联(fun02向信号中心订阅done信号),在fun01执行后通过emit()发布事件(向信号中心发布done信号,引发fun02的执行),消息会立即传递给当前事件done的侦听器fun02执行。
这种模式的性质与“事件监听”类似,都把任务执行的时机和某一事件的发生紧密关联了起来。但是我们可通过查看“信号中心”,了解存在多少信号,每个信号有多少订阅者,从而监控程序的运行。
const events = require('events');
console.log('test');
var emitter = new events(); //信号中心
emitter.on('done', fun02); //fun02向信号中心订阅"done"信号
emitter.on('done', fun03); //支持多个订阅者fun02 & fun03订阅一个信号done
var a = 1;
function fun01(){
setTimeout(function(){
console.log('fun01执行......'+a);
a++;
emitter.emit('done');//向信息中心publish“done”这个信号,从而引发fun02的执行
}, 1000)
}
function fun02(){ //fun02的执行依赖fun01
console.log('fun02执行......'+a);
}
function fun03(){
console.log('fun03执行......'+a);
}
fun01();
/**
test
fun01执行......1
fun02执行......2
fun03执行......2
*/
长久以来,我们一直期望着一种既能实现异步、又可以确保我们的代码好写又好看的解决方案出现。于是,迎来了Promise。
4. Promise对象
内置于ECMAScript 2015中,Promise对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。其思想是:每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,fun01的回调函数fun02,实现如下:
console.log('test');
var a = 1;
function fun01(){
return new Promise((resolve, reject) => {
setTimeout(function(){
console.log('fun01执行......'+a);
a++;
resolve();
}, 1000)
})
}
function fun02(){
console.log('fun02执行......'+a);
}
fun01().then(fun02);
/**
test
fun01执行......1
fun02执行......2
*/
可以看出,Promise会接收一个执行器,在这个执行器里,我们需要把目标的异步任务给“填进去”。Promise实例有三种状态:
- pending 状态,表示进行中。这是 Promise 实例创建后的一个初始态;
- fulfilled 状态,表示成功完成。这是我们在执行器中调用 resolve 后,达成的状态;
- rejected 状态,表示操作失败、被拒绝。这是我们在执行器中调用 reject后,达成的状态。
这种写法大大提高了代码的质量。最直接的例子就是当我们进行大量的异步链式调用时,回调地狱不复存在了,取而代之的是层级简单、赏心悦目的Promise调用链
fun01().then(fun02).then(fun03);
。
5. Generator
除了Promise,ES2015还为我们提供了Generator。Generator一个有利于异步的特性是,它可以在执行中被终端,然后等待一段时间再被我们唤醒。通过这个“中断后唤醒”的机制,我们可以把 Generator看作是异步任务的容器,利用 yield 关键字,实现对异步任务的等待。
console.log('test');
function* fun01(a){
console.log('fun01执行......'+a);
var y = yield a + 1;
return y;
}
function fun02(obj){
console.log('fun02执行......'+obj.value);
}
var g = fun01(1);
fun02(g.next()); //g.next() { value: 2, done: false }
/**
test
fun01执行......1
fun02执行......2
*/
通过这种方式我们不再需要地狱般的回调,也不再需要Promise长长的链式调用,而是可以像写同步代码一样简单、清晰地实现异步特性!
6. Async/Await
await操作符用于等待一个Promise对象,只能在异步函数async function中使用。async function
也于ECMAScript 2017中发布,并在Node.js8中实现。
通过async
关键字声明一个函数为“异步函数”,然后就可以在这个函数内部使用await
关键字了,意思是“我要异步了,可能需要点时间,后面的语句请等一等”。
console.log('test');
function fun01(){
var a = 1;
return new Promise((resolve, reject) => {
setTimeout(function(){
console.log('fun01执行......'+a);
a++;
resolve(a);
}, 1000)
})
}
async function fun02(){
var a = await fun01();
console.log('fun02执行......'+a);
}
fun02();
/**
test
fun01执行......1
fun02执行......2
*/
这里fun02的实现都是依赖于fun01。在前面的几种实现方案中我们都是执行fun01,在fun01的内部执行回调fun02。但是如果使用await,可以在fun02的实现中清晰的看见代码处理逻辑。
事实上,async/await 本身就是 generator 异步方案的语法糖。它的诞生主要就是为了这个单纯而美好的目的——让你写得更爽,让你写出来的代码更美。
有了它,什么都不用操心,只需要写几个关键字,就能把异步代码处理得像同步代码一样优雅,代码逻辑也会相对清晰很多。
其他
async/await 和 generator 方案,相较于 Promise 而言,有一个重要的优势:Promise 的错误需要通过回调函数捕获,try catch 是行不通的。而 async/await 和 generator 允许 try/catch。
参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await