跳到主要内容

SWR

2024年04月09日
柏拉文
越努力,越幸运

一、认识


SWR(stale-while-revalidate) 是一种由 HTTP RFC 5861[1] 推广的 HTTP 缓存失效策略。

  • 普通缓存策略: 当一个资源的缓存过期之后,如果想再次使用它,需要先对该缓存进行 revalidate。在 revalidate 执行期间,客户端就得等待,直到 revalidate 请求结束。在一些特别注重性能的场景下,这种传统的同步更新缓存的机制被认为是有性能问题的。

  • SWR 缓存策略: 当 revalidate 请求进行时,客户端可以不等待,直接使用过期的缓存,revalidate 完了缓存就更新了,下次用的就是新的了。

所以 SWR 实现的功能用通俗的词语解释就是后台缓存刷新异步缓存更新

二、特点


  1. 当请求数据时,首先从缓存中读取,并立即返回给调用者

  2. 如果数据已经过期,则发起 fetch 请求,获取最新数据

三、实现


3.1 支持数据缓存

思路: 通过 Map 来存储缓存的数据, 缓存的数据主要有: 请求返回的数据; 当前正在进行的请求(如果有), 避免多次缓存;

模拟请求

function request(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(timeout);
}, timeout);
});
}

function wrapperSWR() {
const cache = new Map();
return async function (cacheKey, request, timeout) {
let data = cache.get(cacheKey) || { value: null, promise: null };
cache.set(cacheKey, data);
if (!data.value && !data.promise) {
data.promise = request(timeout)
.then((res) => {
data.value = res;
})
.catch((res) => {
console.log(res);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) {
await data.promise;
}
return data.value;
};
}

const swr = wrapperSWR();

async function getData1(timeout) {
const result = await swr("getData1", request, timeout);
console.log("getData1", result);
}

async function getData2(timeout) {
const result = await swr("getData2", request, timeout);
console.log("getData2", result);
}

getData1(5000);
getData1(8000);
getData1(3000);
getData1(1000);

getData2(2000);
getData2(3000);
getData2(6000);
getData2(1000);

真实请求

function normalizeUrl(url, params) {
let queryString = Object.keys(params).reduce((prev, key) => {
return (prev += `${key}=${params[key]}&`);
}, "?");
return url + queryString.slice(0, queryString.length - 1);
}

function request(url, method, params, header, signal) {
const normalizeMethod = method.toUpperCase();
const normalizedUrl =
normalizeMethod === "GET" ? normalizeUrl(url, params) : url;
const normalizedOptions = {
method,
signal: signal,
body: normalizeMethod === "GET" ? null : JSON.stringify(params),
};
return fetch(normalizedUrl, {
method: normalizeMethod,
...normalizedOptions,
})
.then((res) => {
if (res.ok) {
return res.json();
}
return Promise.reject(res.statusText);
})
.catch((error) => {
if (typeof error === "object") {
return Promise.reject(`错误类型为: ${error.name}, 具体原因: ${error}`);
}
return Promise.reject(error);
});
}

function wrapperSWR() {
const cache = new Map();
return async function (cacheKey, request) {
let data = cache.get(cacheKey) || { value: null, promise: null };
cache.set(cacheKey, data);
if (!data.value && !data.promise) {
data.promise = request()
.then((res) => {
data.value = res;
})
.catch((res) => {
console.log(res);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) {
await data.promise;
}
return data.value;
};
}

const swr = wrapperSWR();

/**
* @description: API 管理
*/

function getDataServer1() {
return request("http://test.bolawen.com/server/sync", "post", { a: 1, b: 2 });
}

function getDataServer2() {
return request("http://test.bolawen.com/server/sync", "get", { a: 1, b: 2 });
}

/**
* @description: 各自组件调用 API 服务、处理数据
*/

async function getData1() {
const result = await swr("getData1", getDataServer1);
console.log("getData1", result);
}

async function getData2() {
const result = await swr("getData2", getDataServer2);
console.log("getData2", result);
}

getData1();
getData1();
getData1();
getData1();

getData2();
getData2();
getData2();
getData2();

3.2 支持缓存过期时间

思路: 在发起新的请求之前,判断下是否过期: Data.now() - 获取到数据的时间 > cacheTime

模拟请求

function request(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(timeout);
}, timeout);
});
}

