跳到主要内容

实战场景

· 阅读需 9 分钟
Slipeda

1.异步任务,控制并发数目

  • 任务数量通过 size 来控制
  • 添加 Promise 异步任务,任务数量没达到 max 并发数 则直接执行异步
  • 重要: Promise 异步任务结束后 减少 size 直接从任务池中取出一个新任务来执行

class TaskConcurrent {
constructor(size) {
this.max = size
this.size = 0 // 并发数量控制
this.taskQueue = [] // 任务队列
}

// 生成异步任务对象
taskFactory(fn, params, resolve, reject) {
return {
fn, // 异步任务
params, // 函数参数
resolve, // 异步完成
reject, // 异步错误
}
}

// 添加任务
addTask(fn, ...params) {
return new Promise((resolve, reject) => {
const taskObj = this.taskFactory(fn, params, resolve, reject)
// 添加到栈尾
this.taskQueue.push(taskObj)
if (this.size !== this.max) {
this.queueOutTask()
}
})
}

// 从栈中取出任务
queueOutTask() {
// 任务池 没有任务了
if (this.taskQueue.length === 0) {
return
}
// 开始异步任务 增加当前同时并发的任务数量
this.size++
const {
resolve, fn, params, reject,
} = this.taskQueue.shift() // 先进先出
const taskPromise = this.runTask(fn, params, reject)
// 返回一个promise promise resolve出一个promise 会自动链式调用
resolve(taskPromise)
}

// 执行任务
runTask(fn, params, reject) {
// 执行任务 如果返回值不是异步 包装返回值成异步
const taskPromise = Promise.resolve(fn(...params))
taskPromise
.then((res) => {
console.log('异步结束', res)
this.pullTask() // 取出新的回调
})
.catch((err) => {
this.pullTask() // 取出新的回调
reject(err) // 异步失败
})
return taskPromise
}

// 异步结束 添加新的异步任务
pullTask() {
// 上一个任务有结果了 开放一个并发名额出来
this.size--
// 从任务池中取出任务 自动执行异步任务
this.queueOutTask()
}
}



// 调用addTask一个一个添加异步任务
const task = (timeout) => new Promise((resolve) => setTimeout(() => {
resolve(timeout) // 返回值
}, timeout))

// 模拟异步任务1
// const taskList = [5000, 3000, 1000, 10300, 8000, 2000, 4000, 5000]
// async function startNoConcurrentControl() {
// // 初始化并发池
// const cc = new TaskConcurrent(2)
// console.time('异步执行时间')
// // 添加所有异步任务
// const resArr = await Promise.all(taskList.map((item) => cc.addTask(task, item)))
// console.log('异步任务返回值', resArr)
// console.timeEnd('异步执行时间')
// }
// startNoConcurrentControl()

// 模拟异步2 循环添加异步任务
function start() {
const taskConcurrent2Instance = new TaskConcurrent(2)
let count = 10
// 组织参数
while (count--) {
const p = taskConcurrent2Instance.addTask(task, count * 1000)
p.then((res) => {
console.log('p', res)
})
}
}
start()

2.根据表达式计算字母数

  • 给定一个描述字母数量的表达式,计算表达式里的每个字母实际数量
  • 表达式格式:
    • 字母紧跟表示次数的数字,如 A2B3
    • 括号可将表达式局部分组后跟上数字,(A2)2B
    • 数字为 1 时可缺省,如 AB3
  • 示例_:
   countOfLetters('A2B3'); // { A: 2, B: 3 }
