面试必考题 —— Promise

196 阅读6分钟

JavaScript是一门单线程语言,它一次只能干一件事情。但是在我们日常编程中,总会遇到一些代码需要等待其它代码执行完成之后才能够执行,例如前端需要访问后端数据,那么这个过程就要花费一定的时间。那如果碰到类似这样的情况,是先执行这个耗时的代码还是先执行后面的其他代码呢? 今天我们就一起探讨一下这个问题。

异步

要了解这个概念,让我们先来看一段代码(setTimeout()是一个用于在指定的时间延迟后执行一个特定的代码块或函数)

let a = 1

setTimeout(() => {
    a = 2
}, 1000);

console.log(a);

看这段代码,它的运行结果是1还是2呢?按照代码从上往下执行的原则,是不是应该输出的2呢?那我们看看结果到底是多少。

微信图片_20241201142007.png

运行结果并不是2,是1。这是为什么呢?这就是js中代码的异步性。像这段代码中,它不会等待setTimeout()执行完,而是会将其先挂起,等到后续不耗时的代码执行完毕后再回过头来执行耗时的代码。那这时候就会产生一个问题,如果我们一定要先执行setTimeout()里的代码再执行后续代码,比如登录系统用户输入账号密码后需要先像后端数据库访问账号密码再返回来判断是否登录成功,那像这样的情况就一定得先等后端返回数据后才能进行判断。那这样的话,我们要进行什么处理呢?

解决异步

让我们先来看一个例子:

function xq(){
        setTimeout(() => {
            console.log('章总相亲了')
        }, 2000);
    }

function marry(){
        setTimeout(() => {
            console.log('章总结婚了');
        },1000)
    }

xq()
marry()

微信图片_20241201224450.png

众所周知,肯定是先相亲再结婚的,那按照我们代码编写的顺序,我们是想要先执行xq()再执行marry()的,但是执行结果确实相反,那我们要如何解决这个问题呢?

1.回调

function xq(){
        setTimeout(() => {
            console.log('章总相亲了')
            marry()
        }, 2000);
    }

function marry(){
        setTimeout(() => {
            console.log('章总结婚了');
        },1000)
    }

xq()

我们可以直接把marry()放在xq()console.log('章总相亲了')之后,这样输出结果就是先相亲再结婚了

运行结果: 微信图片_20241201225132.png 这是在比较简单的情况下,只需要进行一次回调,那如果遇到更复杂的情况呢,那代码就会变得异常复杂。让我们再看一个例子:

function a(cb, cb2, cb3){
    setTimeout(() => {
        console.log('a 执行完毕');  
        cb(cb2, cb3)
    },1000)
}

function b(cb, cb3){
    setTimeout(() => {
        console.log('b 执行完毕');  
        cb(cb3)
    },1500)
}

function c(cb){
    setTimeout(() => {
        console.log('c 执行完毕');  
        cb()
    },5000)
}

function d(){
    console.log('d 执行完毕');
    
}

a(b, c, d)

我们想要按照 a b c d 的顺序执行,那就得进行嵌套,这里还只有三层嵌套,那如果在写比较大的项目,需要很多层的嵌套,就会产生一个叫做“回调地狱”的情况,那会使代码变得很冗长,变得难以阅读和维护。那这个时候我们就需要找别的可以解决异步的方法了。

2.Promise

promise是js官方定义的一个方法

定义

Promise 的构造函数如下

new Promise((resolve, reject) => { 
    // 异步操作 
});

参数

Promise 构造函数接收一个函数作为参数,这个函数又接收两个参数:

  1. resolve: 当异步操作成功完成时调用,并传递结果值。
  2. reject: 当异步操作失败时调用,并传递失败原因(错误信息)。

状态

一个 Promise 对象可以处于以下三种状态之一:

  1. Pending(等待中) : 初始状态,既不是成功,也不是失败。
  2. Fulfilled(已兑现) : 表示异步操作成功完成,resolve 被调用。
  3. Rejected(已拒绝) : 表示异步操作失败,reject 被调用。

