javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JS异步解决方案

一篇文章详解JS的四种异步解决方案

作者:chengbo_eva

异步在JS中是常用的,下面这篇文章主要给大家介绍了关于JS四种异步解决方案的相关资料,文中通过代码介绍的非常详细,对大家学习或者使用js具有一定的参考借鉴价值,需要的朋友可以参考下

前言

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

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

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

同步&异步的概念

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

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

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

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

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

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

js中异步的应用场景

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

实现异步的四种方法

对于「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」「fulfilled」「rejected」就是「Promise」中的三种状态啦~

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

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

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

示例代码如下所示:

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」 函数是一个普通函数,但是有两个特征:

在普通函数中,我们想要一个函数最终的执行结果,一般都是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的语法糖:」

 所谓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的使用才是正解!」

总结

到此这篇关于JS四种异步解决方案的文章就介绍到这了,更多相关JS异步解决方案内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文