事件循环
深入了解 Promise 之前,我们需要对 JavaScript 的“事件循环(Event Loop)”机制有一定的了解。那么什么是“事件循环”呢?这个如果细说的话又能开一个系列,这里只简单的描述一下。
在 JavaScript 中异步任务分为两种:宏任务(Task)、微任务(MicroTask)。在主线程运行的时候,同步任务会直接运行,异步任务则会根据其类型分别进入“宏任务队列”和“微任务队列”,在同步任务执行完毕后,会先清空“微任务队列”,然后再清空“宏任务队列”,其大致的运行图如下:

案例简析
了解了“事件循环”后,我们基本明白了 JavaScript 代码的执行机制,Promise 在其中扮演的是一个“微任务”,基于此,我们分析几个案例来一步步了解 Promise。
Promise 状态
众所周知,Promise 有三种状态:pending、fulfilled 和 rejected,代码尝试如下:
const p = new Promise((res, rej) => {
setTimeout(() => {
res();
}, 3000);
});
console.log(p);
console.log(Promise.resolve());
console.log(Promise.reject());
// 浏览器中运行结果:
// Promise { <pending> }
// Promise {<fulfilled>: undefined}
// Promise {{<rejected>: undefined}}
Promise 内的执行函数是同步任务
很多新手容易弄混的部分是“Promise 是异步函数,因此它初始化传入的执行函数属于异步任务”,同样用代码来解释:
console.log('start');
const p = new Promise((res, rej) => {
console.log('working...');
res();
});
console.log('end');
// 浏览器中的运行结果:
// start
// working...
// end
Promise 状态不可逆
除了最开始的 pending 态外,一旦转变为 fulfilled 或 rejected 状态后,其状态就不会再改变了:
const p = new Promise((res, rej) => {
res(1);
rej(2);
});
console.log(p);
// 浏览器中的运行结果:
// Promise {<fulfilled>: 1}
then 链返回的是新 Promise 对象
then 链中,无论你返回什么内容,它都会给你给你包装一个 Promise 的外壳:
const p = new Promise((res,rej) => { res() });
console.log(p.then());
console.log(p.then(() => 1));
console.log(p.then(() => new Promise((res, rej) => {res()})))
// 浏览器中的运行结果:
// Promise {<pending>}
// Promise {<pending>}
// Promise {<pending>}
then 链有两个函数,第二个 onReject 函数如果有设置,那么其后续会返回至 fulfilled 状态链:
new Promise((res,rej)=>{
rej(1)
})
.then(
res => {},
rej => {
console.log('err', rej);
// return 2
}
)
.then(res => {
console.log('success', res)
})
// 浏览器中的运行结果:
// err 1
// success undefined
⚠️ 注意:在日常开发中不要设置 then 链的 onReject 函数,说不定那天就因为常规思维而被它给坑了 (
´∇`
)。
then 链是可透传的
当 then 链内的内容为非函数的情况下,其会将上一个 Promise 的结果和状态传递给下一个函数:
Promise.resolve('start')
.then(console.log('pass'))
.then(res => console.log(res));
// 浏览器中的运行结果:
// pass
// start
【小贴士】其实工作中经常会用到这个特性,比如 then 链默认不写第二个函数,从而使用 catch 在 then 链末尾单独对错误进行处理,当然 catch 的返回值默认也是 fulfilled 状态哟。
【拓展】finally 效果与“then 链透传”一致,仅多了内部函数执行,感兴趣的可自行尝试一下~
案例深入
嵌套 Promise,then 链返回非 Promise 结果
刚刚聊的都是较为基础的例子,想要捋清 Promise 的知识可能还需要一些稍有难度的案例,这样才能加深对它的印象。那么看一下这个嵌套 Promise 的例子,弄懂了它,基本上就能对 Promise 有一定的理解了,不会被大多数的面试题给难住:
Promise.resolve()
.then(() => {
console.log('1-1');
new Promise((res, rej) => {
console.log('1-2');
res();
}).then(() => {
console.log('1-3');
}).then(() => {
console.log('1-4');
});
return 'Anything except promise func, also no "return"';
})
.then(() => {
console.log('2-1');
});
这里注意到,我们在第一个 then 链内调用了一个新的 Promise 方法,但没有返回值,那么运行的结果会是顺序执行吗?在浏览器上运行得到的结果为:
1-1
1-2
1-3
2-1
1-4
可以看到 “2-1” 在 “1-4” 之前打印了,可这又是为什么呢?让我们一步步分析来看:
- 【Step1】then 链先打印 1-1,然后进入 Promise 函数内;
- 【Step2】打印 Promise 内部的 1-2,执行 resolve 函数,并将紧接着的 then 链存入微任务事件队列;
- 【Step3】跳出 Promise 并运行至 then 链结尾,无返回值,默认 resolve 处理并将下一个 then 链存入微任务队列;
- 【Step4】此时微任务队列有俩微任务,依次执行处理,后续内容就是依次打印 1-3 和 2-1 ,最后再打印 1-4 了;
微任务队列内容变化如下:
/** 每步微任务队列内容 */
// 【Step 1】
[
then(() => {
console.log('1-1');
new Promise((res, rej) => {
console.log('1-2');
res();
}).then(() => {
console.log('1-3');
}).then(() => {
console.log('1-4');
});
})
.then(() => {
console.log('2-1');
})
]
// 【Step2】
[
then(() => { console.log('1-3'); })
.then(() => { console.log('1-4'); }),
then(() => { console.log('2-1'); })
]
// 【Step3】
[
then(() => { console.log('1-4'); })
]
从此结果我们可以了解到,在无返回值的情况下最好不要在内部处理别的 Promise 函数并接上 then 链来达到步骤控制,很容易会出现一些意想不到的问题。在业务中,很容易就会写出这样的代码,如:
// 加入 http 是封装好的 Axios 文件
const initFunc = async () => {
// 等初始化
await http.post('getInfo').then(info => {
const { userId, userRightIds } = info;
// 获取用户信息
http.post('getUserInfo', {userId}).then(() => {
// 处理用户信息
});
// 获取用户信息
http.post('getRightList', {userRightIds}).then(() => {
// 处理权限信息
});
});
// 初始化后处理别的事
// ...(电脑前,为什么没拿到权限数据和用户信息???弄个 setTimeout 吧)
}
模拟请求的代码如下:
async function test () {
let testVal = 1;
await Promise.resolve().then(() => {
// 加长 then 链模拟请求的耗时
Promise.resolve().then().then().then().then().then().then(() => {
testVal = 2;
})
})
console.log(testVal)
}
test();
// 结果是:1
所以,业务代码内尽量不要嵌套写 Promise,用 async/await 拆拆,或者每个内部 Promise 函数加上对应的 async/await 方法令其执行完再做其它操作,这样就不会写出 bug 了。
嵌套 Promise,then 链返回 Promise 结果
当然,嵌套 Promise 也是有坑存在的,说不定那天面试也会面到,当然项目内是不会出现这种写法的(有的话那么需要问问是谁面他进来的,同时还得考虑考虑是否继续和他共事),返回 Promise 结果的例子咱先不看,先分解一下,看如下例子:
Promise.resolve()
.then(() => { console.log('1-1'); })
.then(() => { console.log('1-2'); })
.then(() => { console.log('1-3'); });
Promise.resolve()
.then(() => { console.log('2-1'); })
.then(() => { console.log('2-2'); })
.then(() => { console.log('2-3'); });
// 浏览器结果为(就不换行输出了):
// 1-1 2-1 1-2 2-2 1-3 2-3
这结果想必难不倒大家,可以很轻松的解答出来,那么再代入嵌套的例子看看,应该就好分析了:
Promise.resolve()
.then(() => {
// 别名: then1
Promise.resolve()
.then(() => { console.log('1-1'); })
.then(() => { console.log('1-2'); })
.then(() => { console.log('1-3'); });
// 别名 then2
return Promise.resolve()
.then(() => { console.log('2-1'); })
.then(() => { console.log('2-2'); })
.then(() => { console.log('2-3'); });
})
// 别名 then3
.then(() => {
console.log('3-1')
});
// 浏览器结果为(就不换行输出了):
// 1-1 2-1 1-2 2-2 1-3 2-3 3-1
因为 then3 依赖 then2 的状态改变,而 then2 又需要和 then1 抢微任务队列的资源,因此返回的结果就是交替的结果,3-1 最后打印。
但是,我们把 then2 链精简一下,只留一个 2-1,按道理来说 3-1 应该在 1-2 之后打印的,但实际结果如下:
Promise.resolve()
.then(() => {
// 别名: then1
Promise.resolve()
.then(() => { console.log('1-1'); })
.then(() => { console.log('1-2'); })
.then(() => { console.log('1-3'); });
// 别名 then2
return Promise.resolve()
.then(() => { console.log('2-1'); });
})
// 别名 then3
.then(() => {
console.log('3-1')
});
// 浏览器结果为(就不换行输出了):
// 1-1 2-1 1-2 1-3 3-1
为什么和预测结果不一样了呢?其实此问题的问题点并不在 Promise 身上,听我一一分析。
- 首先,在 then2 打印 2-1 后,其会返回一个新的 Promise 对象放入微任务队列,它的返回值为 undefined
- **然后,**then1 打印 1-2 并将 1-3 所在 then 链传入
- 紧接着,这个返回值为 undefiend 的 Promise 对象执行,然后返回一个 undefiend 值,这就回到上面返回值为非 Promise 的情形了,紧接着 then3 就放入了微任务队列
- 再看微任务队列,发现 then3 就在 then1 那最后一个链之后,那结果就呼之欲出了。
代码解如下:
【Step1】
[
// then1
then(() => { console.log('1-1'); })
.then(() => { console.log('1-2'); })
.then(() => { console.log('1-3'); }),
// then2
then(() => { console.log('2-1'); })
]
【Step2】
[
// then1
then(() => { console.log('1-2'); })
.then(() => { console.log('1-3'); }),
// then2 的 then 链会返回一个 Promise 对象
Promise.resolve(undefined)
]
【Step3】
[
then(() => { console.log('1-3'); }),
then(() => { console.log('3-1'); })
]
此需要注意的是,then 链处理返回结果为 Promise 类型时,其会多一个 Promise.resolve(undefined) 的隐藏链。