❤️❤️异步的发展历程(小白版)
前言
因为自己淋过雨,所以想给别人撑把伞。
在这一节,因为主要目标对象是刚学异步的小白同学,所以笔者打算用更通俗的语言来简单梳理一下这方面的内容。😋
同步与异步
既然要谈异步的发展历程,这里当然也要提到一下同步和异步的概念了
什么是同步?
同步就是后一个任务等待前一个任务执行完毕后,再执行,执行顺序和任务排列顺序一致
下面就是同步
console.log('hello 0')
console.log('hello 1')
console.log('hello 2')
// hello 0
// hello 1
// hello 2
什么是异步?
如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
setTimeout(() => {
console.log('hello 0')
}, 1000)
console.log('hello 1')
// hello 1
// hello 0
我们为什么要引入异步这个概念呢?
引入一个新的概念当然是为了解决一个特定的问题的。
比如我们加载页面,如果我们需要获取后台数据来渲染到页面上,但是下面还有好多要执行的操作,也许网络延迟,我们这个没获取到,后面的代码是执行不了的,那么页面就会白屏,非常影响用户体验。而异步则不会,我们不会等待异步代码的之后,继续执行异步任务之后的代码。
通俗解释
:
就好比你追一个女孩子,追了人家好几年了,但人家一直对你不温不火,没有一个具体的答复。所以这个时候就有人会劝你:不要为了一颗树木而放弃整片森林呀。不能在一棵树上吊死,而应该继续走下去。 (有些不严谨😁,因为太俗了)
回调函数 Callback
什么是回调函数?
在MDN的文档中,对callback()
的定义为:
被作为实参传入另一函数,并在该外部函数内被调用,用以来完成某些任务的函数,称为回调函数。
简单理解:
函数a的参数为函数b,当函数a执行完之后再去执行b
可以通俗地认为:做完函数a的事情,再去做函数b的事情
比如下面:
console.log('a');
console.log('b');
console.log('c');
setTimeout(() => {
console.log('我饭卡拿到了!')
},2000)// 异步操作
console.log('d');
console.log('e');
我们可以规定在2000 ms后,再去执行callback
函数
用回调函数的方法来进行异步开发好处就是简单明了,容易理解
回调函数的缺点, 用一个小的实例来说明一下:
setTimeout(function () { //第一层
console.log(111);
setTimeout(function () { //第二程
console.log(222);
setTimeout(function () { //第三层
console.log(333);
}, 1000)
}, 2000)
}, 3000)
虽然这种写法解决我们异步任务的代码按顺序执行但是,这种回调函数的层层嵌套,会造成回调地域。
这段代码用了三层嵌套请求,就已经让代码变得混乱不堪,所以,我们还需要解决这种嵌套调用后混乱的代码结构。
这段代码之所以看上去很乱,主要原因:
异步回调嵌套会导致代码可读性变得非常差,并且不方便统一处理错误
每一层的执行都依赖于上一层的结果 ,这种嵌套函数的方式耦合性太高,一旦有所改动,就要全改
用try catch抛出错误时,不知道哪里出错了
Promise
中文翻译过来就是承诺
,意思是在未来某一个时间点承诺返回数据给你。它是js中的一个原生对象,是一种异步编程的解决方案,可以替换掉传统的回调函数解决方案。
如果你还没用过 Promise,可以先读一下阮一峰老师的这篇文章:ES6 Promise对象
这里简单介绍一下:
Promise
对象只有三种状态:
pendding
: 初始状态,既不是成功,也不是失败状态。fulfilled
: 意味着操作成功完成。rejected
: 意味着操作失败。
Promise
提供了可以链式调用的 then
方法,允许我们在执行完上一步操作后(Promise 从 pendding
到 fulfilled
状态的时候)再去调用then
方法
Promise的状态只能由内部改变,并且只可以改变一次。
那么我们看看Promise是如何解决回调地狱问题的,以readFile 为例(先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C)。
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
read(A).then(data => {
return read(B);
}).then(data => {
return read(C);
}).then(data => {
return read(D);
}).catch(reason => {
console.log(reason);
});
从这段 Promise
代码可以看出来,使用 promise.then
也是相当复杂,虽然整个请求流程已经线性化了,但是代码里面包含了大量的 then
函数,使得代码依然不是太容易阅读。
Generator
在进入到async/await
之前我们还得了解Generator(生成器)
这个概念
我们先来看看什么是生成器函数?
生成器函数是一个带星号(*)
函数,而且是可以暂停执行
和恢复执行
的。
我们可以看下面这段代码:
function* genDemo() {
console.log("开始执行第一段")
yield 'generator 2'
console.log("开始执行第二段")
yield 'generator 2'
console.log("开始执行第三段")
yield 'generator 2'
console.log("执行结束")
return 'generator 2'
}
console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')
执行上面这段代码,观察输出结果,你会发现函数 genDemo
并不是一次执行完的,全局代码和 genDemo
函数交替执行。其实这就是生成器函数的特性,可以暂停执行,也可以恢复执行。
下面我们就来看看生成器函数的具体使用方式:
- 在生成器函数内部执行一段代码,如果遇到
yield
关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。 - 外部函数可以通过
next
方法恢复函数的执行。
关于函数的暂停和恢复这里就不涉及了,因为这里只想讲个大概的印象。
async/await
虽然生成器已经能很好地满足我们的需求了,但是程序员的追求是无止境的,这不又在 ES7 中引入了 async/await
,这种方式能够彻底告别生成器,实现更加直观简洁的代码。
async/await
的优点是代码清晰,不用像 Promise
写很多 then
链,就可以处理回调地狱的问题。并且错误可以被try catch
。
仍然以上文的readFile (先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C) 为例,使用 async/await
来实现:
const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);
async function read() {
await readFile(A, 'utf-8');
await readFile(B, 'utf-8');
await readFile(C, 'utf-8');
//code
}
read().then((data) => {
//code
}).catch(err => {
//code
});
小结
异步最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。但是回调函数有一个很常见的问题,就是回调地狱的问题;
为了解决回调地狱的问题,社区提出了Promise解决方案,ES6将其写进了语言标准。Promise一定程度上解决了回调地狱的问题,但是Promise也存在一些问题,如错误不能被try catch,而且使用Promise的链式调用,其实并没有从根本上解决回调地狱的问题,只是换了一种写法。
ES6中引入 Generator 函数,Generator是一种异步编程解决方案,Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权,Generator 函数可以看出是异步任务的容器,需要暂停的地方,都用yield语句注明。但是 Generator 使用起来较为复杂。
ES7又提出了新的异步解决方案:async/await,async是 Generator 函数的语法糖,async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。
回调函数 ---> Promise ---> Generator ---> async/await.
async/await:使用同步的方式去写异步代码 (geekbang.org)
本文正在参加「金石计划 . 瓜分6万现金大奖」