解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!

Posted 星期一研究室

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!相关的知识,希望对你有一定的参考价值。

我们平常在写 js 中,经常只管程序能跑就行,但很少去深究它的原理。更别说还有一个同步和异步的问题了。因此,程序往往有时候出现莫名其妙的卡死或者有时候执行顺序达不到我们想要的结果时自己都不知道往哪里找错。下面的这篇文章中,将讲解同步和异步的问题,以及如何解决异步问题的promise、async/await方法。

在看本文之前,建议大家先对 event loop 有一个了解。可以先看我的上一篇文章了解event loop以及微任务宏任务的相关知识,这样到后面看本文的 async/await 的时候会更友好一些。

下面开始进行本文的讲解。

一、单线程和异步

1、单线程是什么

(1) JS 是单线程语言,只能同时做一件事情

  • 所谓单线程,就是只能同时做一件事情,多一件都不行,这就是单线程。

(2) 浏览器和 nodejs 已支持 JS 启动进程,如 Web Worker

(3) JSDOM 渲染共用同一个线程,因为 JS 可修改 DOM 结构

  • JS 可以修改 DOM 结构,使得它们必须共用同一个线程,这间接算是一件迫不得已的事情。
  • 所以当 DOM 在渲染时 JS 必须停止执行,而 JS 执行过程 DOM 渲染也必须停止。

2、为什么需要异步

当程序遇到网络请求或定时任务等问题时,这个时候会有一个等待时间。

假设一个定时器设置10s,如果放在同步任务里,同步任务会阻塞代码执行,我们会等待10s后才能看到我们想要的结果。1个定时器的等待时间可能还好,如果这个时候是100个定时器呢?我们总不能等待着1000s的时间就为了看到我们想要的结果吧,这几乎不太现实。

那么这个时候就需要异步,通过异步来让程序不阻塞代码执行,灵活执行程序。

3、使用异步的场景

(1)异步的请求,如ajax图片加载

//ajax
console.log('start');
$.get('./data1.json', function (data1) {
    console.log(data1);
});
console.log('end');

(2)定时任务,如setTimeout、setInterval

//setTimeout
console.log(100);
setTimeout(fucntion(){
	console.log(200);           
}, 1000);
console.log(300);
//setInterval
console.log(100);
setInterval(fucntion(){
	console.log(200);           
}, 1000);
console.log(300);

二、promise

早期我们在解决异步问题的时候,基本上都是使用callback回调函数的形式 来调用的。形式如下:

//获取第一份数据
$.get(url1, (data1) => {
    console.log(data1);
    
    //获取第二份数据
    $.get(url2, (data2) => {
        console.log(data2);
        
        //获取第三份数据
        $.get(url3, (data3) => {
            console.log(data3);
            
            //还可以获取更多数据
        });
    });
});

从上述代码中可以看到,早期在调用数据的时候,都是一层套一层, callback 调用 callback ,仿佛深陷调用地狱一样,数据也被调用的非常乱七八糟的。所以,因为 callback 对开发如此不友好,也就有了后来的 promise 产生, promise 的出现解决了 callback hell 的问题。

用一段代码先来了解一下 Promise

function getData(url){
    return new Promise((resolve, reject) => {
        $.ajax({
            url,
            success(data){
                resolve(data);
            },
            error(err){
				reject(err);
            }
        });
    });
}

const url1 = '/data1.json';
const url2 = '/data2.json';
const url3 = './data3.json';
getData(url1).then(data1 => {
    console.log(data1);
    return getData(url2);
}).then(data2 => {
    console.log(data2);
    return getData(url3);
}).then(data3 => {
    console.log(data3);
}).catch(err => console.error(err));

大家可以看到,运用了 promise 之后,代码不再是一层套一层,而是通过 .then 的方式来对数据进行一个获取,这在写法上就已经美观了不少。那 promise 究竟是什么呢?接下来开始进行讲解。

1、promise的三种状态

  • pending :等待状态,即在过程中,还没有结果。比如正在网络请求,或定时器没有到时间。
  • fulfilled :满足状态,即事件已经解决了,并且成功了;当我们主动回调了 fulfilled 时,就处于该状态,并且会回调 then 函数。
  • rejected :拒绝状态,即事件已经被拒绝了,也就是失败了;当我们主动回调了 reject 时,就处于该状态,并且会回调 catch 函数。

总结:

  • 只会出现pending → fulfilled,或者pending → rejected 状态,即要么成功要么失败;
  • 不管是成功的状态还是失败的状态,结果都不可逆。

2、三种状态的表现和变化

(1)状态的变化

