一、前言
Promise 官方文档中表示:Promise 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和 Rejected(已失败)。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。
但是经过实际测试从外部控制 Promise 的状态其实是可以办到的:Promise 的状态取决于 Promise 主体中是否调用了 resolved 或者 reject,因此我们只要把 resolved 和 reject 赋值给外部变量即可,也就是在外部声明两个变量保存 resolved 和 reject 就行了。
下面通过两个常见的场景来深入的理解:从外部改变 promise 内部状态
二、场景一
1. 需求分析
假设你有多个页面的一些功能需要先收集用户的信息才能允许使用,在点击使用某功能前先弹出信息收集的弹框,你会怎么实现呢?
以下是不同水平的前端同学的实现思路:
初级前端:我写一个模态框,然后复制粘贴到其他页面,效率很杠杠的!
中级前端:你这不便于维护,我们要单独封装一下这个组件,在需要的页面引入使用!
高级前端:封什么装什么封!!!写在所有页面都能调用的地方,一个方法调用岂不更好?
看看高级前端怎么实现的,以Vue3为例来看看下面的示例:
2. 解决方案
模态框组件:声明两个变量用于保存 promise 里的 resolve 和 reject 方法,突破作用域限制
js
<template>
<div class="modal" v-show="visible">
<div>
用户姓名:<input v-model="info.name" />
</div>
<button @click="handleCancel">取消</button>
<button @click="handleConfirm">提交</button>
</div>
</template>
<script setup>
import { provide } from 'vue';
const visible = ref(false);
const info = reactive({
name: ''
});
let resolveFn, rejectFn;
// 将信息收集函数函数传到下面
provide('getInfoByModal', () => {
visible.value = true;
return new Promise((resolve, reject) => {
// 将两个函数赋值给外部,突破promise作用域
resolveFn = resolve;
rejectFn = reject;
});
})
</script>
点击提交或者取消按钮时修改 Promise 的状态(从外部的形式去修改
),比如执行 resolveFn 方法也就相当于 resolve(info),此时 Promise 的状态发生改变,便会执行 .then() 里的回调。
js
const handleConfirm = () => {
resolveFn && resolveFn(info);
};
const handleCancel = () => {
rejectFn && rejectFn(new Error('用户已取消'));
};
当调用 getInfoByModal
方法时,显示模态框;等待用户点击提交或者取消按钮,当用户点击提交按钮后,Promise 的状态发生了改变,从而执行 .then() 里的回调,将数据上报到后端。
js
<template>
<button @click="handleClick">填写信息</button>
</template>
<script setup>
import { inject } from 'vue';
const getInfoByModal = inject('getInfoByModal');
const handleClick = async () => {
// 调用后将显示模态框,用户点击确认后会将promise改为fullfilled状态,从而拿到用户信息
getInfoByModal().then((res) => {
await api.submitInfo(res);
})
}
</script>
三、场景二
1. 场景回顾
项目里有一个 Page.vue 页面,项目初始化后加载的顺序是 App.vue -> Page.vue,在 App.vue 加载的时候,请求后端接口获取所需的数据,先来看下 pinia 中的代码:
js
import { defineStore } from 'pinia';
import { getUserList, getDepartList } from '/@/api/user.ts'
export const userStore = defineStore({
id: 'user',
state: () => ({
userList: [],
departList: [],
}),
actions: {
// 获取数据
async init() {
this.userList = await getUserList();
this.departList = await getDepartList();
}
}
})
然后在 App.vue 的 onMounted 中去执行 init 方法,接着在 Page.vue 初始化的时候,需要去使用这些数据去完成某些操作。
js
// App.vue
import { userStore } from '/@/store/modules/user';
import { onMounted } from 'vue';
const useUserStore = userStore();
onMounted(() => {
useUserStore.init();
})
// Page.vue
import { userStore } from '/@/store/modules/user';
import { onMounted } from 'vue';
const useUserStore = userStore();
onMounted(() => {
console.log(useUserStore.userList, useUserStore.departList)
})
正常情况下是这种逻辑是没问题的,但是当接口响应比较慢时,Page.vue 中就有可能拿不到数据,导致后面的逻辑出现 BUG,因此我们需要考虑到这种情况,作出相对应的处理。
2. 解决方案
比较好的解决方法是使用 Promise,而且需要从外部改变 promise 内部状态
先来封装一个函数,这函数返回两个东西
- readyResolve: 一个 resolve 函数
- onReady: 接收回调函数,只有在 readyResolve 执行后才会执行
主要逻辑:声明一个 promise,将其 resolve 保存到外部,再声明一个方法,里面执行该 promise 的 .then(),在 then() 里面可以自定义操作。
js
export const useOnReady = () => {
let readyResolve = null;
const readyPromise = new Promise(resolve => {
// 保存 resolve
readyResolve = resolve;
})
const onReady = (cb) => {
readyPromise.then(() => {
// resolve执行完才会走 then,然后再执行回调函数
cb();
})
}
return {
onReady,
readyResolve
}
}
接着回到 Pinia 文件中,在获取完数据后执行 readyResolve
,同时将 onReady
暴露出去:
js
import { defineStore } from 'pinia';
import { getUserList, getDepartList } from '/@/api/user.ts'
import { useOnReady } from '/@/hooks/useOnReady'
const { readyResolve, onReady } = useOnReady();
export const userStore = defineStore({
id: 'user',
state: () => ({
userList: [],
departList: [],
}),
actions: {
// 获取数据
async init() {
this.userList = await getUserList();
this.departList = await getDepartList();
// 请求完数据,执行readyResolve
readyResolve();
}
}
})
// 将onReady暴露出去
export const onUserStoreSetup = onReady;
在 Page.vue 页面中,只需要往 onUserStoreSetup 中传回调函数即可;当获取完数据,执行 readyResolve
,改变了 readyPromise 的状态,接着会执行 .then 中的回调函数,最终成功拿到数据!
js
// Page.vue
import { userStore, onUserStoreSetup } from '/@/store/modules/user';
import { onMounted } from 'vue';
const useUserStore = userStore();
onMounted(() => {
// 传入回调
onUserStoreSetup(() => {
console.log(useUserStore.userList, useUserStore.departList)
})
})