背景
最近在做React Native项目时,遇到一个异步调用问题。接口请求时需要传入明文 id,但是应用从其它地方同步过来的 id 是加密的,所以在调用接口前需要再调一次解密请求,以获取到明文 id。
问题是,页面初始化后,可能会同时发出多个接口请求。代码逻辑如下:
// 解密id
const decryptId = () => fetch("url").then(response => response.json());
// 接口请求1
const request1 = async () => {
let decryptedId = await decryptId();
// ...
};
// 接口请求2
const request2 = async () => {
let decryptedId = await decryptId();
// ...
};
// 页面初始化后
request1();
request2();
结果是decryptId中的异步请求执行了两次,造成了不必要的网络请求开销。
思路
既然两次调用都是执行相同的操作,为什么不合二为一只做一次解密请求呢?但是如何把两次请求合成一次呢?理想的情况是:当第一次发起了解密请求后,再想发起第二次请求时,因为知道前面已经发起过一次解密请求,所以就不再发起请求,两次解密调用都使用同一个解密请求。
想好了就要验证一下:
let decryptIdPromise = null;
// 解密id
const decryptId = () => {
if (!decryptIdPromise) {
decryptIdPromise = fetch("url").then(response => {
decryptIdPromise = null;
return response.json();
});
}
return decryptIdPromise;
};
// 接口请求1
const request1 = async () => {
let decryptedId = await decryptId();
// ...
};
// 接口请求2
const request2 = async () => {
let decryptedId = await decryptId();
// ...
};
// 页面初始化后
request1();
request2();
上面代码中,我使用了一个变量decryptIdPromise来保存异步请求,接口请求request1调用解密方法decryptId时,异步请求变量decryptIdPromise还是null,此时发起解密请求,并将解密异步请求对象赋值给变量decryptIdPromise。等接口请求request2调用解密方法时,变量decryptIdPromise已不为空,解密方法直接将该变量返回,这样两个接口请求方法消费的就是同一个解密请求了。
结果自然如预期的那样,只执行了一次解密请求。
发散思维思考一下如果把解密后的 id 缓存下来会怎样?答案是,下次再解密 id 时就不用发起网络请求了,直接返回缓存的 id。
改造后的解密 id 方法如下:
let decryptedId = "",
decryptIdPromise = null;
// 解密id
const decryptId = () => {
if (decryptedId) {
return decryptedId;
}
if (!decryptIdPromise) {
decryptIdPromise = fetch("url").then(response => {
decryptIdPromise = null;
decryptedId = response.json();
return decryptedId;
});
}
return decryptIdPromise;
};
管道方法
项目中还有类似的场景,那么可以把对异步操作的处理逻辑提取出来,封装成一个功能方法。
对解密方法的两次同时调用,得到的都是同一个异步请求对象。形象一点比喻的话,像是在解密请求上架设了一根管道,两次请求连接的都是这条管道。所以,我把这个功能方法也称之为管道方法。
管道方法代码:
const pipeline = (targetFunction, shouldCacheResult = false) => {
let promiseResult = null,
startedPromise = null;
return () => {
if (shouldCacheResult && promiseResult) {
return Promise.resolve(promiseResult);
}
if (!startedPromise) {
startedPromise = new Promise((resolve, reject) => {
targetFunction()
.then(result => {
shouldCacheResult && result && (promiseResult = result);
resolve(result);
})
.catch(error => reject(error))
.finally(() => {
startedPromise = null;
});
});
}
return startedPromise;
};
};
最终上面的解密方法可以改成如下方式:
const decryptId = pipeline(
() => fetch("url").then(response => response.json()),
true
);
TypeScript版管道方法:
const pipeline = <T>(
targetFunction: () => Promise<T>,
shouldCacheResult: boolean = false
) => {
let promiseResult: T, startedPromise: Promise<T> | undefined;
return (): Promise<T> => {
if (shouldCacheResult && promiseResult) {
return Promise.resolve(promiseResult);
}
if (!startedPromise) {
startedPromise = new Promise((resolve, reject) => {
targetFunction()
.then(result => {
shouldCacheResult && result && (promiseResult = result);
resolve(result);
})
.catch(error => reject(error))
.finally(() => {
startedPromise = undefined;
});
});
}
return startedPromise;
};
};
细心的朋友,是不是发现还没有考虑参数的问题。我觉得,参数意味着可变,多次调用时,参数若不同,结果也就可能不一样。而此时的管道方法也就无意义,所以,不考虑有参数的问题。
但不是说有参数的话,就不能使用这个管道方法了。可以把会被同时调用且传入的相同参数场景,使用管道方法封装成一个单独的无参方法。
const getData = async (dateFrom, dateTo) => {};
const getYear2018Data = pipeline(async () => {
let data = await getData("20180101", "20181231");
}, true);
const caller1 = async () => {
let data = await getYear2018Data();
// ...
};
const caller2 = async () => {
let data = await getYear2018Data();
// ...
};
caller1();
caller2();