全解析异步编程的几种方法

更新日期: 2022-09-06阅读: 821标签: 异步

前端技术飞速发展,曾经的最佳实践,在未来可能被新的方法代替,而我们只有保持不断学习,才能不那么迷茫,异步编程一直是JavaScript中的比较难学的部分,也是面试中的高频考题。

在JavaScript中,异步编程的方法一直在改变,目前为止,共有6种异步编程方法,学习并掌握这些方法并不容易,本文通过一个形象的例子,讲解了异步编程的发展历史和 6 种异步编程方法,实践出真知。

前言

在很久以前(大概2015年以前),说起异步还只有 callback,ES2015带来了 Promise 和 Generator,ES2017带来了更先进的async/await,今年发布的ES2022更是带来了顶级await,也就是不需要在async函数中,可以直接使用await了。

作为前端人员,遇到的复杂的异步场景实并不太多,对于请求和事件来说,大部分场景都太简单,思来想去也就只有动画了,本文将用不同方式实现下面的动画,红色方块每次向右移动 100 像素,完整的 demo 在 这里 。

关于异步

异步是一个关于现在和将来的问题,即现在执行的代码和将来执行的代码,举个例子来说一下异步是如何复杂和反人类:

var a = 1;
var b = 1;

function foo() {
    a = b + 3;
    console.log(a);
}

function bar() {
    b = a * 2;
}

ajax("url1", foo);
ajax("url2", bar);

上面的代码如果是同步的,那么打印 a 的地方就是确定的;但如果是异步的,a 的值就有两种可能(依赖于哪个请求先回来),这还是只是最简单的 case,你可以思考下,自己写程序时有没有考虑过这种问题。

异步编程方法

JavaScript 中的异步编程共有6种方法,大部分同学应该不了解全部,其中事件监听和观察者有些类似,下面我们将分别介绍:

  • 回调函数
  • 观察者
  • Promise
  • Generator
  • async/await

回调函数

callback 曾经是我们最熟悉的方式,上面提到的动画的例子,用 callback 实现代码如下:

moveTo(100, 0, function () {
    moveTo(200, 0, function () {
        moveTo(300, 0, function () {
            moveTo(400, 0, function () {
                // 无限不循环
            });
        });
    });
});

快看,这就是传说中的回调地狱吗?是,也不是。。。

有同学可能会问 moveTo 函数是如何实现的?答案如下:

function moveTo(x = 0, y = 0, cb = function () {}) {
    move(".box").x(x).y(y).end(cb);
}

下面来列举回调的 N 大罪状

  • 违反直觉
  • 错误追踪
  • 模拟同步
  • 回调地狱
  • 并发执行
  • 信任问题(多次调用)

违反直觉,并不是说缩进,缩进其实可以通过拆分函数来解决,而是对人友好的顺序执行,现在要跳来跳去。

错误追踪,异步让try catch直接跪了,为了能够捕获到异步的错误,有两种方案,分离回调和 first error。

jquery 的 ajax 就是典型的分离回调,示例代码如下:

function success(data) {
    console.log(data);
}
function error(err) {
    console.error(err);
}
$.ajax({}, success, error);

Node.js 最开始的异步接口,采用的是 first error,第一个参数是 error 对象,代码示例如下:

function callback(err, data) {
    if (err) {
        // 出错
        return;
    }
    // 成功
    console.log(data);
}
async("url", callback);

下面来看下模拟同步问题,node 的 io 相关的 api 都是异步的,我只是想要 python 那种同步的个脚本,并不容易,下面来看一个例子。

一段很容易理解的同步代码:

for (let i = 0; i < arr.length; ++i) {
    arr[i] = sync(arr[i]);
}

然而如果是异步的话要这么写,主要next函数的实现:

(function next(i, len, callback) {
    if (i < len) {
        async(arr[i], function (value) {
            arr[i] = value;
            next(i + 1, len, callback);
        });
    } else {
        callback();
    }
})(0, arr.length, function () {
    // All array items have processed.
});