countOfLetters('A(A3B)2'); // { A: 7, B: 2 }
countOfLetters('C4(A(A3B)2)2'); // { A: 14, B: 4, C: 4 }
// 栈+哈希表
function countOfAtoms(formula) {
let i = 0;
const n = formula.length;

const stack = [new Map()] // 初始化压入一个空栈
while (i < n) {
const ch = formula[i]
// 解析一串连续的字母
function parseAtom() {
const sb = []
sb.push(formula[i++]); // 扫描首字母
// 扫描首字母后的小写字母
while (i < n && formula[i] >= 'a' && formula[i] <= 'z') {
sb.push(formula[i++])
}
return sb.join('');
}
// 解析数字
const parseNum = () => {
// 到末尾了 || 不是数字,视作 1
if (i === n || isNaN(Number(formula[i]))) {
return 1
}
// 获取数字
let num = 0
while (i < n && !isNaN(Number(formula[i]))) {
const base = num * 10 // 如果是多位数字 则扩大十倍
const now = Number(formula[i]) // 当前数字
num = base + now // 扫描数字
i++
}
return num
}

if (ch === '(') {
i++;
//增加括号层级
stack.unshift(new Map()) // 将一个空的哈希表压入栈中,准备统计括号内的原子数量
} else if (ch === ')') {
i++
const num = parseNum() // 括号右侧数字
减少括号层级
const popMap = stack.shift() //弹出括号内的原子数量
const topMap = stack[0]
// 栈中的数量与初始化的栈进行合并字母数量
for (const [atom, count] of popMap.entries()) {
let beforeNum = topMap.get(atom) || 0 //初始化栈中之前的数量
let nowNum = count * num // 栈中的数量 乘以倍数
topMap.set(atom, nowNum + beforeNum) //将括号内的原子数量乘上 num,加到上一层的原子数量中
}
} else {
const atom = parseAtom() // 解析完字母
const num = parseNum() //字母后面是否跟着数字
// 将最外面层级 合并到第一个栈中
const topMap = stack[0]
topMap.set(atom, (topMap.get(atom) || 0) + num)

}
}
// 最后都合并到第一个栈中
let map = stack.pop()
return Object.fromEntries(map.entries()) // map转对象
}

3. 数组转 tree 结构的数据

// 源数据
const list = [
{
id: 19,
parentId: 0,
},
{
id: 18,
parentId: 16,
},
{
id: 17,
parentId: 16,
},
{
id: 16,
parentId: 0,
},
]

// 转换后的数据结构

const tree = {
id: 0,
children: [
{
id: 19,
parentId: 0,
},
{
id: 16,
parentId: 0,
children: [

{
id: 18,
parentId: 16,
},
{
id: 17,
parentId: 16,
},
],
},
],
}


function convert2(list, parentKey, currentKey, rootValue) {
// 数据结构初始化
let obj = {
[currentKey]: rootValue,
children: []
}
let num = 0

// 为所有节点 添加到父结构中
while (num !== list.length) {
list.forEach((item, index) => {
if (!item) return
// 收集最外层
if (item[parentKey] === obj[currentKey]) {
obj.children.push({
...item,
children: []
})
list[index] = null
num++
} else {
// 递归找层级
helpFn(item, index, obj.children)
}
// 递归找层级
})
}
// 为item 添加层级
function helpFn(item, initIndex, arr) {
// 寻找当前层级
let index = arr.findIndex(ele => ele[currentKey] === item[parentKey])
if (index !== -1) {
arr[index].children.push({
...item,
children: []
})
list[initIndex] = null
num++
return true
}
// 找他们的子级的元素
for (let ele of arr.values()) {
if (helpFn(item, index, ele.children)) {
// 找到该item的层级 取消递归
return true
}
}
}
return obj
}

4.前端防刷

前端防刷:

  • 接口防抖,loadding。
  • 前端使用验证码。
  • 前后端,请求参数采取签名加密, 比如带个时间戳,过期则是失效。
  • 前端:源 ip 请求个数限制。对请求来源的 ip 请求个数做限制。

后端防刷:

  • 请求头校验:
    • 源 ip 请求个数限制。对请求来源的 ip 请求个数做限制。
    • User-Agent 校验, User-Agent 首部包含了一个特征字符串,用来让网络协议的对端来识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号。 比如:Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0
    • Referer 限频
    • Referer 限制
  • 将对方的 IP 地址记录在 LocalStorage 里面,考虑相同公司,小区可能使用同一个 IP, 适当限制相同 IP 重复抽奖。
  • 前后端,请求参数采取签名加密。