带你初识前端面试官必问的promise(详解)

1,859 阅读10分钟

🐟什么是 Promise

想象你去一家餐厅点餐。你点完餐之后,服务员不会马上把食物给你,而是给你一个 “小纸条”(就像 Promise),这个 “小纸条” 代表着餐厅对你的一个承诺:他们会在食物做好之后给你。在食物还没做好的时候,这个承诺就处于 “进行中” 的状态。

🐟Promise的三种状态

  1. 进行中(pending) :就像你刚点完餐,厨师还在做菜,这个时候你的这个 “小纸条”(Promise)就处于等待的状态,也就是 “进行中”。
  2. 已完成(fulfilled):当厨师把菜做好了,服务员把菜端到你面前,这个时候 “小纸条” 的承诺就完成了,就像 Promise 的 “已完成” 状态。而且会带着做好的菜(相当于 Promise 成功后的返回值)。
  3. 已拒绝(rejected) :要是厨师在做菜的时候发现材料不够,做不了你点的菜,这时候就会通过服务员告诉你做不了,这个 “小纸条”(Promise)就处于 “已拒绝” 状态,同时会告诉你为什么做不了(相当于 Promise 失败后的错误原因)。

🐟为什么要用promise??

JavaScript 中,异步操作非常常见,例如读取文件、发起网络请求等。在 Promise 出现之前,通常使用回调函数来处理异步操作的结果。当有多个异步操作需要顺序执行时,就会出现回调函数嵌套回调函数的情况,这被称为 “回调地狱”promise的出现就是为了解决回调地狱问题!

首先我们先要了解异步和回调,回调地狱

  • 异步:需要耗时执行的代码
  • 回调:一种函数,它作为参数传递给另一个函数,在该另一个函数执行过程中的某个特定时机(比如异步操作完成后或某个事件触发时)被调用,以此来处理相应的结果或执行特定的后续操作。
  • 回调地狱: 嵌套过深,一旦出现问题,很难排查,维护难度太大

我们来看几段代码帮助我们理解一下这几个概念

var data = null   //全局定义了一个data
function a() {
    setTimeout(function () {
        data = 'hello'
    }, 1000)

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

}
a()
b()

首先定义了两个函数 a 和 b。函数 a 中使用 setTimeout 设置了一个延迟为 1000 毫秒的操作,在这个操作的回调函数中将变量 data 赋值为 'hello'setTimeout 是异步操作,所以当 b 函数被调用时,setTimeout 中的赋值操作还没有执行,因此 console.log(data) 会输出 null

但其实我们是想要输出hello的,在promise还没有被发明前,聪明的前辈们想到了一个办法去解决它,就是使用回调

function a(callback) {
 setTimeout(function () {
     const data = 'hello';
     callback(data);
 }, 1000);
}

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

a(b);

函数a接受一个回调函数作为参数。当setTimeout中的异步操作完成后,调用这个回调函数并将数据传递给它。然后在调用a函数时,将b函数作为回调传递进去,这样当异步操作完成后,数据会被传递给b函数进行处理,即输出数据hello

image.png

这样看起来回调还是很优雅的,但是他其实很不优雅,一旦嵌套的函数过多,就会造成回调地狱 我们先来写四个函数的情况看一看,

function func1(callback2, callback3, callback4) {
    console.log('Function 1');
    callback2(callback3, callback4);
}

function func2(callback3, callback4) {
    console.log('Function 2');
    callback3(callback4);
}

function func3(callback4) {
    console.log('Function 3');
    callback4();
}

function func4() {
    console.log('Function 4');
}

func1(func2, func3, func4);

是不是已经有点头晕了,如果是10个,20个,甚至是100个,是不是想想就很恐怖了,有种如果改了里面一行代码,就出现一片红色海洋的感觉了。他就像串联了一百个灯泡,如果突然一百个灯泡都没亮了,你能知道哪几个出了问题吗,是不是想想就恐怖,怪不得叫回调地狱呢

🐟引入promise去解决异步问题

我们通过结婚这个案例去讲解promise

