目录

同步&异步的概念

js中异步的应用场景

实现异步的四种方法

1、 回调函数

2、Promise

3、Generator

4、 async/await


        「异步编程」是前端工程师日常开发中经常会用到的技术,也是校招面试过程中常考的一个知识点。

        通过掌握「异步编程」的四种方式,可以让我们能够更好地处理JavaScript中的异步操作,提高代码的性能和用户体验。

        因此,「今天就想和大家来聊聊JS异步编程的四种方式!」

同步&异步的概念

在讲这四种异步方案之前,我们先来明确一下同步和异步的概念:

        所谓「同步(synchronization)」,简单来说,就是「顺序执行」,指的是同一时间只能做一件事情,只有目前正在执行的事情做完之后,才能做下一件事情。

        「同步操作的优点」在于做任何事情都是依次执行,井然有序,不会存在大家同时抢一个资源的问题。

        「同步操作的缺点」在于「会阻塞后续代码的执行」。如果当前执行的任务需要花费很长的时间,那么后面的程序就只能一直等待。

        所谓「异步(Asynchronization)」,指的是当前代码的执行不影响后面代码的执行。当程序运行到异步的代码时,会将该异步的代码作为任务放进「任务队列」,而不是推入主线程的调用栈。等主线程执行完之后,再去任务队列里执行对应的任务即可。

        因此,「异步操作的优点就是:不会阻塞后续代码的执行。」

js中异步的应用场景

开篇讲了同步和异步的概念,那么在JS中异步的应用场景有哪些呢?

  • 「定时任务」:setTimeout、setInterval

  • 「网络请求」:ajax请求、动态创建img标签的加载

  • 「事件监听器」:addEventListener

实现异步的四种方法

        对于「setTimeout、setInterval、addEventListener」这种异步场景,不需要我们手动实现异步,直接调用即可。

        但是对于「ajax请求」「node.js中操作数据库这种异步」,就需要我们自己来实现了~

1、 回调函数

在微任务队列出现之前,JS实现异步的主要方式就是通过「回调函数」

以一个简易版的Ajax请求为例,代码结构如下所示:

function ajax(obj){
 let default = {
   url: '...',
   type:'GET',
   async:true,
   contentType: 'application/json',
   success:function(){}
    };

 for (let key in obj) {
        defaultParam[key] = obj[key];
    }

    let xhr;
    if (window.XMLHttpRequest) {
        xhr = new XMLHttpRequest();
    } else {
        xhr = new ActiveXObject('Microsoft.XMLHTTP');
    }
    
    xhr.open(defaultParam.type, defaultParam.url+'?'+dataStr, defaultParam.async);
    xhr.send();
    xhr.onreadystatechange = function (){
        if (xhr.readyState === 4){
            if(xhr.status === 200){
                let result = JSON.parse(xhr.responseText);
                // 在此处调用回调函数
                defaultParam.success(result);
            }
        }
    }
}

我们在业务代码里可以这样调用「ajax请求」

ajax({
   url:'#',
   type:GET,
   success:function(e){
    // 回调函数里就是对请求结果的处理
   }
});

        「ajax请求」中的success方法就是一个回调函数,回调函数中执行的是我们请求成功之后要做的进一步操作。

        这样就初步实现了异步,但是回调函数有一个非常严重的缺点,那就是「回调地狱」的问题。

        大家可以试想一下,如果我们在回调函数里再发起一个ajax请求呢?那岂不是要在success函数里继续写一个ajax请求?那如果需要多级嵌套发起ajax请求呢?岂不是需要多级嵌套?

如果嵌套的层级很深的话,我们的代码结构可能就会变成这样:

图片

        因此,为了解决回调地狱的问题,提出了「promise」「async/await」「generator」的概念。

2、Promise

「Promise」作为典型的微任务之一,它的出现可以使JS达到异步执行的效果。

一个「Promise函数」的结构如下列代码如下:

const promise = new Promise((resolve, reject) => {
 resolve('a');
});
promise
    .then((arg) => { console.log(`执行resolve,参数是${arg}`) })
    .catch((arg) => { console.log(`执行reject,参数是${arg}`) })
    .finally(() => { console.log('结束promise') });

        如果我们需要嵌套执行异步代码,相比于回调函数来说,「Promise」的执行方式如下列代码所示:

const promise = new Promise((resolve, reject) => {
 resolve(1);
});
promise.then((value) => {
     console.log(value);
     return value * 2;
    }).then((value) => {
     console.log(value);
     return value * 2;
    }).then((value) => {
    console.log(value);
    }).catch((err) => {
  console.log(err);
    });

即通过then来实现多级嵌套(「链式调用」),这看起来是不是就比回调函数舒服多了~

每个「Promise」都会经历的生命周期是:

  • 进行中(pending) :此时代码执行尚未结束,所以也叫未处理的(unsettled)

    已处理(settled) :异步代码已执行结束 已处理的代码会进入两种状态中的一种:
    • 已拒绝(rejected):遇到错误,异步代码执行失败 ,由reject()触发

    • 已完成(fulfilled):表明异步代码执行成功,由resolve()触发

