Promise的高阶用法🚀从外部改变promise内部状态

一、前言

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)  
    })
})

四、参考文章

晚上12点,帮组员解决Vue3异步引发的线上BUG

整会promise这8个高级用法,再被问倒来喷我

相关推荐
涵信9 分钟前
第九节:性能优化高频题-首屏加载优化策略
前端·vue.js·性能优化
前端小巷子21 分钟前
CSS单位完全指南
前端·css
SunTecTec1 小时前
Flink Docker Application Mode 命令解析 - 修改命令以启用 Web UI
大数据·前端·docker·flink
拉不动的猪2 小时前
前端常见数组分析
前端·javascript·面试
小吕学编程2 小时前
ES练习册
java·前端·elasticsearch
Asthenia04122 小时前
Netty编解码器详解与实战
前端
袁煦丞3 小时前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛4 小时前
vue组件间通信
前端·javascript·vue.js
一笑code4 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员4 小时前
layui时间范围
前端·javascript·layui