让我们先来看一个例子:

function xq(){
    return new Promise((resolve,reject) => {  //两个参数,可以控制成功与失败
        setTimeout(() => {
            console.log('章总相亲了');
            resolve('相亲顺利')
        }, 2000);
    })
}

function marry(){
        setTimeout(() => {
            console.log('章总结婚了');
        },1000)
    }

xq().then(res => {         // res接收的是resolve()里的参数,在这里就是'相亲顺利'
    console.log(res);  
    marry()
})

promise()resolve()调用了,意味着异步操作成功完成,那.then()才会执行

运行结果: 微信图片_20241201232616.png

如果结婚后再有小孩呢,那又要怎样修改呢?

function xq(){
    return new Promise((resolve,reject) => {  //两个参数,可以控制成功与失败
        setTimeout(() => {
            console.log('章总相亲了');
            resolve('相亲顺利')
        }, 2000);
    })
}

function marry(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('章总结婚了');
            resolve('结婚顺利')
        },1000)
    })
    
}

function baby(){
    console.log('小章出生了');
}

xq().then((res) => {     
    console.log(res); 
    marry().then((res) => {
        console.log(res);
        baby()
    })
})

再加.then()去执行即可

运行结果:

微信图片_20241201234909.png

那如果章总想要很多个小孩,那这段代码岂不是会变成xq().then().then().then().then().then() ...,那这样的话代码就会很不美观也不好阅读,js提供了更加美观的写法:

xq()
.then((res) => {
    console.log(res);
    marry()
})
.then((res) => {
    console.log(res);
    baby()
})

这也就是 js 中 then 的链式调用


有异步操作成功,那对应的也应该有异步操作失败,也就是再Promise中调用reject,如果调用了reject,那就说明代码主动报错了,那就需要捕捉这个错误,不然代码就会报错。如何捕捉错误呢?用.catch就可以了,例如这个例子:

function a(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log('a');
            // resolve('a 执行完毕')
            reject('a 失败了')
        },1000)
    })
}

function b(){
    console.log('b'); 
}

a()
.then(res => { 
    console.log(res);
})
.catch(err => {
    console.log(err);
    
})

运行结果:

微信图片_20241201235958.png

那这个时候有人可能就有一个疑问了,那如果promise既没调用resolve又没调用reject呢?答案是:promise中的代码依旧会执行,但是thencatch都不会执行,大家可以看这个例子以便理解:

let realut = 1

const myPromise = new Promise((resolve, reject) => {
    console.log('hello world')
    // 这里没有调用 resolve 或 reject
});

myPromise.then(result => {
    console.log(result);
}).catch(error => {
    console.log(error);
});

微信图片_20241202000203.png

实战展示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>  
    <ul id='ul'></ul>

    <script>
        function getData(){
            return new Promise((resolve, reject) => {
                let xhr = new XMLHttpRequest();
                xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true)
                xhr.send();
                xhr.onreadystatechange = function(){
                    if(xhr.readyState == 4 && xhr.status == 200){
                        console.log(JSON.parse(xhr.responseText));
                        resolve(JSON.parse(xhr.responseText).movieList)
                    }
                }
            })
            
    }

        

        function showList(data){
            data.forEach(item => {
                let li = document.createElement('li')
                li.innerText = item.nm
                document.getElementById('ul').appendChild(li)
            });
            //console.log(data);
        }

        getData().then((res) => {
            showList(res)
        })
            
    </script>
</body>
</html>

这是一段获取一个电影网站信息并将这电影网站上的电影名返回给我们 的html代码,因为要想获得这些电影名,那么我们得先往这个网站发送请求,得到允许之后才可以返回电影名给我们。因此这里就需要解决 发送请求耗时所带来的异步问题,那就可以使用promise进行解决。

运行结果: image.png


看到这里,相信大家对js的异步特性有更好的理解,也学习到了将异步处理成同步顺序的方法, 感谢观看。

image.png