关于回调地狱,并发执行和信任问题,我建议你阅读《你不知道的JavaScript.中卷》,其中有非常深入的介绍。

观察者

观察者模式需要一个 pub 和 sub 函数,或者其他类似工具,一定有同学说,这不还是回调吗?请看清楚,回调是因为观察者模式,而不是异步。

其实回调的问题,观察者模式并没有解决。

sub("1", function () {
    moveTo(100, 0, function () {
        pub("2");
    });
});

sub("2", function () {
    moveTo(200, 0, function () {
        pub("3");
    });
});

sub("3", function () {
    moveTo(300, 0, function () {
        pub("4");
    });
});

// 无限不循环

pub 和 sub 函数的是怎么实现的呢?最简单的实现如下:

let eventMap = {};
function pub(msg, ...rest) {
    eventMap[msg] &&
        eventMap[msg].forEach((cb) => {
            cb(...rest);
        });
}

function sub(msg, cb) {
    eventMap[msg] = eventMap[msg] || [];
    eventMap[msg].push(cb);
}

Promise

下面是 Promise 实现上面的例子:

moveTo(100, 0)
    .then(function () {
        return moveTo(200, 0);
    })
    .then(function () {
        return moveTo(300, 0);
    })
    .then(function () {
        return moveTo(400, 0);
    })
    .then(function () {
        // 无限不循环
    });

再来看一下,moveTo 函数有何不同,看出来没?

function moveTo(x = 0, y = 0) {
    return new Promise(function (resolve, reject) {
        move(".box").x(x).y(y).end(resolve);
    });
}

初次接触 Promise,可能会觉得这不就是回调吗?请再次注意,这里的回调不是为了异步,而是 Promise 协议。

Promise 其实是一种控制反转,举个例子,就是原来我们要给异步函数传入一个回调函数,现在变成了异步函数返回一个 Promise 对象,堪称神来之笔,而 Promise 就是实现这种反转的工具,Promise 是一个双方约定的契约(规范)。

其实 Promise 还有很多优点,要知道 Promise 是后面新技术的基础,堪称一切异步方案的粘合剂,没有 Promise,可能就不会有 Generator,那么为什么说是可能呢?请看 Generator 一节。

Promise 解决了回调的一些问题,但并没有全部解决,比如 Promise 有很好的错误追踪,避免了回调地狱,对并发执行很友好,因为 Promise 只决议一次,就很好的解决了信任问题。

但 Promise 对违反直觉并不友好,回调变成了长长的 Promise 链。

看一下模拟同步的代码,貌似成功解决了:

var chain = Promise.resolve();
for (let i = 0; i < arr.length; ++i) {
    chain = chain.then(() => async(arr[i]).then((x) => (arr[i] = x)));
}

Generator

Generator 是一个革命性特性,ES2015(ES6)中引入,让原本必须一次执行完毕函数,现在可以在中间暂停,并在下次继续执行,这就让 js 可以模拟协程的概念。

其实关于 Generator 我开始是拒绝的,这东西本来不是为异步而生,是为了产生持续生成的数据,和迭代器是天生一对,而把他用在异步上优点类似那 float 做布局(float 本来只是为了实现文字环绕的效果),有点绕,不是很好理解。

动画的例子用 generator 实现如下,就好像是同步的一样:

function* gen() {
    yield moveTo(100, 0);
    yield moveTo(200, 0);
    yield moveTo(300, 0);
    yield moveTo(400, 0);

    // 无限不循环
}

run(gen);

moveTo 函数如下:

function moveTo(x = 0, y = 0) {
    return new Promise(function (resolve, reject) {
        move(".box").x(x).y(y).end(resolve);
    });
}

看吧,多么简洁,简直完美,但我发誓当你看到 run 函数后你就疯了。

function run(fn) {
    let gen = fn();

    function next(data) {
        var res = gen.next(data);
        if (res.done) return res.value;

        res.value.then(function (data) {
            next(data);
        });
    }

    next();
}

