背景
不知道大家在日常开发中有没有遇到过这样的场景:
给定一个列表,其中的元素是对象,每个对象都有唯一的 id,对列表执行的操作有以下几种可能:
- 往列表中添加新的元素
- 根据 id 查找某个元素
- 根据 id 修改列表中某个元素的属性值
- 根据 id 从列表中删除某个元素
举个例子:给定一个书本列表,其中每个元素都是一本书,每本书都有唯一的编号;除了编号,每本书还有其它的属性,例如书名、评分等。
模型设计
通常情况下,我们使用一个数组来保存这个列表:
const state = {
books: [
{
id: "1",
title: "数据结构",
rate: 6
},
{
id: "2",
title: "编译原理",
rate: 8
}
]
}
增删改查
/**
* 往 books 末尾添加新的 book
* @param {Book} book
*/
function addNewBook(book) {
state.books.push(book)
}
/**
* 根据 id 获取某个 book
* @param {string} id
* @return {Book}
*/
function findBookById(id) {
for (let i = 0; i < state.books.length; ++i) {
if (state.books[i].id === id) {
return state.books[i]
}
}
return null
}
/**
* 根据 id 更新某个 book 的 rate
* @param {string} id
* @param {number} rate
*/
function updateBookRateById(id, rate) {
for (let i = 0; i < state.books.length; ++i) {
if (state.books[i].id === id) {
state.books[i].rate = rate
break
}
}
}
/**
* 根据 id 删除某个 book
* @param {string} id
*/
function removeBookById(id) {
for (let i = 0; i < state.books.length; ++i) {
if (state.books[i].id === id) {
state.books.splice(i, 1)
break
}
}
}
缺点
上述的模型设计有一个比较严重的缺点,即“根据 id 修改元素的属性值”和“根据 id 查找元素”的时候都需要遍历数组,假设数组长度为 n,则每次操作的最坏情况下的时间复杂度为 O(n)。
如果数组比较长,或者更新、查找和删除的操作比较频繁,那么这样的设计是有可能引起性能问题的。
优化模型设计
想要优化模型设计,其实并不难。首先明确一个前提条件:这是一个对象数组,每个对象都有唯一的 id。上述方案的最大问题在于根据 id 查找对象时浪费了太多时间,很显然,想要快速找到对象,不需要每次都遍历数组,只需要找到 id 映射的对象即可。
也就是说,我们需要建立一个 id 到其对象的映射,直接用 JS 的普通对象即可以实现。而为了保证元素的顺序,我们仍需要保留一个 id 列表。
如果需要,我们还可以通过 id 列表和映射通过 map
函数生成完整的列表。
const state = {
bookIdList: ["1", "2"],
bookMap: {
"1": {
id: "1",
title: "数据结构",
rate: 6
},
"2": {
id: "2",
title: "编译原理",
rate: 8
}
}
}
// 完整的列表可以通过 map 函数生成
state.books = state.bookIdList.map(id => state.bookMap[id])
更快的改、查
使用优化后的模型,每次修改、查找的时间复杂度降到 O(1):
/**
* 根据 id 更新某个 book 的 rate
* @param {string} id
* @param {number} rate
*/
function updateBookRateById(id, rate) {
state.bookMap[id].rate = rate
}
/**
* 根据 id 获取某个 book
* @param {string} id
* @return {Book}
*/
function findBookById(id) {
return state.bookMap[id] || null
}
在 Vue 中实现
id 列表和映射可以定义在 data
中,而完整的列表会根据 id 列表和映射的变化而变化,所以应该定义在 computed
中:
<template>
<div>
<ul>
<li v-for="book in books" :key="book.id">
<div>书名:{{ book.title }}</div>
<div>评分:{{ book.rate }}</div>
</li>
</ul>
</div>
</template>
export default {
data() {
return {
bookIdList: [],
bookMap: {}
}
},
computed: {
books() {
return this.bookIdList.map(id => this.bookMap[id])
}
},
methods: {
addNewBook(book) {
this.$set(this.bookMap, book.id, book)
this.bookIdList.push(book.id)
},
updateBookRateById(id, rate) {
this.bookMap[id].rate = rate
}
}
}
实际上,我们并不一定非要定义一个完整的列表,因为 id 列表和映射已经包含了完整的信息,为什么非要把它们组合起来呢?
也就是说,books
在这里设计为 computed
其实是没有必要的,template
里面也可以这样写:
<template>
<div>
<ul>
<li v-for="id in bookIdList" :key="id">
<div>书名:{{ bookMap[id].title }}</div>
<div>评分:{{ bookMap[id].rate }}</div>
</li>
</ul>
</div>
</template>
应用场景
适用条件
- 必须是对象列表
- 每个元素必须有唯一的 id
适用场景
- 频繁的根据 id 修改列表中的对象的操作
- 元素太多,遍历可能会影响性能问题
不足
- 这个模式只是优化了修改、查找的过程,并没有优化删除的过程,如果要从列表中删除某个元素,仍然需要遍历数组。如果要解决这个问题,可以考虑用链表+映射的数据结构。
- 很显然,新的模型会比旧的模型占用更多的内存