likes
comments
collection
share

❤️❤️异步的发展历程(小白版)

作者站长头像
站长
· 阅读数 13

前言

因为自己淋过雨,所以想给别人撑把伞。

在这一节,因为主要目标对象是刚学异步的小白同学,所以笔者打算用更通俗的语言来简单梳理一下这方面的内容。😋

同步与异步

既然要谈异步的发展历程,这里当然也要提到一下同步和异步的概念了

什么是同步?

同步就是后一个任务等待前一个任务执行完毕后,再执行,执行顺序和任务排列顺序一致

下面就是同步

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)

虽然这种写法解决我们异步任务的代码按顺序执行但是,这种回调函数的层层嵌套,会造成回调地域

这段代码用了三层嵌套请求,就已经让代码变得混乱不堪,所以,我们还需要解决这种嵌套调用后混乱的代码结构。

这段代码之所以看上去很乱,主要原因

  1. 异步回调嵌套会导致代码可读性变得非常差,并且不方便统一处理错误
  2. 每一层的执行都依赖于上一层的结果 ,这种嵌套函数的方式耦合性太高,一旦有所改动,就要全改
  3. 用try catch抛出错误时,不知道哪里出错了

Promise

中文翻译过来就是承诺,意思是在未来某一个时间点承诺返回数据给你。它是js中的一个原生对象,是一种异步编程的解决方案,可以替换掉传统的回调函数解决方案。

如果你还没用过 Promise,可以先读一下阮一峰老师的这篇文章:ES6 Promise对象

这里简单介绍一下:

Promise对象只有三种状态:

  1. pendding: 初始状态,既不是成功,也不是失败状态。
  2. fulfilled: 意味着操作成功完成。
  3. rejected: 意味着操作失败。

Promise 提供了可以链式调用的 then 方法,允许我们在执行完上一步操作后(Promise 从 penddingfulfilled 状态的时候)再去调用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 函数交替执行。其实这就是生成器函数的特性,可以暂停执行,也可以恢复执行。

下面我们就来看看生成器函数的具体使用方式

  1. 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
  2. 外部函数可以通过 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万现金大奖」