前言
笔者最近遇见了一个很有趣的需求,算法有一个挖掘数据的输出结果,结果为n条记录,我已经把输出结果展示在页面中了,但是现在让我增加上一页的功能,涉及到的操作如下:
- 获取一定数量的挖掘记录
- 用户会标记这些记录
- 点击下一页获取新的挖掘记录
- 点击上一页返回已经标记过的记录(即使点击了很多次下一页,且这期间没有做任何标记)
- 并且用户可以取消或者修改已经标记过的记录
- 点击多次上一页之后点击下一页回退前面几次的上一页记录,如果没有标记过的,则展示未标记的记录
我很纳闷的问算法同学,为啥不能设计成分页效果,前端根据pageNum和pageSize去请求对应的记录了,算法同学告诉我,因为算法的输出结果是按算法排序的,每次获取都是按算法推荐的优先级优先推送的。所以没法设计成分页效果。
听了之后一脸懵逼。。。
对数据挖掘,咱也不知道,咱也不敢问啊
一个数组走天下
既然算法不支持,那就自己实现吧,我的大脑中首先想到的方式是将已经标记的记录存放在数组中,当点击上一页时,从这个数组中读取一定数量(pageSize)的已标记记录,并用临时变量记录位置,点击上一页继续向上读取,并且改变记录位置,点击下一页时,回退上面的操作,直至回到这个数组的末尾,再次点击下一页,获取未标记的记录。
思路上完全可以走通,但是总觉得一个数组走天下有点low啊
杀鸡要用宰牛刀
宰牛刀
仔细考虑,发现上述数组读取和记录位置的操作其实是栈的操作:后进先出
标记时将记录推入到栈中,点击上一页时,从栈中弹出最近推入的记录
但是点击下一页,需要将上次弹出的记录依次推入栈,直至没有弹出的记录,所以,我们还需要一个栈去存放推出的记录,好家伙,我直接好家伙,还得两个栈,双倍的快乐
好,瞬间就觉得高大上了点,那就class Stack开始做把宰牛刀吧
export class HistoryStack {
constructor () {
// 记录已标记(点击上一页时展示的数据)
this.history = []
// 存放弹出的(点击下一页时展示的数据)
this.memory = []
}
// 标记时推入
push (tag) {
this.history.push(tag)
}
// 弹出
pops (size) {
// 弹出记录下来
const contaienr =
this.history
.splice(
this.history.length - size,
size
)
// 记录弹出
this.memory = this.memory.concat(...cantainer)
// 每次弹出时捕获
return container
}
// 推入
pushs (size) {
// 推入时也记录下来
const contaienr =
this.memory
.splice(
this.memory.length - size,
size
)
// 推入弹出的记录
this.history = this.history.concat(...cantainer)
// 每次推入时捕获
return container
}
}
好,基本的功能已经实现了,但是还记得开头说的吗,用户会返回到之前标记的页面修改或者删除标记,what^s up,所以这个栈还需要额外的功能,修改某一项、删除某一项
export class HistoryStack {
constructor () {
// 记录已标记(点击上一页时展示的数据)
this.history = []
// 存放弹出的(点击下一页时展示的数据)
this.memory = []
}
//...
// 删除
delete (id) {
// 查找位置
const index = this.history.findIndex(his => his.id === id)
// 这里是确认是在上一页中删除还是下一页中删除哦
if (index !== -1) {
this.history.splice(index - 1, 1)
} else {
const index = this.memory.findIndex(meo => meo.id === id)
if (index !== -1) this.memory.splice(index, 1)
}
}
// 修正标记,但是这个修正是有副作用的,因为会改变存放的位置
edit (tag) {
// 查找位置
const index = this.history.findIndex(his => his.id === tag.id)
// 这里是确认是在上一页中删除还是下一页中删除哦
if (index !== -1) {
this.history[index] = tag
} else {
const index = this.memory.findIndex(meo => meo.id === tag.id)
if (index !== -1) this.memory[index] = tag
}
}
}
另外,这些已标记的记录需要缓存在本地,当每次进入页面时都是在下一页的状态,j换句话说,在页面关闭时,需要重置整个状态
export class HistoryStack {
constructor () {
// 记录已标记(点击上一页时展示的数据)
this.history = []
// 存放弹出的(点击下一页时展示的数据)
this.memory = []
}
// ...
// 复制
copy (data) {
const { history = [], memory = [] } = data
this.history = history
this.memory = memory
}
// 关闭时重置
close () {
// 重置
this.history = this.history.concat(...this.memory)
this.memory = []
}
}
太棒了,功能完全实现了,完整代码点这里
杀鸡
在使用这个功能的页面实例化这个类,准备好可能用到的接口
// 初始化数据结构
const history = new HistoryStack()
// 初始化缓存
if (!localStorage.getItem('name')) {
localStorage.setItem('name', JSON.stringify(history))
}
// 将缓存和数据结构绑定
history.copy(JSON.parse(localStorage.getItem('name')))
// pageSize
let size = 3
// 变动列表
const list = []
// 标记时调用
const add = tag => history.push(tag)
// 取消标记时调用
const del = id => history.delete(id)
// 上一页调用
const lasts = size => list = history.pops(size)
// 下一页调用
const nexts = size => list = history.pushs(size)
// 关闭时调用
const close = () => history.close()
下面是以vue为例,实现的demo,源码点这里
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>stack实践</title>
<style>
.bg {
background: #CAE5E8;
color: #fff;
}
.flex {
display: flex;
justify-content: space-between;
}
.padding {
padding: 10px;
}
.margin {
margin: 10px;
}
.radius {
border-radius: 10px;
}
</style>
</head>
<body>
<div id="app">
<div class="flex">
<div @click="onLasts" class="bg margin padding radius">上一页</div>
<div class="margin padding">3</div>
<div @click="onNexts" class="bg margin padding radius">下一页</div>
</div>
<div v-if="list.length" v-for="item in list" key="item" class="flex">
<div class="bg margin padding radius" style="width: 68vw;height: 10vh;">
<!-- {{item}} -->
</div>
<div style="height: 10vh;">
<div @click="onAdd({id: item})" class="bg margin padding radius">标记</div>
<div @click="onDel(item.id)" class="bg margin padding radius">清标</div>
</div>
</div>
<div v-if="!list.length" class="bg margin padding radius">null</div>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script src="./stack.js"></script>
<script>
const {onMounted, reactive, watch} = Vue
const initData = () => {
return [1, 1, 1].map(() => Math.random())
}
Vue.createApp({
setup () {
// 渲染列表
const list = reactive(initData())
// 实例化
const history = new HistoryStack()
// 将变动保存在本地
const copy = () => localStorage.setItem('name', JSON.stringify(history))
// 初始化缓存
if (!localStorage.getItem('name')) {
copy()
}
// 将缓存和数据结构绑定
history.copy(JSON.parse(localStorage.getItem('name')))
// 恢复下,因为推出前可能是在上一页状态中,并保存
history.close()
copy()
// 标记时调用
const add = tag => history.push(tag)
// 取消标记时调用
const del = id => history.delete(id)
// 上一页调用
const lasts = size => history.pops(size)
// 下一页调用
const nexts = size => history.pushs(size)
// 关闭时调用
const close = () => history.close()
// 标记时调用
const onAdd = tag => setTimeout(() => {
add(tag)
copy()
}, 100)
// 取消标记按钮
const onDel = id => setTimeout(() => {
del(id)
copy()
}, 100)
// 上一页按钮调用
const onLasts = () => {
list.length = 0
if (history.history.length > 0) {
list.push(...lasts(3))
copy()
}
}
// 下一页按钮调用
const onNexts = () => {
list.length = 0
if (history.memory.length > 0) {
list.push(...nexts(3))
copy()
} else {
list.push(...initData())
}
}
// 关闭事件时调用
onMounted(() => close())
return {list, onAdd, onDel, onLasts, onNexts}
}
}).mount('#app')
</script>
</body>
</html>
效果图如下:(录屏的时候没录到鼠标,很抱歉)