杀鸡要用宰牛刀:class与栈的应用与实践

96 阅读4分钟

前言

笔者最近遇见了一个很有趣的需求,算法有一个挖掘数据的输出结果,结果为n条记录,我已经把输出结果展示在页面中了,但是现在让我增加上一页的功能,涉及到的操作如下:

  1. 获取一定数量的挖掘记录
  2. 用户会标记这些记录
  3. 点击下一页获取新的挖掘记录
  4. 点击上一页返回已经标记过的记录(即使点击了很多次下一页,且这期间没有做任何标记)
  5. 并且用户可以取消或者修改已经标记过的记录
  6. 点击多次上一页之后点击下一页回退前面几次的上一页记录,如果没有标记过的,则展示未标记的记录

image.png

我很纳闷的问算法同学,为啥不能设计成分页效果,前端根据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>

效果图如下:(录屏的时候没录到鼠标,很抱歉)

GIF 2022-4-19 22-14-23.gif