技术干货(一)—— 怎么优雅合并相同异步请求

1,426 阅读4分钟

我正在参加「掘金·启航计划」

引言

异步问题是一个十分热门的问题,同时异步的处理也是十分复杂的问题。无论在前端还是后端,我们都绕不开异步这座大山。因此对异步的熟练掌握和灵活运用可以让我们的开发水平和理解能力如步青云。这一节我们简单的设计一个相同异步请求并发合并吧。

问题场景

  1. 为什么会有相同异步请求合并的请求?

相同异步请求合并的的场景日常经常遇到.比如在小程序开发中,某些接口需要小程序 wx.login 后才能执行. 进入小程序页面,为了更快的显示内容. 我们可能获取用户个人信息, 获取用户关注列表,获取用户未读信息三个接口同时请求,如:

// 简化代码, 这样login 会被调用3次
async function login() {
  // 异步请求wx.login()
}

async function getUserInfo() {
  await login();
  // 异步请求
}
async function getUserFollowList() {
  await login();
  // 异步请求
}
async function getUserNew() {
  await login();
  // 异步请求
}
function main() {
  getUserInfo();
  getUserFollowList();
  getUserNew();
}

如上, login函数被同时调用了三次,虽然我们可以用一些方法限制,但是很不优雅.

async function login() {
  // 异步请求
}
const loginPromise = login();
async function getUserInfo() {
  await loginPromise;
  // 异步请求
}
async function getUserFollowList() {
  await loginPromise;
  // 异步请求
}
async function getUserNew() {
  await loginPromise;
  // 异步请求
}
function main() {
  getUserInfo();
  getUserFollowList();
  getUserNew();
}


除此之外,如一些页面用过userId去用获取个人信息接口获取大量重复的人员信息,也会很多重复的请求被发送出去,造成大量的性能和带宽的浪费.(不要问为啥后台不写一个通过userId列表获取用户信息的接口,问就是后台没时间)

解决方案

实现合并相同异步请求,减少性能和带宽的浪费,需要注意以下几点:

  • 请求是异步
  • 缓存异步请求的返回值
  • 在请求未返回前,重复请求需要返回统一值

实现过程如代码所示:

interface GetAsyncConfig<T, K> {
    asyncGetter?: (key: K) => T;
    getter: (key: K) => T;
    setter: (key: K, data: T) => any;
}
export const getAsync = (() => {
    let caches: any = {};
    async function getAsync<T = any, K = string>(
        key: K,
        handles: GetAsyncConfig<T, K>,
        config: {
            tryMaxTime?: number; // 失败最大尝试次数
            forceAsync?: boolean; // 强制异步
            isSuccess?: (key: K, data: T) => any; // 判断成功的标准
        } = {},
    ): Promise<T> {
        config = {
            tryMaxTime: 1,
            forceAsync: false,
            isSuccess: () => true,
            ...config,
        };

        let temp = handles.getter(key);
        if (temp && config.isSuccess!(key, temp) && !config.forceAsync) {
            return temp;
        } else {
            caches[key] ||
                (caches[key] = {
                    loadingTime: 1, // 请求次数
                    isLoading: false,
                    getter: handles.asyncGetter,
                    resultCallBack: [],
                });
            let cache = caches[key];
            async function asyncGetData() {
                let result,
                    canUseGetterResult = true;
                try {
                    if (typeof handles.asyncGetter == 'function') {
                        result = await handles.asyncGetter(key);
                    } else {
                        result = handles.asyncGetter;
                    }
                } catch (e) {
                    canUseGetterResult = false;
                } finally {
                    cache.loadingTime++;
                }
                if (canUseGetterResult) {
                    handles.setter(key, result);
                }
                // 如果数据获取到 并且可用  或者  超过最大尝试次数
                if ((canUseGetterResult && config.isSuccess!(key, result)) || cache.loadingTime > config.tryMaxTime!) {
                    return { result, canUseGetterResult };
                } else {
                    return asyncGetData();
                }
            }

            if (cache.isLoading) {
                return new Promise((resolve, reject) => {
                    cache.resultCallBack.push([resolve, reject]);
                });
            } else {
                cache.isLoading = true;
                let result = await asyncGetData();
                cache.isLoading = false;
                while (cache.resultCallBack.length) {
                    let handle = cache.resultCallBack.shift();
                    if (result.canUseGetterResult) {
                        handle[0](result.result);
                    } else {
                        handle[1](result.result);
                    }
                }
                if (result.canUseGetterResult) {
                    return result.result;
                } else {
                    throw result.result;
                }
            }
        }
    }
    return getAsync;
})();

代码使用和讲解

利用闭包原理, getAsync是闭包里面生成的一个函数.

  1. 第一个参数 key 为区分请求的唯一ID. 通过此参数判断是否是相同的请求.
  2. 第二个参数为: { asyncGetter?: (key: K) => T; getter: (key: K) => T; setter: (key: K, data: T) => any; }. asyncGetter 为异步获取函数. getter 为同步获取函数. setter 为同步设置函数. 获取时,函数会先检测 getter 是否可以返回有效的值,如果返回则使用返回值. 如果无,则使用 asyncGetter 异步请求返回值,并在请求值返回以后使用调动 setter.
  3. 第三个参数为配置项 { tryMaxTime?: number; // 失败最大尝试次数 forceAsync?: boolean; // 强制异步 isSuccess?: (key: K, data: T) => any; // 判断成功的标准 }

代码逻辑

函数利用闭包, caches 保存请求的缓存. getAsync 被调用的时候. 会检测缓存中是否有值,如果有则返回缓存即可.如过没有,则包装一个Promsie后返回.

函数实际使用

getAsync('userToken', {
    getter() {
        return GlobalGet('userToken');
    },
    setter(_, token: string) {
        GlobalSet('userToken', token);
    },
    async asyncGetter(): Promise<string> {
        // ajax
        return api.getToken();
    },
})

尾言

技术来源于项目,项目需求推动技术的进步.我们在工作学习中,要时刻保持一颗学习的心. 遇到困难,要善于总结和归纳, 记录每一次困难的解决思路,在成长的道路上勇往直前!