promise 主要有以上三种状态, pendingfulfilledrejected 。当返回一个 pending 状态的 promise 时,不会触发 thencatch 。当返回一个 fulfilled 状态时,会触发 then 回调函数。当返回一个 rejected 状态时,会触发 catch 回调函数。那在这几个状态之间,他们是怎么变化的呢?

1)演示1

先来看一段代码。

const p1 = new Promise((resolved, rejected) => {

});

console.log('p1', p1); //pending

在以上的这段代码中,控制台打印结果如下。

状态变化演示1

在这段代码中, p1 函数里面没有内容可以执行,所以一直在等待状态,因此是 pending

2)演示2

const p2 = new Promise((resolved, rejected) => {
    setTimeout(() => {
        resolved();
    });
});

console.log('p2', p2); //pending 一开始打印时
setTimeout(() => console.log('p2-setTimeout', p2)); //fulfilled

在以上的这段代码中,控制台打印结果如下。

状态变化演示2

在这段代码中, p2 一开始打印的是 pending 状态,因为它没有执行到 setTimeout 里面。等到后续执行 setTimeout 时,才会触发到 resolved 函数,触发后返回一个 fulfilled 状态 promise

3)演示3

const p3 = new Promise((resolved, rejected) => {
    setTimeout(() => {
        rejected();
    });
});

console.log('p3', p3);
setTimeout(() => console.log('p3-setTimeout', p3)); //rejected

在以上的这段代码中,控制台打印结果如下。

状态变化演示3

在这段代码中, p3 一开始打印的是 pending 状态,因为它没有执行到 setTimeout 里面。等到后续执行 setTimeout 时,同样地,会触发到 rejected 函数,触发后返回一个 rejected 状态的 promise

看完 promise 状态的变化后,相信大家对 promise 的三种状态分别在什么时候触发会有一定的了解。那么我们接下来继续看 promise 状态的表现。

(2)状态的表现

  • pending 状态,不会触发 thencatch
  • fulfilled 状态,会触发后续的 then 回调函数。
  • rejected 状态,会触发后续的 catch 回调函数。

我们来演示一下。

1)演示1

const p1 = Promise.resolve(100); //fulfilled
console.log('p1', p1);
p1.then(data => {
    console.log('data', data);
}).catch(err => {
    console.error('err', err);
});

在以上的这段代码中,控制台打印结果如下。

状态的表现演示1

在这段代码中, p1 调用 promise 中的 resolved 回调函数,此时执行时, p1 属于 fulfilled 状态, fulfilled 状态下,只会触发 .then 回调函数,不会触发 .catch ,所以最终打印出 data 100

2)演示2

const p2 = Promise.reject('404'); //rejected
console.log('p2', p2);
p2.then(data => {
    console.log('data2', data);
}).catch(err => {
    console.log('err2', err);
})

在以上的这段代码中,控制台打印结果如下。

状态的表现演示2

在这段代码中, p2 调用 promise 中的 reject 回调函数,此时执行时, p1 属于 reject 状态, reject 状态下,只会触发 .catch 回调函数,不会触发 .then ,所以最终打印出 err2 404

对三种状态有了基础了解之后,我们来深入了解 .then.catch 对状态的影响。

3、then和catch对状态的影响(重要)

  • then 正常返回 fulfilled ,里面有报错则返回 rejected
  • catch 正常返回 fulfilled ,里面有报错则返回 rejected

我们先来看第一条规则: then 正常返回 fulfilled ,里面有报错则返回 rejected

1)演示1

const p1 = Promise.resolve().then(() => {
    return 100;
})
console.log('p1', p1); //fulfilled状态,会触发后续的.then回调
p1.then(() => {
    console.log('123');
});

在以上的这段代码中,控制台打印结果如下。

then和catch对状态的影响演示1

在这段代码中, p1 调用 promise 中的 resolve 回调函数,此时执行时, p1 正常返回 fulfilled , 不报错,所以最终打印出 123

2)演示2

const p2 = Promise.resolve().then(() => {
    throw new Error('then error');
});
console.log('p2', p2); //rejected状态,触发后续.catch回调
p2.then(() => {
    console.log('456');
}).catch(err => {
    console.error('err404', err);
});

在以上的这段代码中,控制台打印结果如下。

then和catch对状态的影响演示2

在这段代码中, p2 调用 promise 中的 resolve 回调函数,此时执行时, p2 在执行过程中,抛出了一个 Error ,所以,里面有报错则返回 rejected 状态 , 所以最终打印出 err404 Error: then error 的结果。

我们再来看第二条规则catch 正常返回 fulfilled ,里面有报错则返回 rejected

1)演示1(需特别谨慎! !)

