JS异步编程的5种方式
前言
总所周知,JS语言的执行环境是单线程的(这里不考虑Worker),在单线程环境中,所有的任务都是串行执行的,一旦某个任务长时间运行,那么,后续的任务都将阻塞。为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步和异步。
本文将会介绍异步编程的5种方式:
回调函数callback
观察者模式 or 发布订阅模式
Promise
生成器generator和yield
async和await
最后,还会带大家手撸一份简单的Promise和async函数。阅读本文,小编建议大家先去简单的了解一下Promise,Generator,async。
正文
01 回调函数
回调函数是异步编程最基础的方式,因为在JS中函数是“一等公民”,所以回调函数可以作为参数传递到主体函数并执行。代码如下,
function call(callback) {
setTimeout(() => {
callback();
}, 300);
console.log('call');
}
function callee() {
console.log('callee');
}
call(callee);
执行代码的效果是 callee -> call,callee函数在300毫秒后执行,并且不会阻塞call函数内的其他代码。
回调函数的优点是简单、容易理解和部署。比如vue-rouer的导航守卫和nodeJS中的next都是使用回调函数的例子,其缺点是不利于代码的阅读和维护,各个部分高度耦合,多个异步操作下容易形成回调地狱。
02 观察者模式 or 发布订阅模式
观察者模式和发布订阅模式同样也可以用来异步编程,只需要在异步结果返回后,发布或触发对应的函数即可,相应的代码如下,
const eventBus = {
events: {},
on(e, f) {
this.events[e] ? this.events[e].push(f) : (this.events[e] = [f]);
},
emit(e) {
let event = this.events[e];
event && event.length && event.map((f) => f());
}
};
function fun() {
console.log('fun');
}
function eve() {
setTimeout(() => {
eventBus.emit('event');
}, 300);
console.log('eve');
}
eventBus.on('event', fun);
eve();
上面的代码也能达到不阻塞的效果,(两种模式可以查看小编之前写的文章),整个流程变成了事件驱动,就像我们DOM的事件处理一样。这种模式也比较容易理解,可以去耦合,形成模块化,但是代码的运行流程就不是很透明了。
03 Promise
ES6给我们带来了新的异步处理方案:Promise - Promise对象表示异步操作的最终完成(或失败)及其结果值。Promise有三种状态:初始态pending, 完成态fulfilled,使用then方法接收,失败态rejected,使用catch方法接收。下面请看例子,
new Promise((resolve,reject) => {
setTimeout(() => {
resolve();
}, 300);
console.log('p');
}).then(() => {
console.log('then');
});
Promise将异步处理变成了链式操作,解决了回调地域的问题,并且有一套完整的处理机制,使得流程更加清晰。但是Promise也有几个不能忽视的缺点,比如如果不设置回调函数,那么promise内部的错误就无法反映到外部,同时Promise的后续处理是一个微任务,将将响应回调函数延迟到同步代码的后面,一定程度上降低了效率。如果需要处理多个异步操作,那么会看到一堆的then,代码的可阅读性就降低了。
04 生成器Generator和yield
Generator函数和yield关键字是ES6提供的另一种改变执行流程,进行异步编程的方案。Generator函数有两个区分于普通函数的部分:一是在 function 后面,函数名之前有个 * ;二是函数内部有 yield 表达式。下面请看例子,
function run() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('success');
}, 300);
});
}
function* gen() {
let data = yield run();
console.log('data = ', data);
}
let it = gen();
it.next() // {value: Promise, done: false}
it.next() // {value: undefined, done: true}
generator函数会返回一个迭代器,通过next方法执行下一个流程,也就是每一个yield关键字的上一行代码。虽然 Generator函数也能用于处理异步,但是如果generator函数内有多个流程yield,那么,我们得一个一个的next下去,所以,generator函数更适合于做流程控制。
function* gen() {
let data = yield run();
console.log('data = ', data);
let data = yield run();
console.log('data = ', data);
let data = yield run();
console.log('data = ', data);
}
为了解决这个问题,async随着诞生。
05 async和await
ES6的Promise和Generator虽然提供了很好的异步处理方案,但在使用和理解上稍微有点复杂,所以在ES7带来了新的关键字async和await, async函数本质是Generator函数的语法🍬。在写法上,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await。例子如下
function p() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('success');
});
});
}
async function run() {
let d = await p();
console.log('d = ', d);
}
run() // Promise {<pending>}
在async函数中,异步操作可以像同步代码一样书写。 d会等待p函数成功返回,且整个函数最后返回的也是promise。
async函数的到来,让我们就是根本不用关心它是不是异步,因此,很多人认为它是异步操作的终极解决方案。
上面给大家简单介绍了5种异步处理方案,下面为大家再上两道菜,讲讲Promise和async的简单实现,也是面试官们爱问的问题。
06 来一份红烧Promise
首先,如果你要手写一个Promise,你得了解一下Promise A+规范 (https://promisesaplus.com/)。该规范定义了Promise必要的一些属性,方法和约定。简单的,我将之归纳成3点:
状态模式
观察者模式
链式调用
下面先完成第一步 - 状态模式,在Promise A+中规定了 Promise的三种状态,初始态pending,完成态fulfilled和失败态rejected。fulfilled,rejected只能由pending转变而来,且不能反向转变。同时Promise的状态决定了Promise返回后的后续操作。
function internalReject(reason) {
if (this.status === 'pending') {
this.reason = reason;
this.status = 'rejected';
}
return;
}
function internalResolve(value) {
if (this.status === 'pending') {
this.value = value;
this.status = 'fulfilled';
}
return;
}
function _Promise(excutor) {
this.value = undefined;
this.reason = undefined;
this.status = 'pending';
try {
excutor(internalResolve.bind(this), internalReject.bind(this));
} catch (err) {
internalReject(this, err);
}
}
Promise是通过new关键字构造的实例,传入的参数是一个excutor函数,用来处理异步的结果。Promise上有三个属性,实际Promise上则是PromiseStatus和PromiseValue,这三个属性分别是:
value: 保存Promise进入完成态的值
reason: 保存为什么Promise被rejected的值
status: Promise三种状态
第二步 - 观察者模式。在Promise的then和catch方法中传递的处理函数实质就是注册了Promise完成态和失败态的回调函数。那么,我们使用两个变量存储,并在then和catch中存储回调函数,在Promise状态改变时,自动触发回调函数即可。下面请看代码。
function internalReject(reason) {
if (this.status === 'pending') {
// ...
this.rejectCbks.forEach((f) => f());
}
return;
}
function internalResolve(value) {
if (this.status === 'pending') {
// ...
this.resolveCbks.forEach((f) => f());
}
return;
}
function _Promise(excutor) {
// ...
// 成功和失败的回调函数
this.resolveCbks = [];
this.rejectCbks = [];
}
_Promise.prototype.then = function (onFulfilled, onRejected) {
// Promise A+ 2.2 then方法
onFulfilled = isFunction(onFulfilled) ? onFulfilled : (val) => val;
onRejected = isFunction(onRejected)
? onRejected
: (err) => {
throw err;
};
let _promise = new _Promise((resolve, reject) => {
if (this.status === 'pending') {
this.resolveCbks.push(() => {
onFulfilled(this.value);
});
this.rejectCbks.push(() => {
onRejected(this.reason);
});
}
});
// Promise A+ 返回promise
return _promise;
}
_Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};
第三步 - 链式调用。完成了上面两步,我们还不能进行链式调用,我们还需要在then方法做进一步的处理。
function resolvePromise(promise, x, resolve, reject) {
// promise A+ 循环引用
if (promise === x) {
reject(new TypeError('Promise-chain cycle'));
}
// 防止重复调用
let called = false;
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then;
// 如果then是函数,默认为promise了
if (typeof then === 'function') {
// 如果 then 是一个函数,以x为this调用then函数
then.call(
x,
(y) => {
if (called) return;
called = true;
resolvePromise(promise, y, resolve, reject);
},
(error) => {
if (called) return;
called = true;
reject(error);
}
);
} else {
// 如果then不是一个函数,则 以x为值fulfill promise。
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
_Promise.prototype.then = function (onFulfilled, onRejected) {
// ...
let _promise = new _Promise((resolve, reject) => {
if (this.status === 'pending') {
this.resolveCbks.push(() => {
let x = onFulfilled(this.value);
resolvePromise(_promise, x, resolve, reject);
});
this.rejectCbks.push(() => {
let x = onRejected(this.reason);
resolvePromise(_promise, x, resolve, reject);
});
} else if (this.status === 'fulfilled') {
let x = onFulfilled(this.value);
resolvePromise(_promise, x, resolve, reject);
} else {
let x = onRejected(this.reason);
resolvePromise(_promise, x, resolve, reject);
}
});
return _promise;
};
在resolvePromsie函数中,我们需要处理以下问题:
Promise的循环调用
回调函数的重复调用
判断上一个then函数的返回值,如果是Promise则继续调用,否则resolve
这里面的then逻辑可能有点绕,需要仔细去理解一下,完成了上面的步骤,我们就实现了一个简单的Promise。下面我们上第二道菜 - async。
07 上一道清蒸async
在上面的generator函数中,多个yield是需要手动调用next方法才能进入下一步的,而在async函数中处理了这一点,在实质上,他是generator函数的语法糖。
let getData = () => new Promise((resolve) => setTimeout(() => resolve('data')));
async function asyncTest() {
let d = await getData();
console.log('d = ', d);
let d1 = await getData();
console.log('d1 = ', d1);
return 'success';
}
上面包含async函数的代码变成generator后如下。所以,我们只有实现一个自动next的asyncToGenerator函数即可。
function asyncToGenerator(function* test() {
let d = yield getData();
console.log('d = ', d);
let d1 = yield getData();
console.log('d1 = ', d1);
return 'success';
})
我们知道generator的next函数会返回value和done,那么,我们只要在done等于false继续执行下一步,done为true时返回结果值。下面请看代码。
function asyncToGenerator(generatorFunc) {
return function () {
const gen = generatorFunc.apply(this, arguments);
return new Promise((resolve, reject) => {
function step(key, arg) {
let generatorResult;
try {
generatorResult = gen[key](arg);
} catch (error) {
return reject(error);
}
const { value, done } = generatorResult;
if (done) {
return resolve(value);
} else {
return Promise.resolve(value).then(
(val) => step('next', val),
(err) => step('throw', err)
);
}
}
step('next');
});
};
}
我们使用一个step函数处理generator的next和done即可,最后在done为true时才resolve最终的值。
结束语
以上就是小编为大家介绍的5种异步编程方法,async函数无疑是现在比较好的方式,实际中,大家可以多多使用。而对于手写代码,个人建议自己先手写,后面可以参考corejs或是babel的代码。
我们对Promise和async函数已经有一定的了解了,下面留下两道题测测自己的基础。
课后题目
如何结束一个Promise的链式调用
写出下面代码执行的结果
console.log('script start');
async function async1() {
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
async1();
setTimeout(function () {
console.log('setTimeout');
}, 0);
new Promise((resolve) => {
console.log('Promise');
resolve();
})
.then(function () {
console.log('promise1');
})
.then(function () {
console.log('promise2');
});
console.log('script end');
同学们好好做题哦~, 最后,希望文章中的内容可以对大家有所帮助,感谢支持~