最近,小伙伴S 问了我一段代码:前端
const funB = (value) => {
console.log("funB "+ value);
};
const funA = (callback) => {
...
setTimeout(() => {
typeof callback === "function" && callback("is_ok!");
}, 1000);
}
funA(funB);
复制代码
他不太理解这段代码中,funB 函数做为 funA 函数的参数这样的写法。从语义上看,callback 的意思是回调,那么是说 funB 是 funA 的回调嘛?vue
我给他解释说,funB 函数的确是 funA 函数的回调,它会等待 funA 中前面的语句都执行完,再去执行。这是一种异步编程的写法。git
小伙伴S 仍是有点不太理解:异步编程是什么?除了回调函数以外,异步编程还有哪些?github
别急,让咱们先从概念入手,再逐个理解异步编程中的方法,看看它的前世此生。ajax
所谓"异步"(Asynchronous),能够理解为一种不连续的执行。简单地说,就是把一个任务分红两段,先执行第一段,而后转而执行其余任务,等接到通知了,再回过头执行第二段。编程
咱们都知道,JavaScript是单线程的。而异步,对于JavaScript的重要性,则体如今非阻塞这一点上。一些常见的异步有:设计模式
onclick
在其事件触发的时候,回调会当即添加到任务队列中。setTimeout
只有当时间到达的时候,才会将回调添加到任务队列中。ajax
在网络请求完成并返回以后,才将回调添加到任务队列中。接下来,咱们一块儿来看看Javascript中的异步编程,具体有哪几种。promise
上面不止一次提到了回调函数。它从概念上说很简单,就是把任务的第二段单独写在一个函数里面,等到从新执行这个任务的时候,就直接调用这个函数。它是异步编程中,最基本的方法。bash
举个例子,假定有两个函数 f1 和 f2,后者等待前者的执行结果。顺序执行的话,能够这样写:网络
f1();
f2();
复制代码
可是,若是 f1 是一个很耗时的任务,该怎么办?
改写一下 f1,把 f2 写成 f1 的回调函数:
const f1 = (callback) => {
setTimeout(() => {
typeof callback === "function" && callback();
}, 1000);
}
f1(f2);
复制代码
onclick 的写法,在异步编程中,称为事件监听。它的思路是:若是任务的执行不取决于代码的顺序,而取决于某个事件是否发生,也就事件驱动模式。
仍是 f1 和 f2 的例子,为了简化代码,这里采用jQuery的写法:
// 为f1绑定一个事件,当f1发生done事件,就执行f2
f1.on('done', f2);
// 改写f1
function f1(){
setTimeout(() => {
// f1的任务代码,执行完成后,当即触发done事件
f1.trigger('done');
}, 1000);
}
复制代码
它的优势是:比较容易理解,耦合度下降了。能够绑定多个事件,并且每一个事件还能指定多个回调函数。
缺点是:整个程序都会变为由事件来驱动,流程会变得很不清晰。
这是一种为了处理一对多的业务场景而诞生的设计模式,它也是一种异步编程的方法。vue中MVVM的实现,就有它的功劳。
关于概念,咱们能够这样理解,假定存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其余任务能够向信号中心"订阅"(subscribe)这个信号,从而知道何时本身能够开始执行。这就叫作"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
下面的例子,采用的是 Morgan Roderick 的 PubSubJS ,这是一个无依赖的JavaScript插件:
import PubSub from 'pubsub-js';
// f2向 'PubSub' 订阅信号 'done'
PubSub.subscribe('done', f2);
const f1 = () => {
setTimeout(() => {
// f1执行完成后,向 'PubSub' 发布信号 'done',从而执行 f2
PubSub.publish('done');
}, 1000);
};
f1();
// f2 完成执行后,也能够取消订阅
PubSub.unsubscribe("done", f2);
复制代码
这种模式有点相似于“事件监听”,可是明显优于后者。由于,咱们能够经过查看“消息中心”,了解存在多少信号、每一个信号有多少订阅者,从而监控程序的运行。
接下来,咱们聊聊与ajax相关的异步编程方法,Promise对象。
Promise 是由 CommonJS 提出的一种规范,它是为了解决回调函数嵌套,也就是回调地狱的问题。它不是新的语法功能,而是一种新的写法,容许将回调函数的横向加载,改为纵向加载。它的思想是,每个异步任务返回一个Promise对象,该对象有一个then方法,容许指定回调函数。
继续改写 f1 和 f2:
const f1 = () => {
return new Promise((resolve, reject) => {
let timeOut = Math.random() * 2;
setTimeout(() => {
if (timeOut < 1) {
resolve('200 OK');
} else {
reject('timeout in ' + timeOut + ' seconds.');
}
}, 1000);
});
};
const f2 = () => {
console.log('start f2');
};
f1().then((result) => {
console.log(result);
f2();
}).catch((reason) => {
...
);
复制代码
例子中,用随机数模拟了请求的超时。当 f1 返回 Promise 的 resolve 时,执行 f2。
Promise的优势是:回调函数变成了链式的写法,程序的流程能够看得很清楚。还有就是,若是一个任务已经完成,再添加回调函数,该回调函数会当即执行。因此,你不用担忧是否错过了某个状态。
缺点就是:编写和理解,都相对比较难。
generator(生成器)是 ES6 标准引入的数据类型。它最大特色,就是能够交出函数的执行权(即暂停执行),是协程在 ES6 中的实现。
看上去它像一个函数,定义以下:
function* gen(x) {
var y = yield x + 2;
return y;
}
复制代码
它不一样于普通函数,函数名以前要加星号(*
),是能够暂停执行的。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。用 yield 语句注明异步操做须要暂停的地方。
咱们来看一下 Generator 函数执行的过程:
var g = gen(1);
// { value: 3, done: false }
g.next();
// { value: undefined, done: true }
g.next();
复制代码
上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器 )g 。这是 Generator 函数不一样于普通函数的另外一个地方,即执行它不会返回结果,返回的是指针对象。调用指针 g 的 next 方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的 yield 语句,上例是执行到 x + 2 为止。
换言之,next 方法的做用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,便是否还有下一个阶段。
这是 ES8 中提出的一种更优雅的异步解决方案,灵感来自于 C# 语言。具体可前往 细说 async/await 相较于 Promise 的优点 ,深刻理解其原理及特性。
来看个例子,要实现一个暂停功能,输入 N 毫秒,则停顿 N 毫秒后才继续往下执行。
const sleep = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
})
};
const start = async () => {
console.log('start');
// 在这里使用起来就像同步代码那样直观
await sleep(1000);
console.log('end');
};
start();
复制代码
控制台先输出 start,稍等 1 秒后,输出结果 ok,最后输出 end。
解析一下上述代码:
JavaScript的异步编写方式,从 回调函数 到 async/await,感受在写法上,每次都有进步,其本质就是一次次对语言层抽象的优化。以致于如今,咱们能够像同步同样地,去处理异步。
换句话说就是:异步编程的最高境界,就是根本不用关心它是否是异步。
PS:欢迎关注个人公众号 “超哥前端小栈”,交流更多的想法与技术。