function date() {
    setTimeout(() => {
        console.log('你相亲了');

    },3000)
}
function marry() {
    setTimeout(() => {
        console.log('你结婚了');

    },1000)
}
date()
marry()

你花了三天时间去相亲,然后花了一天时间结婚了(不要在意时间哈哈哈,就当是闪婚啦),我们定义了这两个函数,然后分别去调用,先调用date(),然后再是marry(),我们来看看结果

image.png

我的发,居然是先结婚,再相亲,这太逆天了,这是因为date()marry()函数分别使用setTimeout设置了异步操作。但是,由于 JavaScript 的异步执行机制,这些异步操作的执行顺序是不确定的,不能保证先执行date()函数中的异步操作,再执行marry()函数中的异步操作。可以用回调解决,当然我们这里不是用它,而是用promise,首先你想要结婚,肯定得在相亲的时候,使出浑身解数,许下很多承诺,结婚了我要送你大砖戒💍,住大房子🏠,这样别人才会和你结婚

return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('相亲了');
            resolve()
        }, 2000)
    })

🐟resolve

🐟改变 Promise 状态

这个操作就等同于你在date函数中,返回了一个promsie实例对象,其中加了一行resolve(),这个是什么呢,在这段代码中,resolve是一个函数,它是Promise构造函数的参数(一个执行函数)内部的一部分。resolve的主要作用是将Promise的状态从pending(进行中)转换为fulfilled(已成功)。

就好比你安排了一个任务(这里是setTimeout模拟的等待 2 秒后 “相亲” 这个任务),当这个任务顺利完成(相亲这个动作执行了,也就是console.log('相亲了')被执行),就通过resolve来告诉这个Promise:“嘿,我完成任务啦,你可以把状态更新为成功啦。”

Promise的状态变为fulfilled后,会触发与这个Promise相关联的.then()方法中的回调函数 即 date().then,完整代码如下

function date() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('相亲了');
            resolve()
        }, 2000)
    })
}
function marry() {
    setTimeout(() => {
        console.log('你结婚了');

    }, 1000)
}
date().then(() => {
    marry()
})

这样就正确输出了

image.png

是不是很丝滑! 接下来如果你还想生宝宝呢,呢是不是还是得哄哄你的媳妇,给一点promise(承诺),以后宝宝归我带,毕竟十月怀胎是很辛苦的! 我们再加一个

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

如果你是直接加到date().then()里显然是不可以的,先生宝宝再结婚。不能加到date().then()里。 image.png image.png

我们需要加到marry()里,我们要让marry有许诺的能力

 function date() {
    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('出生了');

}
date().then(() => {
    marry().then(() => {
        baby()
    })

})

但是如果一旦事情嵌套的多了,这样写then不太优雅,代码会很臃肿,所以官方将其优化了一下,我先不告诉大家正确的用法,先挖个坑,这个能正确输出吗

xq()
    .then(() => {
       marry()
    }).then(() => {
        baby()
    })

image.png

答案是NO,呢这是为什么呢,看起来很合理但是不行。我们来详细解释一下

  • date()被调用时,它返回的Promise开始执行。这个Promise内部通过setTimeout模拟一个异步操作,等待 2 秒后输出'相亲了',然后调用resolve,此时这个Promise的状态变为fulfilled(已成功)。
  • 一旦date()返回的Promise状态变为fulfilled,第一个.then中的回调函数() => { marry() }就会被执行。这里需要注意的是,虽然marry函数被调用了,但由于没有返回marry函数的返回值,这个.then实际上返回了一个默认的Promise对象,其状态是pending
  • marry函数本身返回一个Promise,它内部通过setTimeout模拟一个异步操作,等待 1 秒后输出'结婚了',然后调用resolve,使自己的状态变为fulfilled。但是这个状态变化暂时没有被有效地传递给后续的.then,因为前面的.then没有正确地返回marryPromise
  • 第一个.then返回的默认Promise(状态为pending)等待状态变化。当marry函数返回的Promise状态变为fulfilled时,由于第一个.then没有正确地返回这个Promise,第二个.then暂时无法直接获取到这个状态变化来执行。
  • 不过,在 JavaScript 的 Promise 链式调用机制下,它会继续往前查找已经确定状态(fulfilledrejected)的Promise。当它发现marry返回的Promise状态变为fulfilled后,第二个.then中的回调函数() => { baby() }就会被执行,从而在控制台输出'出生了'

