JavaScript中的异步编程
ES6 诞生以前,异步编程的方法,大概有下面四种。
回调函数、事件监听、发布/订阅、Promise 对象。
注意这里的Promise对象和ES6里的Promise其实不太一样,因为之前的Promise是由commonJS社区提出的Promise规范,用于统一处理异步回调,之后ECMAscript 6 才原生提供了 Promise 对象。
在ES6中,主要涉及到的异步有Promise对象,Generator函数。
在ES8增加了async函数。
所以本文就介绍这六种异步实现方法。
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
- Generator函数
- async函数
1.回调函数
A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.
回调是作为参数传递给另一个函数并在其父函数完成后执行的函数。
回调函数是异步操作最基本的方法。以下代码就是一个回调函数的例子:
ajax(url, () => {
// 处理逻辑
})
但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:
ajax('xxxxxxx.json', function() {
// doing something 1
ajax('xxxxxxx.json', function() {
// doing something 2
ajax('xxxxxxx.json', function() {
// doing something 3
ajax('xxxxxxx.json', function() {
// doing something 4
});
});
});
});
这是是单纯的嵌套代码,如若再加上业务代码,代码可读性可想而知,如果是开发起来还好,但是后期的维护和修改的难度足以让人疯掉。这就是那个**JQuery时代的“回调地狱”**问题。
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
2.事件监听
另一种思路是采用事件驱动模式。
异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
以f1
和f2
为例。首先,为f1
绑定一个事件(这里采用的 jQuery 的写法)。
f1.on('done', f2);
上面这行代码给f1
注册了done事件,当f1
发生done
事件,就执行f2
。然后,对f1
进行改写:
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
上面代码中,f1.trigger('done')
表示,执行完成后,立即触发done
事件,从而开始执行f2
。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以**“去耦合”(decoupling)**,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
3.发布/订阅
事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。
发布订阅其实和事件监听非常相似,事件监听的on相当于就是订阅,事件的发生就是发布。
如果不太了解的可以看我的另一篇博客,观察者模式
但是观察者模式一般是用在同步任务中的,发布订阅模式则是异步。
// 主体
let subject = new Subjet();
// 像向体订阅getData通知
subject.subscribe('getData', function(data) {
// do something...
});
// 获取数据方法
function getDate(params) {
// do something...
// 获取数据后主题发布getData通知
subject.publish('getData');
}
4.Promise对象和then
Promise详细文档请阅读:阮一峰老师的ES6入门:Promise
为了解决“回调地狱”问题,提出了Promise对象,并且后来加入了ES6标准,Promise对象简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
Promise的状态
Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。
因此,Promise 的最终结果只有两种。
- 异步操作成功,Promise 实例传回一个值(value),状态变为
fulfilled
。 - 异步操作失败,Promise 实例抛出一个错误(error),状态变为
rejected
。
Promise
首先,Promise 是一个对象,也是一个构造函数。
var f1 = function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
}
const promise1 = new Promise(f1);
Promise
构造函数接受一个回调函数f1
作为参数,f1
里面是异步操作的代码。然后,返回的promise1
就是一个 Promise 实例。
Promise 新建后就会立即执行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
上面代码中,Promise 新建后立即执行,所以首先输出的是Promise
。然后,then
方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved
最后输出。
注意,调用
resolve或
reject并不会终结 Promise 的参数函数的执行。
then
Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then
方法,用来指定下一步的回调函数。它的作用是为 Promise 实例添加状态改变时的回调函数。
then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,两个函数只会有一个被调用。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// proceed
});
上面代码中,getJSON
的异步操作执行完成,就会执行then
的回调函数。
通过 .then 形式添加的回调函数,不论什么时候,都会被调用。
then
方法会返回一个新的Promise
实例(注意,不是原来那个Promise
实例)。
当然也可以自己返回一个Promise对象,达到异步操作的逐步执行的效果。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// 对comments进行处理
});
如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。
catch
catch()方法是
.then(null, rejection)或
.then(undefined, rejection)`的别名,用于指定发生错误时的回调函数。
// 写法一
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});
// 写法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
上面代码中,promise
抛出一个错误,就被catch()
方法指定的回调函数捕获。注意,上面的两种写法是等价的。reject()
方法的作用,等同于抛出错误。
一般来说,不要在then()
方法里面定义 Reject 状态的回调函数(即then
的第二个参数),总是使用catch
方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise.then(function(data) { //cb
// success
}).catch(function(err) {
// error
});
上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then
方法执行中的错误,也更接近同步的写法(try/catch
)。因此,建议总是使用catch()
方法,而不使用then()
方法的第二个参数。
跟传统的try/catch
代码块不同的是,如果没有使用catch()
方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
5.Generator和yield
ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。 基本用法
Generator 、yield
Generator 有两个区分于普通函数的部分:
- 一是在 function 后面,函数名之前有个 * ;
- 函数内部有 yield 表达式。
其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。
function* func(){
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}
调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行。
next和return
next方法的作用就是,使当前的Generator 函数返回的Iterator对象从当前指向的状态继续执行,遇到yield或者return就会停止。
next ()函数也可以传入参数,next 传入参数的时候,该参数会作为上一步yield的返回值;如果不提供参数,那么上一步yield的返回值为undefined。
yield的返回值的定义就是
x = yield '1';
这段代码中x的值。但是它是作为上一步的yield的返回值,你需要尝试去理解。调用next函数 遇到yield会挂起,但是不是这个yield,而是上一个yield。第一个yield没有上一个,所以第一个next的赋值其实是没有意义的。
return 方法提供参数时,返回该参数;不提供参数时,返回 undefined 。
function* func(){
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}
f=func();
console.log(f.next());
// one
// {value: "1", done: false}
console.log(f.next());
// two
// {value: "2", done: false}
console.log(f.next());
// three
// {value: "3", done: true}
console.log(f.next());
// {value: undefined, done: true}
console.log(f.return(1000));
// { value: 1000, done: true }
//完整运行结果
// one
// { value: '1', done: false }
// two
// { value: '2', done: false }
// three
// { value: '3', done: true }
// { value: undefined, done: true }
// { value: 1000, done: true }
第一次调用 next 方法时,从 Generator 函数的头部开始执行,先是打印了 one ,执行到 yield 就停下来,并将yield 后边表达式的值 ‘1’,作为返回对象的 value 属性值,此时函数还没有执行完, 返回对象的 done 属性值是 false。
第二次调用 next 方法时,同上步 。
第三次调用 next 方法时,先是打印了 three ,然后执行了函数的返回操作,并将 return 后面的表达式的值,作为返回对象的 value 属性值,此时函数已经结束,多以 done 属性值为true 。
第四次调用 next 方法时, 此时函数已经执行完了,所以返回 value 属性值是 undefined ,done 属性值是 true 。如果执行第三步时,没有 return 语句的话,就直接返回{value: undefined, done: true}
。
第五步调用return方法时,此时函数已经执行完了,本来返回 value 属性值是 undefined,但是return方法提供一个参数之后,就会返回这个参数,所以最后输出的value就是传入的1000。
除了使用 next ,还可以使用 for… of 循环遍历 Generator 函数生产的 Iterator 对象
yield* 表达式
yield* 表达式表示 yield 返回一个遍历器对象,用于在 Generator 函数内部,调用另一个 Generator 函数。
这里就不过多描述了。
6.async和await
async
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。和 Promise , Generator 有很大关联的。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
async function name([param[, param[, ... param]]]) { statements }
- name: 函数名称。
- param: 要传递给函数的参数的名称。
- statements: 函数体语句。
async 返回值
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。
async function helloAsync(){
return "helloAsync";
}
console.log(helloAsync()) // Promise {<resolved>: "helloAsync"}
helloAsync().then(v=>{
console.log(v); // helloAsync
})
await
await 操作符用于等待一个 Promise 对象, 它只能在异步函数 async function 内部使用。
[return_value] = await expression;
async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。
await 关键字仅在 async function 中有效。如果在 async function 函数体外使用 await ,你只会得到一个语法错误
function testAwait (x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function helloAsync() {
var x = await testAwait ("hello world");
console.log(x);
}
helloAsync ();
// hello world
await 返回值
await根据后面不同表达式有不同的处理方式:
-
Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值。
-
非 Promise 对象:直接返回对应的值。
正常情况下,await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数
function testAwait(){
console.log("testAwait");
}
async function helloAsync(){
await testAwait();
console.log("helloAsync");
}
helloAsync();
// testAwait
// helloAsync
Promise,Generator函数,async函数理解
关于JavaScript的简单异步编程价绍就这些,说实话我自己看了很久,感觉Promise,Generator函数,async函数是层层递进的。
Promise比较简单,也是最常用的,主要就是将原来的用回调函数的异步编程方法转成用relsove和reject触发事件, 用then和catch捕获成功或者失败的状态执行相应代码的异步编程的方法。
Generator函数可以看成是一个分步函数,只有主动调用next() 才能进行下一步。
async函数相当于自执行的Generator函数,会自己不断的执行,遇到await等待返回结果,然后继续,和普通的同步写法相同,提供效率。
关于三者区别我也专门整理了一下,想了解的可以看阅读。
不能区分Promise,Generator函数,async函数可以进来看看
参考
https://www.ruanyifeng.com/blog/2015/04/generator.html