JavaScript 异步的 “超级英雄”——Promise 登场!

255 阅读8分钟

异步

首先我们需要了解:

JS 是单线程语言,一次只能干一件事(多线程执行效率快但是开销性能也多)

ps.进程是计算机中正在运行的程序的实例

ps.线程是进程内部的执行单元,是程序执行流的最小单元。一个进程可以包含多个线程

了解完毕,我们来看一个代码:

let a = 1

console.log(a);

setTimeout(() => {           //时间器回调函数,模拟耗时的代码,1s之后再执行
    console.log(a);
}, 1000)

这里会先执行第一个输出,在执行第二个输出:

image.png

这样的执行顺序的原理是什么呢?

JS 是单线程语言,遇到需要耗时执行的代码,会将其先挂起,等到后续不耗时的代码执行完毕后,再回过头来执行耗时的代码。

我们再来实验一次,这里把回调函数放在全文中间:

let a = 1

console.log(a);

setTimeout(() => {           //时间器回调函数,模拟耗时的代码,1s之后再执行
    a = 2
    console.log(a);
}, 1000)

console.log(a);

可以看到,这个回调函数的 a = 2console.log(a) 会最后执行。

image.png

在 JS 中,异步是指将耗时的代码挂起并放入任务队列,然后优先执行后面不耗时的代码,而非严格按照代码的书写顺序依次执行。

这里接收了一个 a = 2 ,最后想要输出刚刚接收到的 a = 2,那这里这样做,可以实现吗?

let a = 1

console.log(a);

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

console.log(a);

很明显,不行,这里 a = 2 是最后执行的。

image.png

我们可以把它提炼成这样一个代码:

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

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

a()
b()

来看看它的执行结果,还是 b() 先执行。

image.png

那我们这里要先让 b() 执行,再让 a() 执行,要怎么做?也就是怎么解决异步?

解决异步

1. 回调

首先,第一个方法就是回调函数。

//接收一个实参 b
 function a(cb) {
     setTimeout(() => {
         console.log('a 执行完毕'); 
         cb()
     }, 1000)
 }

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

 a(b)

来看看结果,可以看到确实是 a 执行之后,b 才执行。

image.png

如果我们这里,b 的执行需要 a 的值,c 的执行需要 b 的值,d 的执行需要 c 的值,那我们这里就需要,一直往函数里增加形参:

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

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

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

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


a(b, c, d)

使用这个方法,虽然解决了代码的异步问题,但是同时也带来一个问题,回调地狱。

回调地狱:代码嵌套过深,维护成本大,一旦出现问题很难排查

2. promise

第二种方法。也就是今天的主角,Promise 。

我们这里来举个例子:

function pw() {
    setTimeout(() => {
        console.log('排位上分了');
        
    }, 2000)
}

function lp() {
    console.log('打完啦');
}

pw()
lp()

我们想要的顺序是,“排位上分了”,接着是“打完了”。但是这里因为异步问题,输出不是这样子的。

image.png

那我们这里来使用 promise,来实现我们想要的输出:

function pw() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('排位上分了');
            resolve()  //开关,over
        }, 2000)
    })
}

function lp() {
    console.log('打完啦');
}

pw().then(() => {
    lp()
})

代码里的也就是 promise 的使用方法,

函数 pw 返回一个新创建的 Promise 对象。

promise 有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)

在 Promise 的构造函数中,传入一个回调函数,该回调函数接收两个参数 resolve 和 reject,它们是用于改变 Promise 状态的函数。这里通过 setTimeout 函数来模拟一个异步操作,设置了一个 2 秒的延迟。

当延迟 2 秒结束后,会在控制台输出 排位上分了,然后调用 resolve 函数。这会将 Promise 的状态从 pending 转变为 fulfilled,表示异步操作成功完成,并且可以继续执行后续通过 then 方法绑定的回调函数。

=====================================================

介绍完 pw 函数,那我们来捋一捋这个代码的执行流程:

首先调用 pw 函数,这会立即返回一个 Promise 对象,并开始执行其内部的异步操作(即等待 2 秒)。然后通过调用 Promise 对象的 then 方法,传入一个回调函数。当 pw 函数内部的 Promise 状态变为 fulfilled(也就是 resolve 被调用后),就会执行 then 方法中传入的这个回调函数。

在 pw 的 Promise 状态变为 fulfilled 后,then 方法中的回调函数会被执行,这里面调用了 lp 函数,所以会在控制台输出 打完啦

image.png

拓展一下,resolve() 里面可以放东西,就像下面这样子,这里后面用 res 来接收:

resolve 函数就像一个开关。调用 resolve 函数,就相当于告诉这个 Promise 相关的流程:“嘿,我这边的异步任务已经顺利搞定啦”。若未调用 resolve 函数,那么后续的.then 语句将不会被执行。

