promise的异步合并(promiseLock/promiseConcat)

为什么要用promiseLock

1、组件化背景

前端组件化趋势越来越强,不管是vue、还是react又或者是其它的一些前端框架、甚至原生都在走组件化这条路。于是在组件化设计背景下就会存在一个设计问题:"解耦",这里只谈数据层面的解耦,一个组件内的数据由组件去发起请求获取,没问题吧;那如果一个页面上同时用到多个重复的组件呢?这就会导致每个组件都会发起一个重复的请求~

2、模块化

不止组件,其实很多时候我们会把前端一些通用的逻辑或者可复用的逻辑单独封装到一个模块内,然后供其它业务去调用,不可避免的会出现同一时间同时调用多次的情况;

以上两种场景都会出现重复请求的情况,当然对于业务来说没有影响,但是会涉及到一个问题:性能

明知道这里的重复请求,我们就不能避免了吗?比如在公共的顶层去缓存数据、或者在组件间共享数据。。。当然可以解决,但这又导致了一个问题:"耦合",比如vuex缓存数据,还会导致一个问题,数据更新问题。这时候promiseLock的设计就出现了,promiseLock实现很简单,就是预先创建一个闭包用于缓存promise,在promise完成后销毁,promise未结束前的请求全部指向一开始创建的promise,当然会有key参数去区分不同请求;这样就做到了一个请求周期的缓存,在上一个请求为完成前不会创建新的请求。这样就可以很好的解决同一时间点并发的相同请求,并且把请求优化的逻辑和组件间"解耦"了。

为什么要用promiseConcat

promiseConcat和promiseLock是极其相似的,包括使用场景、用法;

1、组件

思考一个问题:promiseLock在组件中解决了重复的请求问题,是不是可以进一步优化"同类型"的请求;比如云图标组件,一个页面上会使用比较多的图标情况下,每个图标组件内会单独获取图标资源,是不是可以合并这个获取操作以节省http请求次数;

2、列表数据

在批量获取列表内关联的表单时,是不是可以把获取单个表单的请求改用批量获取表单的请求,再逐一分发到当个获取动作里;

promiseLock使用示例

1、列表重复数据

即使在一个方法内也会出现重复请求的问题,比如获取一个数据列表内关联的表单详情逻辑,如果不做任何处理的话大概是循环再发起请求,发起的请求数是N,一般情况下的话可能的做法是:创建一个缓存map用于记录以获取的form,同时阻塞循环:

javascript 复制代码
const formMap = {};
for (let i = 0; i < data.length; i++) {
    const item = data[i];
    if (!formMap[item.formId]) {
        formMap[item.formId] = await getForm(item.formId);
    }
    item.form = formMap[item.formId];
}

这个例子业务逻辑上没问题,但是有两个问题值得考虑的,1、阻塞导致的延迟较高;2、多维护了一个formMap;

那这个场景用promiseLock后是怎样的呢:

javascript 复制代码
const getFormLock = promiseLock(getForm, formId => formId);
Promise.all(data.map(item => getFormLock(item.formId).then(form => item.form = form)));

可以看出,使用了promiseLock后的代码对比传统的方式代码简洁了许多,并且没有多余的变量维护,同时又实现了请求并发并优化了重复请求。

2、全局弹窗

对于登录失效后我们会有一个全局的跨企业数据访问异常弹窗,但是异常是在接口抛出跨企业数据访问异常后调用的,但是一个页面上会有非常多的接口是同时触发的,如果不做限制的话会弹出多个弹窗。这种情况下,传统的解决方案可能是在闭包里面单独维护一个弹窗开关变量用于判断当前是否已经打开弹窗了:

javascript 复制代码
let modalInstance = null;
export default function createTenantErrorModal() {
    if (modalInstance) return modalInstance;
    modalInstance = TenantError.create();
    return modalInstance;
}

当然,这个方案业务上也没什么毛病,代码也比较简洁了,但还是说我们能不能再挑点毛病,比如说我不想再写一个闭包,当然可以!直接在TenantError上改造就行:

