JS 异步的发展流程

583 阅读6分钟

想要了解异步的发展,需要了解一堆的概念。

抛出问题:

  • 什么是异步

"调用"在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在"调用"发出后,"被调用者"通过状态、通知来通知调用者,或通过回调函数处理这个调用。异步调用发出后,不影响后面代码的执行。

  • 什么是同步

所谓同步,就是在发出一个"调用"时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。此调用执行完之前,阻塞之后的代码执行。

  • JS中为什么需要用到异步

首先我们知道JS是单线程的,当我们执行一个任务是,他不会开辟其他的新的线程,如果是同步执行,这就意味着阻塞,也就是一直等待结果的返回。而异步则不会,我们不会等待异步代码的之后,我们是这里烧着开水,然后你可以去干其他的事情。

由于之前写过类似的总结,这里就不再多家赘述,详细参考:JS同步 异步

接下来我们就要详细了解异步的解决方案的发展:

简单的总结异步的发展流程:

回调函数callback ---> Promise ---> generator+co ---> async/await.

一、回调函数callback

所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。我们介绍一个常见的node中的异步方法readFile可以用来读取文件。

fs.readFile(filename, function (err, data) {
  if (err) throw err;
  console.log(data);
});

注意:在node中回调函数的第一个参数必须是错误对象(error-first callbacks)

1.1 、回掉函数作用

callback的使用场景:

  • 事件回调
  • Node API
  • setTimeout/setInterval中的回调函数
  • ajax 请求

1.2 、回掉函数的优缺点:

优点:

  • 使用简单

缺点:

  • 回调地狱问题,异步多级依赖的情况下嵌套非常深,代码难以阅读的维护
  • 异步不支持try/catch,回调函数是在下一事件环中取出,所以一般在回调函数的第一个参数预置错误对象
  • 多个异步在某一时刻获取所有异步的结果
  • 无法使用return语句返回值,并且也不能使用throw关键字。

如先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C

fs.readFile(A, 'utf-8', function(err, data) {
    fs.readFile(B, 'utf-8', function(err, data) {
        fs.readFile(C, 'utf-8', function(err, data) {
            fs.readFile(D, 'utf-8', function(err, data) {
                //....
            });
        });
    });
});

二、Promise

Promise 一定程度上解决了回调地狱的问题,Promise 最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了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
  • 当处于pending状态时,无法得知目前进展到哪一个阶段
  • 错误不能被 try catch

三、generator+co

Generator 函数一般配合 yield 或 Promise 使用。Generator函数返回的是迭代器。首先你需要了解一下Generator生成器和迭代器的概念。

举个简单的例子:

// 生成器可以产出很多值,迭代器只能next一下,拿一值,next一下,拿一下值
function * read(){
    yield 1;  
    yield 2;
    yield 3;
}
// 调用read()  返回值是迭代器
let it = read()
console.log(it.next())  // { value: 1, done: false }
console.log(it.next())  // { value: 2, done: false }
console.log(it.next())  // { value: 3, done: false }
console.log(it.next())  // { value: undefined, done: true }

Generator 函数是 ES6 提供的一种异步编程解决方案,整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。

function* gen() {
    let a = yield 111;
    console.log(a);
    let b = yield 222;
    console.log(b);
    let c = yield 333;
    console.log(c);
    let d = yield 444;
    console.log(d);
}
let t = gen();
//next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
t.next(1); //第一次调用next函数时,传递的参数无效
t.next(2); //a输出2;
t.next(3); //b输出3; 
t.next(4); //c输出4;
t.next(5); //d输出5;

仍然以上文的 readFile (先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C)为例,使用 Generator + co库来实现:

const fs = require('fs');
const co = require('co');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);

function* read() {
    yield readFile(A, 'utf-8');
    yield readFile(B, 'utf-8');
    yield readFile(C, 'utf-8');
    //....
}
co(read()).then(data => {
    //code
}).catch(err => {
    //code
});

四、async/await

ES7中引入了 async/await 概念。async 其实是一个语法糖,它的实现就是将 Generator函数和自动执行器(co),包装在一个函数中。

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
});

使用 async/await 实现此需求:读取A,B,C三个文件内容,都读取成功后,再输出最终的结果。

function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}

async function readAsync() {
    let data = await Promise.all([
        read(A),
        read(B),
        read(C)
    ]);
    return data;
}

readAsync().then(data => {
    console.log(data);
});

所以JS的异步发展史,可以认为是从 callback -> promise -> generator -> async/await。async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。

参考链接:参考一 参考二 参考三