前端技术飞速发展,曾经的最佳实践,在未来可能被新的方法代替,而我们只有保持不断学习,才能不那么迷茫,异步编程一直是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 链。
0 Comments