生成器要实现异步,必须得有启动函数,也就是 run,好在社区有封装好的 run 函数,比如 co 这个库。

生成器能很好的解决 Promise 不能解决的问题,如违反直觉,繁琐的代码,但也有其自身的问题,比如不太好理解,蹩脚,需要启动器。

Promise 那一节,我说如果没有 Promise 就可能没有 Generator,而不是必须呢?因为 Generator 实现异步除了基于 Promise,还可以基于 thunk 函数,感兴趣的你自行查阅吧,祝好。

async/await

那么有没有一种完美方案呢?还真有啊,ES2017(ES8)给我们带来了异步函数,这家伙简直就是自带启动器的生成器函数,好吧他就是给予 Generator 的。

动画的例子用异步函数来实现:

async function run() {
    await moveTo(100, 0);
    await moveTo(200, 0);
    await moveTo(300, 0);
    await moveTo(400, 0);

    // 无限不循环
}

run();

moveTo 函数如下:

async function moveTo(x = 0, y = 0) {
    await new Promise(function (resolve, reject) {
        move(".box").x(x).y(y).end(resolve);
    });
}

完美同步般的代码,自带启动器,世界终于重归美好,终极异步方案,还学什么 generator 啊。

总结

本文介绍了 JavaScript 中异步编程的全部方法,希望帮助你,更好的理解异步编程,并选择正确的方法,最后借用网上的一张图来收尾总结区别:


来源: 稀土掘金

链接: https://fly63.com/article/detial/12092

通过alert方法,去理解js中阻塞、局部作用域、同步/异步任务

javascript中alert是Bom中的成员函数,alert对话框是模态的,具有阻塞性质的,不点击是不会执行后续代码的。js的阻塞是指在调用结果返回之前,当前线程会被挂起, 只有在得到结果之后才会继续执行。

如何优化async代码?更好的编写async异步函数

如何优化async代码?更好的编写async函数:使用return Promise.reject()在async函数中抛出异常,让相互之间没有依赖关系的异步函数同时执行,不要在循环的回调中/for、while循环中使用await,用map来代替它

【JS】异步处理机制的几种方式

Javascript语言的执行环境是单线程,异步模式非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。

js异步加载方式有哪些?_详解异步加载js的多种方案

js异步加载又被称为非阻塞加载,浏览器在下载JS的同时,还会进行后续页面处理。那么如何实现js异步加载呢?下面整理了多种实现方案供大家参考。异步加载js方案:Script Dom Element、onload时的异步加载、$(document).ready()、async属性、defer属性、es6模块type=module属性

Nodejs 处理异步(获取异步数据并处理)的方法

回调函数方式:将异步方法如readFile封装到一个自定义函数中,通过将异步方法得到的结果传给自定义方法的回调函数参数。事件驱动方式:使用node events模块,利用其EventEmitter对象

JS常用的几种异步流程控制

JavaScript引擎是基于单线程 (Single-threaded) 事件循环的概念构建的,同一时刻只允许一个代码块在执行,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列 (job queue) 中

前端异步编程之Promise和async的用法

传统的异步解决方案采用回调函数和事件监听的方式,而这里主要记录两种异步编程的新方案:ES6的新语法Promise;ES2017引入的async函数;Generator函数(略)

异步的JavaScript

JS本身是一门单线程的语言,所以在执行一些需要等待的任务(eg.等待服务器响应,等待用户输入等)时就会阻塞其他代码。如果在浏览器中JS线程阻塞了,浏览器可能会失去响应,从而造成不好的用户体验。

js 多个异步的并发控制

请实现如下的函数,可以批量请求数据,所有的URL地址在urls参数中,同时可以通过max参数 控制请求的并发度。当所有的请求结束后,需要执行callback回调。发请求的函数可以直接使用fetch。

解读react的setSate的异步问题

将setState()认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React可能会推迟它,稍后会一次性更新这些组件。React不会保证在setState之后,能够立刻拿到改变的结果。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!