前端面试的最后一道题往往是手写题,题目不限于基础 api 实现,算法题,场景应用题等。今天和大家分享一下之前我面试某大厂时遇到的一道手写题: 使用 JS 简单实现一套 SWR 机制 。
很多同学可能都没听过什么是 SWR,更不用说用代码实现了。
SWR 这个名字来自于 stale-while-revalidate :一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略 。
与 max-age 类似,它是控制缓存的,是 Cache-Control 的一个指令,英文单词 stale 的意思是陈旧的,不新鲜的。在 HTTP 缓存领域,stale 用来形容一个缓存过期了。
普通的缓存策略是这样的:当一个资源的缓存过期之后,如果想再次使用它,需要先对该缓存进行 revalidate。在 revalidate 执行期间,客户端就得等待,直到 revalidate 请求结束。
在一些特别注重性能的场景下,这种传统的同步更新缓存的机制被认为是有性能问题的。
而这个 SWR 策略是说:当 revalidate 请求进行时,客户端可以不等待, 直接使用过期的缓存 ,revalidate 完了缓存就更新了,下次用的就是新的了。
所以 SWR 实现的功能用通俗的词语解释就是“ 后台缓存刷新 ”、“ 异步缓存更新 ”。
SWR 通常与 max-age 一起使用,比如 Cache-Control: max-age=1, stale-while-revalidate=59 表示:这个缓存在 1s 内是新鲜的,在 1-60s 内,虽然缓存过期了,但仍可以直接使用,同时进行异步 revalidate,在 60s 后,缓存完全过期需要进行传统的同步 revalidate。
SWR 的使用场景通常有:当前天气状况的 API,或者过去一小时内编写的头条新闻等。
了解了什么是 SWR 后,接下来看看如何实现它。
实现之前,先拆解下目标:
我们需要用一个“容器”来缓存请求回来的复杂数据,在 JS 中,我们很容易第一时间想到使用 Object 。
使用 Object 虽然没有什么问题,但它的结构是 “字符串—值” 的对应,只支持字符串作为键名。而在 ES6 中,Map 提供了 “值—值” 对应这种更完善的 Hash,更适合用于“键值对”这种数据结构。
我们在面试中,应该随时向面试官展现我们的知识储备,因此这里选择 Map 更好。
为了方便代码实现后,有一个比较好的对比。这里先写一下不使用缓存时数据请求方式:
const data = await fetcher();
为了让 fetcher 支持数据缓存的能力,这里需要对 fetcher 进行一层封装。
封装之前,先定义一下 需要被缓存的数据 ,那么什么数据需要被缓存呢?
很显然,不就是 请求返回的数据 吗。
但与此同时,你也应该想到, 如果重复调用函数,最好不要发送多次请求 。
所以缓存数据中应该有:
请求返回的数据
当前正在进行中的请求(如果有),避免多次请求
const cache = new Map(); // 缓存数据
async function swr(cacheKey, fetcher) {
// 首先从缓存中获取
let data = cache.get(cacheKey) || { value: null, promise: null };
// 写入缓存
cache.set(cacheKey, data);
// 没有数据且也没有在请求中,需要发送请求
if (!data.value && !data.promise) {
// 保存当前请求的 promise
data.promise = fetcher()
.then((val) => {
data.value = val; // 请求成功,将数据存起来
});
.catch((err) => {
console.log(err);
})
.finally(() => {
data.promise = null; // 请求完毕,不再保存 promise
});
}
// 没有数据,但正在请求中,复用保存的 promise
if (data.promise && !data.value) await data.promise;
// 返回数据
return data.value;
}
这样,我们就实现了数据缓存的能力。
在已有缓存能力的基础上,再支持 过期时间 cacheTime 就很容易了。
只需要在发起新的请求前,判断下是否过期:
const isStaled = Date.now() - 获取到数据的时间 > cacheTime
所以,在缓存数据中我们还需要保存 获取到数据的时间 :
const cache = new Map();
// 新增 cacheTime 参数
async function swr(cacheKey, fetcher, 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 = fetcher()
.then((val) => {
data.value = val;
data.time = Date.now(); // 保存获取到数据的时间
});
.catch((err) => {
console.log(err);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) await data.promise;
return data.value;
}
有了以上的封装,调用方法变更为:
// before
const data = await fetcher();
// after
const data = await swr('cache-key', fetcher, 3000);
首次调用时,会通过接口请求数据。随后调用会立即返回缓存数据。如果调用间隔超过 3s,将先返回缓存数据,再请求接口获取最新的数据。
大功告成!我们用近 20 行代码简单实现了一套 SWR 机制。
以上的代码只能是一个合格的水平,我们应该充分利用自己的技术来打动面试官,让他记住你。
目前的代码中,我们虽然使用了 Map,但使用时 cacheKey 还是一个字符串,没有真正发挥 Map 的作用。作为基础能力的补充,可以考虑将 function 作为 cacheKey 传入来实现 条件请求 特性。
将函数返回值作为 cacheKey,如果有返回,则执行上述逻辑,如果没有,则不缓存。
const shouldCache = function() { ... }
// cacheKey 支持传入函数
const data = await swr(shouldCache, fetcher, 3000);
async function swr(cacheKey, fetcher, cacheTime) {
// 如果是函数,则调用函数将返回值作为 cacheKey
const cKey = typof cacheKey === 'function' ? cacheKey() : cacheKey;
// 如果有 cacheKey 才启用缓存
if (cKey) {
let data = cache.get(cKey) || { value: null, time: 0, promise: null };
cache.set(cKey, data);
...
} else {
return await fetcher();
}
}
让我们来继续发挥 Map 的能力。
我们知道, Map 的遍历顺序就是插入顺序 ,再加上其键值对的数据结构,很容易想到基于此特性来实现 LRU 缓存淘汰策略。
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行 淘汰 数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
整个流程大致为:
新加入的数据插入到第一项
每当缓存命中(即缓存数据被访问),则将数据提升到第一项
当缓存数量满的时候,将最后一项的数据丢弃
由于面试时间有限,我不推荐大家在面试时继续写了,很容易弄巧成拙。但你可以积极地向面试官介绍这个思路和想法,继续加分,最好再补一句:“vue 的 keep-alive 组件中就用到了此算法”,间接地向面试官传递 你清楚 Vue 相关的原理实现 这个信息。
其实,LRU 算法通常会单独作为一道手写题,因此今天我们也手写巩固一下:
只需要对之前声明的 cache 容器 const cache = new Map(); 进行一些改造:
同时向外暴露的 get 和 set API 用法保持不变
class LRUCahe {
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 undefined;
}
set(key, value) {
if (this.cache.has(key)) {
// 存在即更新(删除后加入)
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// 不存在即加入
// 缓存超过最大值,则移除最近没有使用的,也就是 map 的第一个 key
// map.keys() 会返回 Iterator 对象
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
}
// before
const cache = new Map();
// after
const cache = new LRUCahe(50); // 缓存最大容量为 50
// 后续的 SWR 代码不做改动
使用 Map 实现 LRU 的时间复杂度为 O(1)
可见,一个小小的手写题还是隐藏了很多很深的知识点的。面试官考察的是你全方位的能力,如果你写出了以上的代码,并向面试官陈述你因为时间关系没来得及实现的后续特性,可以体现你多方面的能力:
理解 HTTP 相关缓存策略
理解 Object 与 Map 的差异与 Map 的使用场景
理解 Promise 与 async 函数,并能实际使用
写代码时考虑性能优化
掌握数据类型的判断方法
了解 Vue 相关原理实现
具有 API 抽象与封装能力
能严谨,全面地考虑问题
如果我是面试官,一定已经被惊艳到了。
来源:前端印象
浏览器缓存主要分为强强缓存(也称本地缓存)和协商缓存(也称弱缓存),强缓存是利用http头中的Expires和Cache-Control两个字段来控制的,用来表示资源的缓存时间。协商缓存就是由服务器来确定缓存资源是否可用.
一个缓存就是一个组件,它可以透明地存储数据,以便未来可以更快地服务于请求。缓存能够服务的请求越多,整体系统性能就提升得越多。
浏览器缓存就是把一个已经请求过的Web资源(如html页面,图片,js,数据等)拷贝一份副本储存在浏览器中,为什么使用缓存:减少网络带宽消耗,降低服务器压力,减少网络延迟,加快页面打开速度
一个H5页面在APP端,如果勾选已读状态,则下次打开该链接,会跳过此页面。用到了HTML5 的本地存储 API 中的 localStorage作为解决方案,回顾了下Web缓存的知识
在描述CDN的实现原理之前,让我们先看传统的未加缓存服务的访问过程,以便了解CDN缓存访问方式与未加缓存访问方式的差别,用户访问未使用CDN缓存网站的过程为:用户向浏览器提供要访问的域名;
页面打开时,由于缓存的存在,刚刚更新的数据有时无法在页面得到刷新,当这个页面作为模式窗口被打开时问题更为明显, 如何将缓存清掉?
通过在Response Header设置Cache-Control head 信息可以控制浏览器的缓存行为。我们先来看一下Cache-Control可以设置哪些值:缓存头Cache-Control只能在服务端设置,在客户端是由浏览器设置的,自己不能修改它的值。
增量更新是目前大部分团队采用的缓存更新方案,能让用户在无感知的情况获取最新内容。具体实现方式通常是(一般我们通过构建工具来实现,比如webpack):
浏览器会默认缓存网站的静态资源文件,如:js文件、css文件、图片等。缓存带来网站性能提升的同时也带来了一些困扰,最常见的问题就是不能及时更新静态资源,造成新版本发布时用户无法及时看到新版本的变化,严重影响了用户体验。
一个后台管理系统,一个列表页A路由配置需要缓存,另一个页面B里面有多个跳转到A路由的链接。问题描述:首先访问/A?id=1页面,然后到B页面再点击访问A?id=2的页面,发现由于页面A设置了缓存,数据还是id=1的数据,并没有更新。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!