function wrapperSWR() {
const cache = new Map();
return async function (cacheKey, request, cacheTime, timeout) {
let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
cache.set(cacheKey, data);

const isStaled = Date.now() - data.time > cacheTime;

if (isStaled && !data.promise) {
data.promise = request(timeout)
.then((res) => {
data.value = res;
data.time = Date.now();
})
.catch((res) => {
console.log(res);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) {
await data.promise;
}
return data.value;
};
}

const swr = wrapperSWR();

async function getData1(timeout) {
const result = await swr("getData1", request, 3000, timeout);
console.log("getData1", result);
}

async function getData2(timeout) {
const result = await swr("getData2", request, 4000, timeout);
console.log("getData2", result);
}

getData1(5000);
getData1(8000);
getData1(3000);
getData1(1000);

getData2(2000);
getData2(3000);
getData2(6000);
getData2(1000);

真实请求

function normalizeUrl(url, params) {
let queryString = Object.keys(params).reduce((prev, key) => {
return (prev += `${key}=${params[key]}&`);
}, "?");
return url + queryString.slice(0, queryString.length - 1);
}

function request(url, method, params, header, signal) {
const normalizeMethod = method.toUpperCase();
const normalizedUrl =
normalizeMethod === "GET" ? normalizeUrl(url, params) : url;
const normalizedOptions = {
method,
signal: signal,
body: normalizeMethod === "GET" ? null : JSON.stringify(params),
};
return fetch(normalizedUrl, {
method: normalizeMethod,
...normalizedOptions,
})
.then((res) => {
if (res.ok) {
return res.json();
}
return Promise.reject(res.statusText);
})
.catch((error) => {
if (typeof error === "object") {
return Promise.reject(`错误类型为: ${error.name}, 具体原因: ${error}`);
}
return Promise.reject(error);
});
}

function wrapperSWR() {
const cache = new Map();
return async function (cacheKey, request, cacheTime) {
let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
cache.set(cacheKey, data);

const isStaled = Date.now() - data.time > cacheTime;
if (isStaled && !data.promise) {
data.promise = request()
.then((res) => {
data.value = res;
data.time = Date.now();
})
.catch((res) => {
console.log(res);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) {
await data.promise;
}
return data.value;
};
}

const swr = wrapperSWR();

/**
* @description: API 管理
*/

function getDataServer1() {
return request("http://test.bolawen.com/server/sync", "post", { a: 1, b: 2 });
}

function getDataServer2() {
return request("http://test.bolawen.com/server/sync", "get", { a: 1, b: 2 });
}

/**
* @description: 各自组件调用 API 服务、处理数据
*/

async function getData1() {
const result = await swr("getData1", getDataServer1, 3000);
console.log("getData1", result);
}

async function getData2() {
const result = await swr("getData2", getDataServer2, 3000);
console.log("getData2", result);
}

getData1();
getData1();
getData1();
getData1();

getData2();
getData2();
getData2();
getData2();

3.3 支持函数条件缓存

函数条件缓存 可以考虑将 function 作为 cacheKey 传入来实现条件请求特性。将函数返回值作为 cacheKey,如果有返回,则执行上述逻辑,如果没有,则不缓存。

模拟请求

function request(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(timeout);
}, timeout);
});
}

function wrapperSWR() {
const cache = new Map();
return async function (cacheKey, request, cacheTime, timeout) {
const mergedCacheKey =
typeof cacheKey === "function" ? cacheKey() : cacheKey;

if (!mergedCacheKey) {
return await request(timeout);
}
let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
cache.set(cacheKey, data);

const isStaled = Date.now() - data.time > cacheTime;

if (isStaled && !data.promise) {
data.promise = request(timeout)
.then((res) => {
data.value = res;
data.time = Date.now();
})
.catch((res) => {
console.log(res);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) {
await data.promise;
}
return data.value;
};
}

const swr = wrapperSWR();

async function getData1(timeout) {
const result = await swr(
() => {
return false;
},
request,
3000,
timeout
);
console.log("getData1", result);
}

async function getData2(timeout) {
const result = await swr("getData2", request, 4000, timeout);
console.log("getData2", result);
}

getData1(5000);
getData1(8000);
getData1(3000);
getData1(1000);

getData2(2000);
getData2(3000);
getData2(6000);
getData2(1000);

真实请求

function normalizeUrl(url, params) {
let queryString = Object.keys(params).reduce((prev, key) => {
return (prev += `${key}=${params[key]}&`);
}, "?");
return url + queryString.slice(0, queryString.length - 1);
}

function request(url, method, params, header, signal) {
const normalizeMethod = method.toUpperCase();
const normalizedUrl =
normalizeMethod === "GET" ? normalizeUrl(url, params) : url;
const normalizedOptions = {
method,
signal: signal,
body: normalizeMethod === "GET" ? null : JSON.stringify(params),
};
return fetch(normalizedUrl, {
method: normalizeMethod,
...normalizedOptions,
})
.then((res) => {
if (res.ok) {
return res.json();
}
return Promise.reject(res.statusText);
})
.catch((error) => {
if (typeof error === "object") {
return Promise.reject(`错误类型为: ${error.name}, 具体原因: ${error}`);
}
return Promise.reject(error);
});
}

function wrapperSWR() {
const cache = new Map();
return async function (cacheKey, request, cacheTime) {
const mergedCacheKey =
typeof cacheKey === "function" ? cacheKey() : cacheKey;

if (!mergedCacheKey) {
return await request();
}

let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
cache.set(cacheKey, data);

const isStaled = Date.now() - data.time > cacheTime;
if (isStaled && !data.promise) {
data.promise = request()
.then((res) => {
data.value = res;
data.time = Date.now();
})
.catch((res) => {
console.log(res);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) {
await data.promise;
}
return data.value;
};
}

const swr = wrapperSWR();

/**
* @description: API 管理
*/

function getDataServer1() {
return request("http://test.bolawen.com/server/sync", "post", { a: 1, b: 2 });
}

function getDataServer2() {
return request("http://test.bolawen.com/server/sync", "get", { a: 1, b: 2 });
}

/**
* @description: 各自组件调用 API 服务、处理数据
*/

async function getData1() {
const result = await swr(
() => {
return false;
},
getDataServer1,
3000
);
console.log("getData1", result);
}

async function getData2() {
const result = await swr("getData2", getDataServer2, 3000);
console.log("getData2", result);
}

getData1();
getData1();
getData1();
getData1();

getData2();
getData2();
getData2();
getData2();

3.4 支持 LRU 缓存淘汰

LRU 缓存淘汰 根据数据的历史访问记录来进行淘汰数据,其核心思想是如果数据最近被访问过,那么将来被访问的几率也更高。整个流程大致为:

  1. 新加入的数据插入到第一项

  2. 每当缓存命中(即缓存数据被访问),则将数据提升到第一项

  3. 当缓存数量满的时候,将最后一项的数据丢弃

模拟请求

function request(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(timeout);
}, timeout);
});
}