const p3 = Promise.reject('my error').catch(err => {
    console.error(err);
});
console.log('p3', p3); //fulfilled状态,注意!触发后续.then回调
p3.then(() => {
    console.log(100);
});

在以上的这段代码中,控制台打印结果如下。

then和catch对状态的影响演示3

在这段代码中, p3 调用 promise 中的 rejected 回调函数,此时执行时, p3 在执行过程中,正常返回了一个 Error这个点需要特别谨慎!!这看起来似乎有点违背常理,但对于 promise 来说,不管时调用 resolved 还是 rejected ,只要是正常返回而没有抛出异常,都是返回 fulfilled 状态。所以,最终 p3 的状态是 fulfilled 状态,且因为是 fulfilled 状态,之后还可以继续调用 .then 函数。

2)演示2

const p4 = Promise.reject('my error').catch(err => {
    throw new Error('catch err');
});
console.log('p4', p4); //rejected状态,触发.catch回调函数
p4.then(() => {
    console.log(200);
}).catch(() => {
    console.log('some err');
});

在以上的这段代码中,控制台打印结果如下。

then和catch对状态的影响演示4

在这段代码中, p4 依然调用 promise 中的 reject 回调函数,此时执行时, p4 在执行过程中,抛出了一个 Error ,所以,里面有报错则返回 rejected 状态 , 此时 p4 的状态为 rejected ,之后触发后续的 .catch 回调函数。所以最终打印出 some err 的结果。

4、then和catch的链式调用(常考)

学习完以上知识后,我们通过几道题来再总结回顾一下。

第一题:

Promise.resolve().then(() => {
    console.log(1);
}).catch(() => {
    console.log(2);
}).then(() => {
    console.log(3);
});

这道题打印的是 13 ,因为调用的是 promiseresolve 函数,所以后续不会触发 .catch 函数。

第二题:

Promise.resolve().then(() => {
    console.log(1);
    throw new Error('error');
}).catch(() => {
    console.log(2);
}).then(() => {
    console.log(3);
});

这道题打印的是 12 ,虽然调用的是 promiseresolve 函数,但是中间抛出了一个异常,所以此时 promise 变为 rejected 状态,所以后续不会触发 .then 函数。

第三题:

Promise.resolve().then(() => {
    console.log(1);
    throw new Error('error');
}).catch(() => {
    console.log(2);
}).catch(() => {  //这里是catch
    console.log(3);
});

这道题打印的是 123 ,跟第二题一样,中间抛出了一个异常,所以此时 promise 变为 rejected 状态,所以后续只触发 .catch 函数。

三、async/await

现代 js 的异步开发,基本上被 asyncawait 给承包和普及了。虽然说 promise 中的 .then.catch 已经很简洁了,但是 async 更简洁,它可以通过写同步代码来执行异步的效果。如此神奇的 asyncawait 究竟是什么呢?让我们一起来一探究竟吧!

1、引例阐述

先用一个例子来展示 promiseasync/await 的区别。假设我们现在要用异步来实现加载图片。

(1) 如果用 promise.then.catch 实现时,代码如下:

function loadImg(src){
    const picture = new Promise(
        (resolve, reject) => {
            const img = document.createElement('img');
            img.onload = () => {
                resolve(img);
            }
            img.onerror = () => {
                const err = new Error(`图片加载失败 ${src}`);
                reject(err);
            }
            img.src = src;
        }
    )
    return picture;
}

const url1 = 'https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58';
const url2 = 'https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=614367910,2483275034&fm=58';

loadImg(url1).then(img1 => {
    console.log(img1.width);
    return img1; //普通对象
}).then(img1 => {
    console.log(img1.height);
    return loadImg(url2); //promise 实例
}).then(img2 => {
    console.log(img2.width);
    return img2;
}).then(img2 => {
    console.log(img2.height);
}).catch(ex => console.error(ex));

(2) 如果用 async 实现时,代码如下:

function loadImg(src){
    const picture = new Promise(
        (resolve, reject) => {
            const img = document.createElement('img');
            img.onload = () => {
                resolve(img);
            }
            img.onerror = () => {
                const err = new Error(`图片加载失败 ${src}`);
                reject(err);
            }
            img.src = src;
        }
    )
    return picture;
}

const url1 = 'https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=2144980717,2336175712&fm=58';
const url2 = 'https://dss2.bdstatic.com/8_V1bjqh_Q23odCf/pacific/1990552278.jpg';


!(async function () 以上是关于解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!的主要内容,如果未能解决你的问题,请参考以下文章

Promise完全解读

Promise解决异步

js的三种异步处理

教你如何在工作中“偷懒”,python优雅的帮你解决

promise/async/await

Async/Await替代Promise的6个理由