function pw() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('排位上分了');
            resolve('太有鸡蛋了')  //开关,over
        }, 2000)
    })
    
}

function lp() {
    console.log('打完啦');
}

pw().then((res) => {
    console.log(res);
    lp()
})

这里也是成功输出了 resolve()的内容:

image.png

那我们这里再增加一个函数,要怎么做呢?

function pw() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('排位上分了');
            resolve('太有鸡蛋了')  //开关,over
        }, 2000)
    })
    
}

function lp() {
    setTimeout(() => {
        console.log('打完啦');
    }, 2000)
}

function sf() {
    console.log('今天上了三颗星');
}

pw().then((res) => {
    console.log(res);
    lp()
    sf()
})

输出显然不符合我们的预期。

image.png

这里函数 lp 也返回一个新创建的 Promise 对象:

function pw() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('排位上分了');
            resolve('太有鸡蛋了')  //开关,over
        }, 2000)
    })
    
}

function lp() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('打完啦');
            resolve()  //开关,over
        }, 2000)
    })
}

function sf() {
    console.log('今天上了三颗星');
}

pw().then((res) => {
    console.log(res);
    lp().then((res) => {
        sf()
    })
})

可以看到输出是我们想要的顺序:

image.png

===================================================

我们为了美观,把下面这段注释的代码,修改成这个样子,这也就是 then 的链式调用

// pw().then((res) => {
//     console.log(res);
//     lp().then((res) => {
//         sf()
//     })
// })

//then的链式调用
pw()
.then((res) => {
    return lp()       //把又一个promise的实例对象给了.then
})
.then(() => {
    sf()
})

==================================================

简单看下面这个代码,和前面的差不多,模拟了一个异步操作,用 promise 解决异步问题:

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

//.then放在了Promise的原型上
a()
.then(res =>{
    console.log(res);
})

image.png

那如果我们在这里使用 reject() 呢?会怎么样?

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

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

报错了,“UnhandledPromiseRejectionWarning” 明确提示这是一个未处理的 Promise 拒绝情况的警告。

image.png

所以这里应该添加一个 catch 方法来捕获并处理这个 rejected 状态:

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

//.then放在了Promise的原型上
a()
.then(res =>{          //开关开了,里面的才会执行
    console.log(res);
})

.catch(err =>{       //捕获错误
    console.log(err);
})

这里执行成功。

image.png

OK啊,你已成功习得运用 Promise 来妥善处理异步难题的诀窍。

=====================================================

接下来我们来在实战中解决异步问题:

给一个网址,要求先访问这个网址,再输出里面的电影名字到网页上。 这个网址里的数据是这些:

image.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>
        // 定义函数getData,用于发送接口请求并获取数据
        function getData() {
            // 发接口请求
            return new Promise((resolve, reject) => {
                // 创建一个XMLHttpRequest对象,用于发送HTTP请求
                let xhr = new XMLHttpRequest();
                // 配置请求,设置请求方法为GET,指定请求的URL,true表示异步请求
                xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
                // 发送请求
                xhr.send();

                // 监听XMLHttpRequest对象的状态变化
                xhr.onreadystatechange = function () {
                    // 当请求状态为4(表示请求已完成)且状态码为200(表示请求成功)时
                    if (xhr.readyState === 4 && xhr.status === 200) {
                        // 将响应文本解析为JSON格式,并在控制台打印出来
                        console.log(JSON.parse(xhr.responseText));
                        // 从解析后的JSON数据中提取电影列表数据(假设接口返回的数据结构中有movieList字段包含电影列表信息)
                        // 并通过resolve将电影列表数据传递出去,使Promise状态变为fulfilled,以便后续处理
                        resolve(JSON.parse(xhr.responseText).movieList)
                    }
                }
            });
        }

        // 定义函数showList,用于展示获取到的电影列表数据
        function showList(data) {
            // 遍历传入的电影列表数据数组
            data.forEach(item => {
                // 为每个电影数据项创建一个新的<li>元素
                let li = document.createElement('li');
                // 将电影名称(假设每个电影数据项有nm字段表示电影名称)设置为<li>元素的文本内容
                li.innerText = item.nm;
                // 将创建好的<li>元素添加到页面上id为"ul"的无序列表中
                document.getElementById('ul').appendChild(li);
            });
            // console.log(`拿到了数据${data}`);
        }

        // 调用getData函数发起接口请求,返回一个Promise对象
        getData()
            // 当Promise状态变为fulfilled(即接口请求成功并获取到数据)时,执行这个回调函数
           .then(res => {
                // 将获取到的电影列表数据传递给showList函数进行展示
                showList(res)
            });
    </script>
</body>

</html>

这里附上了一些注释,有些内容或许还是看不懂,没关系,我们只需要知道在真正的项目中,Promise 是这样子使用的就可以了。

这里也是成功的拿到了 nm 也就是电影名:

image.png