class LRUCache {
constructor(capacity) {
this.cache = new Map();
this.capacity = capacity;
}

get(key) {
if (this.cache.has(key)) {
const temp = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, temp);
return temp;
}
return null;
}

set(key, value) {
if (this.cache.has(key)) {.
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
}

function wrapperSWR() {
const cache = new LRUCache(10);
return async function (cacheKey, request, cacheTime, timeout) {
const mergedCacheKey =
typeof cacheKey === "function" ? cacheKey() : cacheKey;

if (!mergedCacheKey) {
return await request(timeout);
}
let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
cache.set(cacheKey, data);

const isStaled = Date.now() - data.time > cacheTime;

if (isStaled && !data.promise) {
data.promise = request(timeout)
.then((res) => {
data.value = res;
data.time = Date.now();
})
.catch((res) => {
console.log(res);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) {
await data.promise;
}
return data.value;
};
}

const swr = wrapperSWR();

async function getData1(timeout) {
const result = await swr("getData1", request, 3000, timeout);
console.log("getData1", result);
}

async function getData2(timeout) {
const result = await swr("getData2", request, 4000, timeout);
console.log("getData2", result);
}

getData1(5000);
getData1(8000);
getData1(3000);
getData1(1000);

getData2(2000);
getData2(3000);
getData2(6000);
getData2(1000);

真实请求

function normalizeUrl(url, params) {
let queryString = Object.keys(params).reduce((prev, key) => {
return (prev += `${key}=${params[key]}&`);
}, "?");
return url + queryString.slice(0, queryString.length - 1);
}

function request(url, method, params, header, signal) {
const normalizeMethod = method.toUpperCase();
const normalizedUrl =
normalizeMethod === "GET" ? normalizeUrl(url, params) : url;
const normalizedOptions = {
method,
signal: signal,
body: normalizeMethod === "GET" ? null : JSON.stringify(params),
};
return fetch(normalizedUrl, {
method: normalizeMethod,
...normalizedOptions,
})
.then((res) => {
if (res.ok) {
return res.json();
}
return Promise.reject(res.statusText);
})
.catch((error) => {
if (typeof error === "object") {
return Promise.reject(`错误类型为: ${error.name}, 具体原因: ${error}`);
}
return Promise.reject(error);
});
}

class LRUCache {
constructor(capacity) {
this.cache = new Map();
this.capacity = capacity;
}

get(key) {
if (this.cache.has(key)) {
const temp = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, temp);
return temp;
}
return null;
}

set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
}

function wrapperSWR() {
const cache = new LRUCache();
return async function (cacheKey, request, cacheTime) {
const mergedCacheKey =
typeof cacheKey === "function" ? cacheKey() : cacheKey;

if (!mergedCacheKey) {
return await request();
}

let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
cache.set(cacheKey, data);

const isStaled = Date.now() - data.time > cacheTime;
if (isStaled && !data.promise) {
data.promise = request()
.then((res) => {
data.value = res;
data.time = Date.now();
})
.catch((res) => {
console.log(res);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) {
await data.promise;
}
return data.value;
};
}

const swr = wrapperSWR();

/**
* @description: API 管理
*/

function getDataServer1() {
return request("http://test.bolawen.com/server/sync", "post", { a: 1, b: 2 });
}

function getDataServer2() {
return request("http://test.bolawen.com/server/sync", "get", { a: 1, b: 2 });
}

/**
* @description: 各自组件调用 API 服务、处理数据
*/

async function getData1() {
const result = await swr("getData1", getDataServer1, 3000);
console.log("getData1", result);
}

async function getData2() {
const result = await swr("getData2", getDataServer2, 3000);
console.log("getData2", result);
}

getData1();
getData1();
getData1();
getData1();

getData2();
getData2();
getData2();
getData2();