这里是修真院前端小课堂,每篇分享文从
【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】
八个方面深度解析前端知识/技能,本篇分享的是:
【异步编程有哪几种方法来实现?】
大家好,我是IT修真院武汉分院web第16期的学员孟晨,一枚正直纯洁善良的web程序员 今天给大家分享一下,修真院官网js(职业)任务五,深度思考中的知识点——异步编程有哪几种方法来实现?
1.背景介绍
你可能知道,Javascript语言的执行环境是"单线程"(single thread)。
所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
2.知识剖析
常用的异步编程的几种方法?
首先我们来看一个基本的例子,在这个例子中输出的顺序是1,3,2,我们想让他按顺序1,2,3输出就需要用到异步编程的方法
function fn1() {
console.log('Function 1')
}
function fn2() {
setTimeout(() => {
console.log('Function 2')
}, 2000)
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
fn1()
fn2()
fn3()
// output =>
// Function 1
// Function 3
// Function 2
壹.回调函数
回调函数是异步编程的方法中最简单也是最常用的一个方法
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。
如果再嵌套多几层,代码会变得多么难以理解 这个被称之为“回调函数噩梦”(callback hell)!!!
也是可以看看例子,如果按照咱们刚刚的写法的话输出顺序会是3,2,1,所以把每个函数中写成回调函数的形式
就可以让执行完了前面的才会执行后面的,然后标红的部分就是被称为回调函数噩梦的原因,
只是少数嵌套函数的话不明显但多层嵌套代码就会显得很混乱
function fn1(f) {
setTimeout(() => {
console.log('Function 1')
f()
}, 1000)
}
function fn2(f) {
setTimeout(() => {
console.log('Function 2')
f()
}, 2000)
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
fn1(function () {
fn2(fn3)
})
// output =>
// Function 1
// Function 2
// Function 3
贰.事件发布/订阅
发布/订阅模式也是诸多设计模式当中的一种,恰好这种方式可以在es5下相当优雅地处理异步操作。什么是发布/订阅呢?以上一节的例子来说,fn1,fn2,fn3都可以视作一个事件的发布者,只要执行它,就会发布一个事件。这个时候,我们可以通过一个事件的订阅者去批量订阅并处理这些事件,包括它们的先后顺序。下面我们基于上一章节的例子,增加一个消息订阅者的方法(为了简单起见,代码使用了es6的写法):
class AsyncFunArr {
constructor(...arr) {
this.funcArr = [...arr]
}
next() {
const fn = this.funcArr.shift()
if (typeof fn === 'function') fn()
}
run() {
this.next()
}
}
const asyncFunArr = new AsyncFunArr(fn1, fn2, fn3)
function fn1() {
console.log('Function 1')
asyncFunArr.next()
}
function fn2() {
setTimeout(() => {
console.log('Function 2')
asyncFunArr.next()
}, 500)
}
function fn3() {
console.log('Function 3')
asyncFunArr.next()
}
// output =>
// Function 1
// Function 2
// Function 3
叁.PROMISE对象
romises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。 简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成: f1().then(f2); 这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。 比如,指定多个回调函数: f1().then(f2).then(f3); 再比如,指定发生错误时的回调函数: f1().then(f2).fail(f3); 而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是编写和理解,都相对比较难。
标红处就是所谓的链式写法,这样写带来了结构清晰的好处
function fn1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 1')
resolve()
}, 1000)
})
}
function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 2')
resolve()
}, 2000)
})
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
fn1()
.then(fn2)
.then(fn3)
// output =>
// Function 1
// Function 2
// Function 3
肆.GENERATOR
如果说Promise的使用能够化回调为链式,那么generator的办法则可以消灭那一大堆的Promise特征方法,比如一大堆的then()。
generator函数asyncFunArr()接受一个待执行函数列表fn,异步函数将会通过yield来执行。在异步函数内,通过af.next()激活generator函数的下一步操作。
这么粗略的看起来,其实和发布/订阅模式非常相似,都是通过在异步函数内部主动调用方法,告诉订阅者去执行下一步操作。但是这种方式还是不够优雅,比如说如果有多个异步函数,那么这个generator函数肯定得改写,而且在语义化的程度来说也有一点不太直观。
function fn1() {
setTimeout(() => {
console.log('Function 1')
}, 1000)
}
function fn2() {
setTimeout(() => {
console.log('Function 2')
}, 2000)
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
function* asyncFunArr(...fn) {
fn[0]()
yield fn[1]()
fn[2]()
}
const af = asyncFunArr(fn1, fn2, fn3)
af.next()
// output =>
// Function 1
// Function 2
// Function 3
伍.优雅的ASYNC/AWAIT
使用最新版本的Node已经可以原生支持async/await写法了,通过各种pollyfill也能在旧的浏览器使用。那么为什么说async/await方法是最优雅的呢?
有没有发现,在定义异步函数fn2的时候,其内容和前文使用Promise的时候一模一样?再看执行函数asyncFunArr(),其执行的方式和使用generator的时候也非常类似。
异步的操作都返回Promise,需要顺序执行时只需要await相应的函数即可,这种方式在语义化方面非常友好,对于代码的维护也很简单——只需要返回Promise并await它就好,无需像generator那般需要自己去维护内部yield的执行。
function fn1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 1')
resolve()
}, 3000)
})
}
function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Function 2')
resolve()
}, 2000)
})
}
function fn3() {
setTimeout(() => {
console.log('Function 3')
}, 500)
}
async function asyncFunArr() {
await fn1()
await fn2()
await fn3()
}
asyncFunArr()
// output =>
// Function 1
// Function 2
// Function 3
3.常见问题
何时使用异步
4.解决方案
在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
5.代码实战
6.拓展思考
异步的好处: 1、异步流程可以立即给调用方返回初步的结果。
2、异步流程可以延迟给调用方最终的结果数据,在此期间可以做更多额外的工作,例如结果记录等等。
3、异步流程在执行的过程中,可以释放占用的线程等资源,避免阻塞,等到结果产生再重新获取线程处理。
4、异步流程可以等多次调用的结果出来后,再统一返回一次结果集合,提高响应效率。
7.参考文献
谈一谈几种处理JavaScript异步操作的办法
Javascript异步编程的4种方法
8.更多讨论
鸣谢
感谢大家观看
BY : 孟晨
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。