简单的说就是

  1. date()返回的 Promise 在 2 秒后变为fulfilled,触发第一个.then,调用marry()但未正确返回其 Promise,导致第一个.then返回默认的pending状态的 Promise。
  2. marry自身返回的 Promise 在 1 秒后变为fulfilled,但由于第一个.then未正确返回,第二个.then暂时无法直接获取该状态变化。
  3. 在 Promise 链式调用机制下,第二个.then会继续往前查找已确定状态的 Promise,当发现marry的 Promise 变为fulfilled后,执行第二个.then的回调函数输出'出生了'

当你理解了之后我们给出你正确的使用方法

xq()
    .then(() => {
        return marry()
    }).then(() => {
        baby()
    })

为什么是return marry呢,这是因为在第一个.then中调用return marry()的话,可将marry函数返回的 Promise 对象传递给下一个环节,确保 Promise 链式调用能正确处理异步操作顺序。若没有此返回语句,第一个.then会默认返回状态为pending的新 Promise 对象,后续.then可能无法正确获取前面异步操作结果。

🐟传递成功结果值

resolve(x)不仅会转变Promise状态,同时resolve可以传递一个参数,这个参数会作为成功的值传递给.then方法中的回调函数。即传递x,我们改造上面的函数,来简单的运行一下

function date() {
    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('出生了');
}

date()
    .then(resultFromDate => {
        console.log('相亲结果:', resultFromDate);
        return marry();
    })
    .then(resultFromMarry => {
        console.log('结婚信息:', resultFromMarry);
        baby();
    });

image.png

是不是很nice, resolve传递的参数可以是任何 JavaScript 数据类型,如对象、数组、字符串、数字等。这里我们就不一一实验了,他的用途有很多!大家可以自行深入了解! 下面我们来介绍一下rejected

🐟rejected

在 Promise 中,“rejected” 状态表示 Promise 被拒绝,通常因异步操作失败或出错。 触发方式:可在 Promise 的执行函数内部通过调用reject()来触发 “rejected” 状态。例如:

function asyncOperation() {
    return new Promise((resolve, reject) => {
        // 模拟异步操作失败
        setTimeout(() => {
            reject(new Error("Async operation failed"));
        }, 2000);
    });
}

处理方式:使用.catch()方法来处理 “rejected” 状态。当 Promise 被拒绝时,.catch中的回调函数会被执行,接收拒绝的原因(这里是一个错误对象)并进行相应错误处理,比如在控制台打印错误消息。例如:

asyncOperation().catch(error => {
    console.log(error.message);
});

🐟 Demo

我在上面相亲结婚的基础上,加上了rejected,catch处理,方便大家理解,大家可以运行一下这个小Demo,自己上手试一试

function date() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const isSuccess = Math.random() > 0.5;
            if (isSuccess) {
                console.log('相亲了');
                resolve();
            } else {
                reject(new Error('相亲失败'));
            }
        }, 2000);
    });
}

function marry() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const isSuccess = Math.random() > 0.5;
            if (isSuccess) {
                console.log('结婚了');
                resolve();
            } else {
                reject(new Error('结婚失败'));
            }
        }, 1000);
    });
}

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

date()
    .then(() => {
        return marry();
    })
    .then(() => {
        baby();
    })
    .catch(error => {
        console.error('出现错误:', error.message);
    });

🐟 End

希望大家能有所收获,Promise 是一种用于处理异步操作的强大工具,它允许我们以更优雅和可读的方式处理异步代码。也是面试官最爱问的,先初步搞懂它,再去理解他的所有api,这就需要大家自己去看官方文档了,知识才会进脑子!

image.png