JavaScript实现异步编程方案总结

308 阅读9分钟

什么是异步编程,为什么要使用异步编程呢?

1. Event Loop

介绍异步编程之前,先来介绍一下事件循环(Event Loop)吧!

我们都知道javascript是一门单线程语言,用来处理用户的交互,实现DOM的增删改等,一次事件循环只处理一个事件响应,使得脚本的执行相对连续,所以就有了事件队列,用来存储待执行的事件,那么事件队列中的事件是从哪里push进来的呢,这就是Event Loop的作用了,它的作用主要是将定时触发器线程,异步HTTP请求线程等满足特定条件下的回调函数push到事件队列中,等到javascript引擎空闲的时候去执行,这就是Event Loop的作用了。
简单来说,就是程序中设置有两个线程,一个负责程序本身的,被称为“主线程”,另一个负责主线程与其他线程(主要是各种I/O操作)的通信,被称为“Event Loop线程”,它属于一个程序结构,用来等待和发送消息事件。

2. 微任务和宏任务

介绍完Event Loop之后,来介绍一下微任务和宏任务。
javascript引擎的执行是有先后顺序的,它会先执行主线程中的任务,等待主线程中的任务执行完毕,就会在微任务队列中查找是否有微任务,有微任务就执行微任务,微任务执行完毕就会在宏任务队列中进行查找是否有宏任务,然后开始执行宏任务。

宏任务:事件队列中的每一个事件都是一个宏任务,例如主代码块,setTimeOut,setInterval,setImmediate等等
微任务:当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件,例如Promise.then就是一个典型的微任务,proness.nextTick等等。
微任务一般一次性执行完毕,宏任务一般需要多次事件循环才能执行完毕。

3. 异步编程的解决方案——回调函数

回调函数是最基本的实现异步编程的解决方案,在es5里面,我们通常使用的就是这种方式来达到异步编程的目的

假设有两个函数fn1,fn2,后者需要等待前者的执行结果,正常的情况下是这样:

fn1();
fn2();

但如果函数fn1是一个很耗时的任务,那么整个程序就会很慢,页面加载时间就会延长,所以就可以考虑使用回调函数的方式,将fn2作为fn1的回调函数

function fn1(callback){
   setTimeOut(function(){
       //fn1的任务执行代码
       callback();
   },100)
}

也就是相当于fn1(fn2)这样
这种方式比较简单,容易理解,把同步操作变成了异步操作,fn1不会阻塞程序运行,相当于先执行程序的主要逻辑,将耗时的任务推迟执行,但是这种方式会不利于代码的维护,各个函数嵌套,高度耦合,流程会比较混乱,如果代码过长会不易于理解阅读,而且每个任务只能指定一个回调函数。

4. 异步编程的解决方案——事件监听

事件监听主要是采用事件驱动的模式,任务的执行不取决于代码的顺序,而取决于某个事件是否发生

f1.on('done', f2);
1

function f1(){
  setTimeout(function () {
   // f1的任务代码
    f1.trigger('done');
   }, 1000);
}

即f1若执行就会触发f2的执行,这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

5. 异步编程的解决方案——发布和订阅

假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

优点:该方法的性质与“事件监听”类似,但是明显优于后者,因为我们可以通过查看“消息中心”从而知道存在多少个信号,每个信号有多少订阅者,从而监听程序的运行。

举个例子吧:

//采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
jQuery.subscribe("done", f2);  //f2向"信号中心"jQuery订阅"done"信号。

function f1(){
  setTimeout(function () {
    // f1的任务代码
    jQuery.publish("done");
  }, 1000);
}
//jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。

6. 异步编程的解决方案——Promise

重中之重来啦,前面介绍的三种方案除了回调函数其它两种基本没有使用过,哈哈哈哈,还好ES6为我们提供了一种新的异步编程实现方式——Promise,让我们可以快速高效的实现异步编程。
下面就先来介绍一下Promise吧!

  • Promise的三种状态

Promise是一个对象,内部会存在一个异步操作,提供了统一的api来获取异步操作的结果,它有三种状态,分别是pending(等待)、resolved(已完成)、rejected(已拒绝),三种状态可以进行转变,可以从等待变成已完成,也可以从等待变成已拒绝,不可逆,一旦从一种状态变成另一种状态之后就是不可逆的了,不能再转变回去。

  • Promise所传递的参数

Promise构造函数接收一个函数作为参数,函数的两个参数为resolve和reject,resolve为成功时调用,reject为失败时调用,两个方法会将异步操作结果通过参数传递出去。

  • Promise中重要的API

promise.all([p1,p2,p3]):将几个promise对象打包放到一个数组里面,打包完还是promise对象,这种方法必须确保几个promise对象都为resolve状态,如果有一个对象为reject状态,则就会以该失败的结果进行接下来的回调

