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+ 返回promisereturn _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');
同学们好好做题哦~, 最后,希望文章中的内容可以对大家有所帮助,感谢支持~
