单线程JavaScript异步方案
内容概要
- 同步模式与异步模式
- 事件循环与消息队列
- 异步编程的几种方式
- Promise异步方案、宏任务/微任务队列
- Generator异步方案、Async/Await语法糖
单线程的JavaScript
- JavaScript作为浏览器脚本语言,主要用途是与用户互动,以及操作DOM。因为DOM只有一个,于是从一诞生,JavaScript就是单线程。
- 因为单线程这一特性,同一个时间只能执行一个任务,有多个任务则需要按顺序一个接一个去执行。
- 单线程缺点:可能会阻塞代码,导致代码执行效率低下,页面未能及时响应,最终影响用户体验。
- 解决办法:为了避免这个问题,出现了异步编程。异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。
- 简单而言,异步就是从主线程发射一个独立的子线程来完成任务。因为子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行(像发起网络请求、读取文件等容易发生阻塞的操作,我们应该采用异步编程的方式来实现)。
- 子线程局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。
- 解决办法:JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。
- 回调函数:它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终。
同步模式
在这一模式下,代码当中的任务是依次执行的,同一个时间只执行一个任务,有多个任务则需要按照代码编写的顺序一个接一个去执行。看上去是不是很简单明了?是的,这是它的优点,然而缺点就是容易引发代码阻塞。
- 总结:
- 优点:简单直观,可读性高,有利于代码的编写;
- 缺点:容易引发代码阻塞
异步模式
- 异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。
- 简单而言,异步就是从主线程发射一个独立的子线程来完成任务。因为子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行(像发起网络请求、读取文件等容易发生阻塞的操作,我们应该采用异步编程的方式来实现)
- 子线程局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的。
- 解决办法:JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。
- 回调函数:它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终。
- 总结:
- 优点:异步模式让单线程的JavaScript语言具备了同时处理大量耗时任务的能力
- 缺点:代码的执行顺序比较灵活,提高了代码编写的难度。
消息队列
同步任务在主线程上排队执行,异步任务不进入主线程,由“任务队列”管理。这个“任务队列”也称为“消息队列”,回调函数就是归“消息队列”管理了。
事件循环
具体来说,异步执行的运行机制如下:
- (1)所有同步任务都在主线程上执行,形成一个执行栈。
- (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- (4)主线程不断重复上面的第三步。 主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为事件循环(Event Loop)。所以, Event Loop是一种运行机制,是我们经常使用JavaScript进行异步编程的基础。
异步编程的实现方式
回调函数
回调函数可以说是所有异步编程方案的根基。 假定有两个函数f1和f2,后者等待前者的执行结果。
f1();
f2();
如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。
function f1(callback){
setTimeout(function () {
// f1的任务代码
callback();
}, 1000);
}
执行代码就变成下面这样:
f1(f2);
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
- 总结:
- 优点:简单、容易理解和部署
- 缺点:不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱
事件监听
另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)。
f1.on('done', f2);
上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}
f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。
- 总结:
- 优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化
- 缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰。
发布/订阅
上一节的"事件",完全可以理解成"信号"。
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
首先,f2向"信号中心"jQuery订阅"done"信号。
jQuery.subscribe("done", f2);
然后,f1进行如下改写:
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
此外,f2完成执行后,也可以取消订阅(unsubscribe)。
jQuery.unsubscribe("done", f2);
这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
除此之外,还有Promise异步方案和Generator异步方案,下面单独分开来写。
Promise异步方案
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。 简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
f1().then(f2);
Promise的三种状态:
- pending:等待的状态
Promise对象实例创建时候的初始状态 - fulfilled :成功的状态
- rejected :失败的状态
注意: 状态不可回退
const p1 = new Promise((resolve, reject)=>{
resolve('success');
});
const p2 = new Promise((resolve, reject)=>{
reject('fail');
}).catch(error => {
console.log(error) // fail
});
p1.then(res => {
console.log(res)
})
// success
resolve方法
将Promise对象的状态从“等待”变为“成功”(即从 pending 变为 fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject方法
将Promise对象的状态从“等待”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
then方法
Promise对象的then方法会返回一个全新的Promise对象,所以Promise支持then方法的调用
const p1 = new Promise((resolve, reject)=>{
resolve('success');
});
p1.then(res => {
console.log(res);
return 'hello'
}).then(res => {
console.log(res);
return 'world'
}).then(res => {
console.log(res);
return 'done'
}).then(res => {
console.log(res);
})
// success
// hello
// world
// done
catch方法
Promise.prototype.catch(onRejected) 添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的promise。当这个回调函数被调用,新 promise 将以它的返回值来resolve,否则如果当前promise 进入fulfilled状态,则以当前promise的完成结果作为新promise的完成结果.
const p3 = new Promise((resolve, reject)=>{
reject('fail');
}).catch(error => {
console.log('catch u,', error) // catch u, fail
});
finally方法
添加一个事件处理回调于当前promise对象,并且在原promise对象解析完毕后,返回一个新的promise对象。回调会在当前promise运行完毕后被调用,无论当前promise的状态是完成(fulfilled)还是失败(rejected)
const p4 = new Promise((resolve, reject)=>{
reject('fail');
}).catch(error => {
console.log('catch u,', error) // catch u, fail
}).finally(res => {
console.log('finally,', res) // finally, undefined
});
宏任务与微任务
- ES6 规范中,宏任务(macrotask) 称为 task,微任务(microtask) 称为 jobs
- 宏任务是由宿主发起的,而微任务由JavaScript自身发起。
- 在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。
总结一下,两者区别为:
| 宏任务(macrotask) | 微任务(microtask) | |
|---|---|---|
| 谁发起的 | 宿主(Node、浏览器) | JS引擎 |
| 具体事件 | script、setTimeout/setInterval、setImmediate、UI事件 | Promise、MutaionObserver、process.nextTick |
| 谁先运行 | 后运行 | 先运行 |
Generator异步方案
generator(生成器)
ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。
- 特征:
- function命令与函数名之间有一个*
- 函数体内部使用yield语句定义不同的内部状态
- 直接调用 Generator函数并不会执行,也不会返回运行结果,而是返回一个遍历器对象(Iterator Object)
- 依次调用遍历器对象的next方法,遍历 Generator函数内部的每一个状态
function *show(x, y){
yield x;
yield y;
return x+y;
}
let genObj = show(1, 2);
genObj.next(); // {value: 1, done: false}
genObj.next(); // {value: 2, done: false}
genObj.next(); // {value: 3, done: true }
yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
使用Generator函数
我们来使用Generator来进行异步任务的封装
function * gen () {
var url = "https://api.github.com/users/github";
var result = yield fetch(url);
console.log(result.bio);
}
先读取一个远程接口,然后从json格式的数据解析信息,这段代码非常像同步操作,除了加上yield命令,执行这段代码的方法:
var g = gen();
var result = g.next();
result.value.then( function (data) {
return data.json();
}).then(function(data){
g.next(data);
});
首先执行generator函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段,由于fetch模块返回的是一个promise对象,因此要用then方法调用下一个next方法。
Async/Await语法糖
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。 它可以搭配着await 一起使用,让异步编程像编写同步代码那样直观
async function getUser () {
const url = "https://api.github.com/users/github";
const result= await fetch(url);
return result
}
const data = getUser()
console.log(data)