因此,「pending」「fulfilled」「rejected」就是「Promise」中的三种状态啦~

        需要注意的是,在「Promise」中,要么包含resolve() 来表示 「Promise」 的状态为fulfilled,要么包含 reject() 来表示「Promise」的状态为rejected。

        不然我们的「Promise」就会一直处于pending的状态,直至程序崩溃...

除此之外,「Promise」不仅很好的解决了链式调用的问题,它还有很多高频的操作:

  • ·Promise.all(promises) :接收一个包含多个Promise对象的数组,等待所有都完成时,返回存放它们结果的数组。如果任一被拒绝,则立即抛出错误,其他已完成的结果会被忽略

  • ·Promise.allSettled(promises) : 接收一个包含多个Promise对象的数组,等待所有都已完成或者已拒绝时,返回存放它们结果对象的数组。每个结果对象的结构为{status:'fulfilled' // 或 'rejected', value // 或reason}

  • ·Promise.race(promises) : 接收一个包含多个Promise对象的数组,等待第一个有结果(完成/拒绝)的Promise,并把其result/error作为结果返回

示例代码如下所示:

function getPromises(){
    return [
        new Promise(((resolve, reject) => setTimeout(() => resolve(1), 1000))),
        new Promise(((resolve, reject) => setTimeout(() => reject(new Error('2')), 2000))),
        new Promise(((resolve, reject) => setTimeout(() => resolve(3), 3000))),
    ];
}

Promise.all(getPromises()).then(console.log);
Promise.allSettled(getPromises()).then(console.log);
Promise.race(getPromises()).then(console.log);

打印结果为:

图片

图片

图片

3、Generator

        「generator」是ES6提出的一种异步编程的方案。因为手动创建一个iterator十分麻烦,因此ES6推出了「generator」,用于更方便的创建iterator。

        也就是说,「generator」就是一个返回值为iterator对象的函数。

在讲「generator」之前,我们先来看看iterator是什么:

iterator中文名叫「迭代器」。它为js中各种不同的数据结构(Object、Array、Set、Map)提供统一的访问机制。
任何数据结构只要部署iterator接口,就可以完成遍历操作。
因此iterator也是一种对象,不过相比于普通对象来说,它有着专为迭代而设计的接口。

我们通过一个例子来看看generator的特征:

function* createIterator() {
  yield 1;
  yield 2;
  yield 3;
}
// generators可以像正常函数一样被调用,不同的是会返回一个 iterator
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

形式上,「generator」 函数是一个普通函数,但是有两个特征:

  • ·function关键字与函数名之间有一个星号

  • ·函数体内部使用yield语句,定义不同的内部状态

        在普通函数中,我们想要一个函数最终的执行结果,一般都是return出来,或者以return作为结束函数的标准。运行函数时也不能被打断,期间也不能从外部再传入值到函数体内。

        但在「generator」中,就打破了这几点,所以「generator」和普通的函数完全不同。

        当以function*  的方式声明了一个「generator」生成器时,内部是可以有许多状态的,以yield进行断点间隔。期间我们执行调用这个生成的「generator」,他会返回一个遍历器对象,用这个对象上的方法,实现获得一个yield后面输出的结果。

function* generator() {
    yield 1
    yield 2
};
let iterator = generator();
iterator.next()  // {value: 1, done: false}
iterator.next()  // {value: 2, done: false}
iterator.next()  // {value: undefined, done: true}

4、 async/await

最后我们来讲讲「async/await」,终于讲到这儿了!!!

「async/await」是ES7提出的关于异步的终极解决方案。我看网上关于「async/await」是谁的语法糖这块有两个版本:

  • 第一个版本说「async/await」是Generator的语法糖

  • 第二个版本说「async/await」是Promise的语法糖

其实,这两种说法都没有错。

「关于async/await是Generator的语法糖:」

        所谓generator语法糖,表明的就是「aysnc/await」实现的就是generator实现的功能。但是「async/await」比generator要好用。因为generator执行yield设下的断点采用的方式就是不断的调用iterator方法,这是个手动调用的过程。

        而async配合await得到的就是断点执行后的结果。因此「async/await」比generator使用更普遍。

「关于async/await是Promise的语法糖:」

如果不使用「async/await」的话,Promise就需要通过链式调用来依次执行then之后的代码:

function counter(n){
    return new Promise((resolve, reject) => { 
        resolve(n + 1);
    });
}

function adder(a, b){
    return new Promise((resolve, reject) => { 
        resolve(a + b);
    });
}

function delay(a){
    return new Promise((resolve, reject) => { 
        setTimeout(() => resolve(a), 1000);
    });
}
// 链式调用写法
function callAll(){
    counter(1)
       .then((val) => adder(val, 3))
       .then((val) => delay(val))
       .then(console.log);
}
callAll();//5

虽然相比于回调地狱来说,链式调用确实顺眼多了。但是其呈现仍然略繁琐了一些。

「async/await的出现,就使得我们可以通过同步代码来达到异步的效果」

async function callAll(){
   const count = await counter(1);
   const sum = await adder(count, 3);
   console.log(await delay(sum));
}
callAll();// 5

由此可见,「Promise搭配async/await的使用才是正解!」

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