javascript 复制代码
export default class TenantError extends Vue {
  static create = promiseLock(createTenantChangeModal, () => 'tenantError');
}

// 或者是
export default const createTenantErrorModal = promiseLock(TenantError.create, () => 'tenantError');

3、守卫

比较常见的例子是接口签名逻辑,思考一下,如果我想做一个token过期的时候去自动续期的逻辑,应该怎么去实现;正常思维,在封装的请求方法里面做一个拦截:

javascript 复制代码
export class BaseRequest {
    expiredTime = 0;
    token = '';
    isRefreshTokenNow = null;
    function refreshToken() {
        return httpRefreshToken().then(t => {
            token = t;
            // 两个小时有效期
            expiredTime = Date.now() + 2 * 60 60 1000;
        })
    }
    return async function request(url) {
        if (Date.now() > expiredTime || !token) {
            isRefreshTokenNow ? (await isRefreshTokenNow) : (await refreshToken());
        }
        const headers = {
            'X-Access-Token': token;
        };
        return fetch(url, { headers });
    }
}
export default BaseRequest();

这里如果使用promiseLock的话会简洁很多:

javascript 复制代码
export class BaseRequest {
    expiredTime = 0;
    token = '';
    cosnt refreshToken = promiseLock(httpRefreshToken().then(t => {
         token = t;
         // 两个小时有效期
         expiredTime = Date.now() + 2 * 60 60 1000;
    }), () => 'refreshToken')
    return async function request(url) {
        if (Date.now() > expiredTime || !token) {
            await refreshToken();
        }
        const headers = {
            'X-Access-Token': token;
        };
        return fetch(url, { headers });
    }
}
export default BaseRequest();

promiseConcat使用示例

1、图片组件、图标组件

这是目前最常用的例子,图片组件会在加载的时候去根据oss地址去获取带有签名的地址

javascript 复制代码
const getAuthLock = promiseConcat(paths => getAuth(paths));

@Components
export default class OssImage expends Vue {
    @Prop() ossPath;
    url = '';
    loading = true;
    async getAbsolutePath() {
        this.url = await getAuthLock(this.ossPath);
        this.loading = false;
    }
    created() {
        this.getAbsolutePath()
    }
}

2、列表数据

在promiseLock使用场景中已经优化了相同数据的获取逻辑,但是很多情况下列表中可能大部分是不同数据的,这样的话就可以用promiseConcat进一步优化

javascript 复制代码
const getFormConcat = promiseConcat(getForms);
Promise.all(data.map(item => getFormConcat(item.formId).then(form => item.form = form)));

promiseLock原理

1、这里实现一个简易版

javascript 复制代码
function promiseLock(fn, keyFn) {
    const promiseMap = {};
    return function(...args) {
        const key = keyFn(...args);
        if (promiseMap[key]) return promiseMap[key];
        const promise = fn(...args);
        promiseMap[key] = promise;
        return promise.finally(() => delete promiseMap[key]);
    }
}

2、方法流程图

promiseConcat原理

1、实现一个简易版的方法

promiseConcat里面实际上包含了promiseLock的逻辑,内部直接可以使用promiseLock方法;promiseConcat会有一个收集时间段,默认是nextFrame,也就是下一帧,实际上可以定义这个时间段,这里就不做实现了;

javascript 复制代码
import { nextFrame } from '@triascloud/utils';

function promiseConcat(concatFn) {
    let args = [];
    const lockedDurationFn = promiseLock(() => nextFrame().then(() => promiseLock(args => concatFn(args), () => 'lockFn')), () => 'duration');
    return function(arg) {
        args.push(arg);
        const index = args.length - 1;
        return lockedDurationFn().then(lockFn => lockFn(args.splice(0))).then(result => result[index]);
    }
}

2、方法实现流程图


至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。

相关推荐
王解42 分钟前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录43 分钟前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑2136 小时前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程