「万变不离其宗」10个高频场景题助力业务开发 🚀🚀🚀
前言
本文列举了 10 个日常开发中遇到的业务需求,通过抽出其基本模板、解决思路及应用场景,来帮助大家理解特定场景下的解决方案。
1. LRU 算法
题目
LRU
算法(least recently usd),即 最近最少使用页面置换算法 。Vue 中 keep-alive
组件使用的就是 LRU
算法进行缓存。
实现方式
Map 实现
class LRUCache {
constructor(capacity = 10) {
this.cache = new Map();
this.capacity = capacity;
}
put(key, value) {
// 已经存在的情况下,更新其位置到”最新“即可
// 先删除,后插入
if(this.cache.has(key)) {
this.cache.delete(key);
} else {
// 插入数据前先判断,size是否符合capacity
// 已经>=capacity,需要把最开始插入的数据删除掉
// keys()方法得到一个可遍历对象,执行next()拿到一个形如{ value: 'xxx', done: false }的对象
if(this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value);
}
}
this.cache.set(key, value);
}
get(key) {
if(this.cache.has(key)) {
const value = this.cache.get(key);
// 更新位置
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return -1;
}
}
const lRUCache = new LRUCache(2)
console.log(lRUCache.put(1, 1)) // 缓存是 {1=1}
console.log(lRUCache.put(2, 2)) // 缓存是 {1=1, 2=2}
console.log(lRUCache.get(1)) // 返回 1
console.log(lRUCache.put(3, 3)) // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
console.log(lRUCache.get(2)) // 返回 -1 (未找到)
console.log(lRUCache.put(4, 4)) // 该操作会使得关键字 1 作废,缓存是 {3=3, 4=4}
console.log(lRUCache.get(1) ) // 返回 -1 (未找到)
console.log(lRUCache.get(3)) // 返回 3
console.log(lRUCache.get(4) ) // 返回 4
使用 Map
作为容器的一个好处就是,它能够 记住键的原始插入顺序 。这样我们就不用 额外 维护一个数据结构来记录使用频率,因为从 Map
中元素的顺序就可以很清楚的知道了。
对象 + 数组
const removeKeys = (arr, key) => {
const index = arr.indexOf(key);
index > -1 && arr.splice(index, 1);
}
class LRUCache2 {
constructor(capacity) {
this.capacity = capacity;
this.cache = {};
this.keys = []
}
put(key, value) {
if(this.cache[key]) {
// 存在的时候更新值
this.cache[key] = value;
// 更新位置
removeKeys(this.keys, key);
this.keys.push(key);
} else {
this.keys.push(key);
this.cache[key] = value;
if(this.keys.length > this.capacity) {
const delKey = this.keys[0];
this.cache[delKey] = null;
removeKeys(this.keys, delKey);
}
}
}
get(key) {
if(this.cache[key]) {
// 更新新鲜度
removeKeys(this.keys, key);
this.keys.push(key);
return this.cache[key];
}
return -1;
}
}
对象是用来记录存储的值,而数组的作用是为了知道谁是最久没使用的,它是按使用时间来排列的。
业务场景
在我们开发登录页时,可能需要 记住账号 ,并且 账号个数 也要做限制。拿 QQ 登录来说,每次登录时勾选“记住账号”,下次打开登录页时就会有历史账号下拉框供我们选择。如果登录另一个账号且记住密码,每次登录就有两个可选账号了。这不是重点,不知道大家有没有注意,最新登录的 QQ 都会“置顶”,也就是更新为第一个,方便我们下次登录。除此之外,当登录的账号超出限制个数时,被删除的账号也是最久没被使用的那个(这个场景中就是最后一个账号了)。再想想我们的 LRU 算法,是不是茅塞顿开?
2. 间隔时间调用 n 次回调
题目
根据传入的 间隔时间 以及 调用次数 来调用指定的回调。
实现方式
const repeat = (fn, interval, times) => {
if (times <= 0) return;
setTimeout(() => {
fn();
repeat(fn, interval, times - 1);
}, interval * 1000);
}
const repeatPrint = () => console.log(new Date().toLocaleString());
const r = repeat(repeatPrint, 3, 3)
这是一个递归函数了,递归的条件为 剩余调用次数 time 等于 0 时。而下一次递归的开始时间必须在前一次回调调用后才被允许。
业务场景
这个逻辑其实有点像 定时任务 。以前实习的时候,在公司做的是 云函数 ,需要每天固定时间去调用一个接口,来更新传感器返回的湿度、温度等信息,这个平台的云函数它自带触发器,可以选择我们任意时间点去调用该函数。回顾下我们上面的逻辑,两者是不是很像呢?
除此之外,相信很多小伙伴都有用过 番茄时钟 吧,我们可以制定一个计划,包括计划的持续时间,计划的待完成次数,当计划的待完成次数为 0 时,就圆满完成任务啦。
3. 合并时间
题目
实现一个函数,判断一组数字是否连续。当出现连续数字的时候以‘-’输出
const arr = [2, 3, 4, 7, 8, 9, 10, 13, 15]
期望结果:["2-4", "7-10", 13, 15]
实现方式
const merge = (arr) => {
const lens = arr.length;
if (lens == 1) return [arr[0]];
const result = []
let prev = 0, next = 1;
while (prev < lens) {
const diffIndex = next - prev;
if (arr[prev] + diffIndex === arr[next]) { // 连续
next++;
} else { // 不连续
if (diffIndex === 1) { // 单个
result.push(arr[prev]);
} else {
result.push(`${arr[prev]}-${arr[next - 1]}`)
}
prev = next;
next++;
}
}
return result;
}
const arr = [2, 3, 4, 7, 8, 9, 10, 13, 15]
console.log(merge(arr))
业务场景
国庆期间想必酒店预定房间都是个难事儿,相信大家都有碰到过房间订满或者是想要订的日期已经被别人订了的情况吧。当大家在选择日期的时候,有没有思考过该如何去实现这个需求呢?前端接收到接口返回的已预约数据,通过合并已预约日期,随后挑拣出可选择日期范围,最后呈现给用户,这个时候就要用到这个逻辑了。
4. promise 超时重试
题目
实现 Promise.retry,重试异步函数,异步函数执行成功后 resolve 结果,失败后重试,尝试超过一定次数才真正的 reject。
实现代码
// 1. 异步函数 Promise和setTimeout
// 2. Promise.retry 重试Promise
function fn() {
const n = Math.random(); // 0-1
return new Promise((resolve, reject) => {
setTimeout(() => {
if (n > 0.7) {
resolve(n);
} else {
reject(n);
}
}, 1000);
});
}
Promise.retry = (fn, times) => {
new Promise(async (resolve, reject) => {
while (times--) {
try {
const res = await fn();
console.log('执行成功,得到的结果是:', res);
resolve(res);
break;
} catch (error) {
console.log('执行失败一次,得到的结果是:', error);
if (!times) {
reject(error);
}
}
}
}).catch(() => {
console.log('全部此时尝试完成,仍然失败');
});
};
Promise.retry(fn, 3);
超时重试就是对 promise
做一个包装处理,如果在规定的次数内没有得到想要的结果,不去改变 promise
的状态,直到 重试次数 times 为 0 时将其变为 rejected
态,或是在 重试期间得到想要的结果 ,就调用 resolve
返回。
业务场景
这个案例比较有意思,对于一些比较重要的操作,进行重试,如果仍不成功,可以做降级处理,用别的方式完成。
5. 数组分组 chunk
题目
按一定长度分割数组。
实现方式
取余
// 实现 chunk(arr: any[], size: number);
const chunk = (arr, size = 0) => {
if(size <= 0) return [];
return arr.reduce((total, cur, index) => {
if(index % size == 0) {
total.push([cur]);
} else {
const last = total.pop() || [];
total.push(last.concat(cur));
}
return total;
}, []);
}
slice
const chunk2 = (arr, size = 0) => {
if(size <= 0) return [];
const res = [];
for(let i=0; i<arr.length; i+=size) {
res.push(arr.slice(i, i+size));
}
return res;
}
console.log(chunk2([1,2,3,4,5], 1));
console.log(chunk2([1,2,3,4,5], 2));
console.log(chunk2([1,2,3,4,5], 3));
console.log(chunk2([1,2,3,4,5], 4));
console.log(chunk2([1,2,3,4,5], 5));
[ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ] ]
[ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]
[ [ 1, 2, 3 ], [ 4, 5 ] ]
[ [ 1, 2, 3, 4 ], [ 5 ] ]
[ [ 1, 2, 3, 4, 5 ] ]
业务场景
对于前端来说,我们一次性向页面插入过多数据时,会造成卡顿。如果可以切片一部分一部分的插入,就会减小卡顿时间,提高用户体验。比如我们插入 1000 条数据,如果通过切片,每次插入 100 条,那么首屏数据是很快就会显示出来的,用户不会觉得卡顿,同时也不影响用户浏览,当用户往下翻的时候,已经完成了对剩余数据的处理了。
6. 拍平对象
题目
实现一个对象的 flatten
方法
const obj = {
a: {
b: 1,
c: 2,
d: {e: 5}
},
b: [1, 3, {a: 2, b: 3}],
c: 3
}
flatten(obj)
调用结果如下:
{
'a.b': 1,
'a.c': 2,
'a.d.e': 5,
'b[0]': 1,
'b[1]': 3,
'b[2].a': 2,
'b[2].b': 3
c: 3
}
实现代码
function isObject(val) {
return typeof val === "object" && val !== null;
}
const flatten = (obj) => {
if (!isObject(obj)) return new TypeError('must accept an Object');
const res = {}
const dfs = (target, prefix) => {
if (isObject(target)) {
for (let key in target) {
let suffix = Array.isArray(target) ? `[${key}]` : (prefix && '.') + key;
dfs(target[key], prefix + suffix);
}
} else {
res[prefix] = target;
}
}
dfs(obj, '');
return res;
}
业务场景
这个可能有的小伙伴不常见,我举个例子。
比如我们常看的小说或文章,都有对应的目录或是章节,大家的章节或是目录分的五花八门,什么第一小节第一话啊、第一章第一节啊等等。像这种情况下后端会返回这么个数组,然后由前端去将章节信息拍平展示。
7. 并发异步调度器
题目
实现一个带 并发限制 的异步调度器 Scheduler
,保证 同时运行 的任务最多有两个
addTask(1000,"1");
addTask(500,"2");
addTask(300,"3");
addTask(400,"4");
输出顺序是:2 3 1 4
整个的完整执行流程:
- 一开始1、2两个任务开始执行
- 500ms时,2任务执行完毕,输出2,任务3开始执行
- 800ms时,3任务执行完毕,输出3,任务4开始执行
- 1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
- 1200ms时,4任务执行完毕,输出4
实现代码
class Scheduler {
constructor(limit) {
this.queue = [];
this.maxCount = limit;
this.runCounts = 0;
}
add(time, order) {
const promiseCreator = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(order);
resolve();
}, time);
});
};
this.queue.push(promiseCreator);
}
taskStart() {
for (let i = 0; i < this.maxCount; i++) {
this.request();
}
}
request() {
// 当任务队列中没有任务 或 并发执行的异步任务数大于限定数,则不执行
if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {
return;
}
this.runCounts++;
this.queue
.shift()()
.then(() => {
this.runCounts--;
this.request();
});
}
}
const scheduler = new Scheduler(2);
const addTask = (time, order) => {
scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();
业务场景
首先从题目我们就可以看出这段代码是用于控制异步并发的,那么结合实际,如果我们将其用于并发请求高的场景,用于限制同一时间内能够执行的最多请求个数。
有的小伙伴可能不知道为啥要控制并发,直接一股脑请求完不就完事了?
实际上对并发数做限制不仅是因为浏览器对并发连接做了数量上的限制,还有一个是它可以减轻服务器压力,不至于过多的请求 同时到达 。
8. 合并公共区间
题目
假设原数组按照每个数组的第一个元素大小排好序。
[[0, 1], [2, 5], [3, 7]]
输出为:
[[0, 1], [2, 7]]
实现代码
const merge = (arrs) => {
return arrs.reduce((f, c) => {
if (f.length == 0) {
f.push(c);
} else if (f[f.length - 1][1] < c[0]) {
f.push(c);
} else {
f[f.length - 1] = [f[f.length - 1][0], c[1]];
}
return f;
}, [])
}
console.log(merge([[0, 1], [2, 5], [3, 7]]))
业务场景
这也是对 时间占用 的一个业务场景,拿我们最常见的日期安排来说,为了让用户更直观的知道哪些日期是可以操作的,我们就需要将已经 占用的连续时间 进行合并,将其区间个数尽可能的减少,这样会更直观。
9. 数字千分位
题目
实现一个千分位 format
函数
接收一个 number
,返回一个 string
。
实现代码
const format = (number) => {
const [integer, decimal] = number.toString().split('.');
const intLens = integer.length;
let formatedInteger = '';
for(let i=intLens-1; i>=0; i--) {
formatedInteger = integer[i] + formatedInteger
if((intLens - i) % 3 == 0 && i !== 0) { // 3位前面增加一个分割
formatedInteger = ',' + formatedInteger;
}
}
// 如果没有小数部分, 则直接返回整数部分
if(decimal == void 0) return formatedInteger;
const deciLens = decimal.length;
let formatedDecimal = '';
for(let i=0; i<deciLens; ++i) {
formatedDecimal += decimal[i];
if((i + 1) % 3 == 0 && i != deciLens - 1) {
formatedDecimal += ','
}
}
return `${formatedInteger}.${formatedDecimal}`;
}
console.log(format(12345.7890)); // 12,345.789,0
console.log(format(0.12345678));// 0.123,456,78
console.log(format(1234567)); // 123,456
业务场景
数字千分位大家开发中应该经常碰到,不知道大家是直接使用 toLocaleString
还是通过正则来实现呢?大家有去比对它们的效率吗?实际上通过 toLocaleString
处理的效率是比较慢的,而且也不够灵活。
10. 概率分配
题目
根据传入的姓名权重信息,返回随机的姓名(随机概率依据权重)。
实现代码
const person = [
{
name: '张三',
weight: 1
},
{
name: '李四',
weight: 10
},
{
name: '王五',
weight: 100
}
]
const getPersonName = (persons) => {
// 计算总的权重,并标记区间
const totalWeight = persons.reduce((pre, cur) => {
cur.startW = pre;
return cur.endW = cur.weight + pre
}, 0)
const num = Math.random() * totalWeight; // 获得一个 0 - totalWeight 的随机数
let person = persons.find(item => item.startW < num && num <= item.endW);
return person.name;
}
function getResult(count) {
const res = {}
for (let i = 0; i < count; i++) {
const name = getPersonName(person)
res[name] = res[name] ? res[name] + 1 : 1
}
return res;
}
console.log(getResult(100));
业务场景
相信大家小时候在学校门口都有抽过奖吧?你是手气好的那个还是手气差的那个呢?如果每个人都有个幸运值的属性,那么幸运值高的人是不是抽到好奖品的概率就高呢?但是幸运值高的人就一定抽到好奖品吗?这是不一定的,黑马就是黑马,爆冷的事天天都在发生。
结语
本文就到此结束啦,列举的 10 个业务场景都是很容易就碰上的,可能小伙伴们还会碰上其他类似的场景,但是万变不离其宗,吃透实现逻辑,做到举一反三,才是阅读本文的最大意义。
如果小伙伴们有别的应用场景的例子,欢迎留言,让我们共同学习进步💪💪。
如果文中有不对的地方,或是大家有不同的见解,欢迎指出🙏🙏。
如果大家觉得所有收获,欢迎一键三连💕💕。
转载自:https://juejin.cn/post/7152722756587978760