promise.race([p1,p2,p3]):这个方法其实和promise.all功能是一样的,唯一不同的地方在与,打包的几个promise对象,只要有一个为resolve状态就可以成功返回

  • Promise处理错误的三种方式

A:then(resolve,reject),then方法中的第二个回调是失败的时候做的失败时候的事
promise遇到then,也就是resolve和reject的时候是异步的,所以try…catch不起作用

function f(val){
    return new Promise((resolve,reject) => {
          if(val){
               resolve({ name:'小明' },100);  //成功时也可以传递一个值,但需要注意的是只能传递一个参数,传两个的话第二个参数是拿不到的
          }else{
               reject('404');  //传递参数,错误的原因
          }
     });
}
f(false)
     .then( (data, data2) => {
         console.log(data2);  //undefined
     }, e => {  
         console.log(e);  //404
     })

//需要注意的是只能传递一个参数,如果传递了两个参数,第二个参数是拿不到的,data2会为undefined			
f(true)				  
    .then( (data,data2) => {					    
         console.log(data2);  //打印结果为undefined				  
    },e => {					    
       console.log(e);				  
    })

B:使用catch进行捕获错误

function f(val) {
   return new Promise((resolve, reject) => {
     if (val) {
       resolve({ name: "小明" });
     } else {
       reject("404");
     }
   });
}

f(true)
  .then((data) => {
    console.log(data); //{name:'小明'}
    return f(false); //返回的promise是失败的话,后面的then对这个失败没有处理的话,就会继续往下走
  })
  .then(() => {
    console.log("我永远不会被输出");
  })
  .then(
    () => {},
    (e) => console.log("失败");
  ) //
  .catch((e) => {
    //上面处理了错误的话,这个catch就不会运行了
    console.log(e); //404
  })
  .then(() => {
    //catch后面可以继续then,但是如果后面的then出错了,跟上一个catch就没有关系了
    console.log(e);
    return f(false);
  })
  .catch(); //如果最后一个catch有错误,会无限catch

C:finally方法进行捕获错误,不论成功还是失败,finally中的内容一定会执行,即使返回一个成功的promise,下面的finally也会执行,所以可以利用这个在finally中做一些收尾的工作。

function f(val){
    return new Promise((resolve,reject) => {
        if(val){
            resolve({ name:'小明' });  
        }else{
            reject('404');  
        }
    });
}

f(true)
    .then(data => {
        console.log(data);  //{name:'小明'}
        return f(false);
    })
    .catch(e => {
        console.log(e)   //404
        return f(false);  //如果返回的是失败的promise,控制台最后一行会报错uncaught (in promise) 404
    })
    .finally( () => {
        console.log(100)  //100
    })
  • Promise的缺点

Promise的缺点在于:错误需要回调函数来进行捕获、一旦创建就会立即执行,不会中途停止、当处于等待状态的时候无法得知目前进展到哪一阶段,是刚刚开始,还是即将完成。

  • Promise如何实现异步编程

它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:

f1().then(f2);

function f1(){
   var dfd = $.Deferred();
  setTimeout(function () {
    // f1的任务代码
    dfd.resolve();
  }, 500);
  return dfd.promise;
}

这种方式可以指定多个回调,f1().then(f2).then(f3)…,回调函数变成了链式写法,程序的流程可以看得很清楚。
如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,不用担心是否错过了某个事件或信号。

7. 异步编程的解决方案——Async

Async也是ES6提供的一种解决方案。

async function fn(){return 'hello world'}

async会自动执行,await后面可以是Promise对象,也可以是原始数据类型,返回的是一个promise对象,return后的值会作为Promise对象then方法中成功回调函数中的参数

Async函数的特点:await只能放在async函数里面,语义化更强,await后面可以是Promise对象,也可以是数字,字符串,布尔值等,只要await语句后面Promise对象状态变为reject,那么整个async函数会终止执行。

Async函数处理错误:

可以使用try…catch进行捕获错误,因为await后面跟着的是Promise对象,当有异常的情况下会被Promise内部的catch捕获,而await就是一个then的语法糖,并不会捕获异常,那么就需要使用try…catch进行捕获错误了,并进行相应的逻辑处理

Async实现异步编程:

// 等待执行函数
function sleep(timeout) {
  return new Promise((resolve) => {
    setTimeout(resolve, timeout)
  })
}

// 异步函数
async function test() {
  console.log('test start')
  await sleep(1000)
  console.log('test end')
}

console.log('start')
test()
console.log('end')

JavaScript实现异步编程的方式就介绍这么多,总结了好几个小时,也属于跨天完成了,哈哈哈哈哈,个人觉得Promise实现异步编程更好一些,各有各的好处吧,掌握这些面对面试官的提问一定是没有问题的!