前端设计模式:MVVM框架中如何更新对象数组

721 阅读4分钟

背景

不知道大家在日常开发中有没有遇到过这样的场景:

给定一个列表,其中的元素是对象,每个对象都有唯一的 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>

应用场景

适用条件

  1. 必须是对象列表
  2. 每个元素必须有唯一的 id

适用场景

  • 频繁的根据 id 修改列表中的对象的操作
  • 元素太多,遍历可能会影响性能问题

不足

  • 这个模式只是优化了修改、查找的过程,并没有优化删除的过程,如果要从列表中删除某个元素,仍然需要遍历数组。如果要解决这个问题,可以考虑用链表+映射的数据结构。
  • 很显然,新的模型会比旧的模型占用更多的内存