vlambda博客
学习文章列表

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上则是PromiseStatusPromiseValue,这三个属性分别是:

  • 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函数已经有一定的了解了,下面留下两道题测测自己的基础。


课后题目

  1. 如何结束一个Promise的链式调用

  2. 写出下面代码执行的结果

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

    同学们好好做题哦~, 最后,希望文章中的内容可以对大家有所帮助,感谢支持~