1.5 拆分数据优化

77 阅读3分钟

上节最后,我提出一个疑问:

我们这次使用文件或localStorage持久化了数据,但上面的代码有什么问题?还能优化吗?

问题

其实早在1.3节,有段高亮的内容,不知道大家有没有注意到:

注意:增加、修改、删除都是在它的基础上,修改之后,再覆盖原文件。

仔细看user.service.ts代码:

async addUser(user: Omit<User, "id">) {
  const users = await this.getAll();
  const id = users.length + 1;
  const newUser = {
    ...user,
    id,
  };
  users.push(newUser);
  await this.saveToFile(users);
  return newUser;
}

async removeUser(id: number) {
  const users = await this.getAll();
  const newUsers = users.filter((user) => user.id !== id);
  await this.saveToFile(newUsers);
}

async updateUser(id: number, user: Omit<User, "id">) {
  const users = await this.getAll();
  const oldUser = users.find((u) => u.id === id);
  if (!oldUser) {
    throw new Error(`user not found`);
  }
  Object.assign(oldUser, user);
  await this.saveToFile(users);
 }

无论增加、修改还是删除,都需要先读取一遍所有数据,然后在它的基础上进行操作。

假设我们先增加一条数据,未等它结束,就更新另一条数据,会是什么结果呢?

在浏览器打开http://localhost:8000/user,F12里执行:

fetch("/user", {
  method: "post",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ author: "王五", age: 15 }),
});
fetch("/user/1", {
  method: "put",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ age: 10 }),
});

呃,你发现好像没什么问题。

image.png

从我们代码上看,要复现这个问题,时机很难把握。

所以我们在代码中添加以下:

userService.addUser({
  author: "张三",
  age: 20,
});
userService.updateUser(1, {
  author: "李四",
  age: 11,
});

刷新http://localhost:8000/user,我们预期的是会有个新用户张三,但事实只是李四年龄修改了:

image.png

所以,现有代码只维护了一个数据文件,多人同时操作时就有可能出现bug。

而且,当这个文件体量越来越大时,每次读取、处理的性能开销也会越来越大。

优化

针对这种情况,怎么办呢?

当然是分而治之。大了就往小的拆呗。

现在所有数据都在localStorage的users下面。我们需要有几个空间呢?

  1. 一条数据一个文件,比如id是1,就有一条users_1的数据。
  2. 一个维护自增长的id:users_id
  3. 存储当前用户的ids:users_ids

这样一来,根据id查找、修改都较之前要快,而且修改不会与其它操作冲突。比原来要慢的是获取全量数据,而在实际业务中,如果数据量大的话,一般都要使用分页。

修改后的代码如下:

class UserService {
  async getAll(): Promise<User[]> {
    const userIdsStr = localStorage.getItem("users_ids");
    if (userIdsStr) {
      const ids: string[] = JSON.parse(userIdsStr);
      const users = await Promise.all(
        ids.map((id) => this.getUserById(parseInt(id))),
      );
      return users.filter(Boolean) as User[];
    }
    return [];
  }
  async getUserById(id: number): Promise<User | null> {
    const userStr = localStorage.getItem(`users_${id}`);
    if (userStr) {
      return JSON.parse(userStr) as User;
    }
    return null;
  }

  generateId(): number {
    const idStr = localStorage.getItem("users_id");
    if (idStr) {
      const id = parseInt(idStr, 10) + 1;
      localStorage.setItem("users_id", id.toString()); // 取一次就得改一次
      return id;
    } else {
      localStorage.setItem("users_id", "1");
      return 1;
    }
  }

  async addUser(user: Omit<User, "id">) {
    const id = this.generateId();
    const newUser = {
      ...user,
      id,
    };
    const userIdsStr = localStorage.getItem("users_ids");
    const userIds = userIdsStr ? JSON.parse(userIdsStr) : [];
    userIds.push(id);
    localStorage.setItem(`users_${id}`, JSON.stringify(newUser));
    localStorage.setItem("users_ids", JSON.stringify(userIds));
    return newUser;
  }

  async removeUser(id: number) {
    const userIdsStr = localStorage.getItem("users_ids");
    if (!userIdsStr) {
      return;
    }
    const userIds = JSON.parse(userIdsStr);
    const index = userIds.indexOf(id);
    if (index > -1) {
      userIds.splice(index, 1);
      localStorage.setItem("users_ids", JSON.stringify(userIds));
    }
  }

  async updateUser(id: number, user: Omit<User, "id">) {
    const oldUser = await this.getUserById(id);
    if (!oldUser) {
      return;
    }
    Object.assign(oldUser, user);
    localStorage.setItem(`users_${id}`, JSON.stringify(oldUser));
  }
}

看起来有些复杂,再封装2个函数:

function getData<T = string>(key: string) {
  const str = localStorage.getItem(key);
  if (str) {
    return JSON.parse(str) as T;
  }
  return null;
}

function setData(key: string, val: unknown) {
  localStorage.setItem(key, JSON.stringify(val));
}

这样代码能清减一点:

class UserService {
  async getAll(): Promise<User[]> {
    const ids = getData<string[]>("users_ids");
    if (ids) {
      const users = await Promise.all(
        ids.map((id) => this.getUserById(parseInt(id))),
      );
      return users.filter(Boolean) as User[];
    }
    return [];
  }
  async getUserById(id: number): Promise<User | null> {
    return getData<User>(`users_${id}`);
  }

  generateId(): number {
    const idStr = getData("users_id");
    if (idStr) {
      const id = parseInt(idStr, 10) + 1;
      setData("users_id", id); // 取一次就得改一次
      return id;
    } else {
      setData("users_id", 1);
      return 1;
    }
  }

  async addUser(user: Omit<User, "id">) {
    const id = this.generateId();
    const newUser = {
      ...user,
      id,
    };
    const userIds = getData<number[]>("users_ids") || [];
    userIds.push(id);
    setData(`users_${id}`, newUser);
    setData("users_ids", userIds);
    return newUser;
  }

  async removeUser(id: number) {
    const userIds = getData<number[]>("users_ids");
    if (!userIds) {
      return;
    }
    const index = userIds.indexOf(id);
    if (index > -1) {
      userIds.splice(index, 1);
      setData("users_ids", userIds);
    }
  }

  async updateUser(id: number, user: Omit<User, "id">) {
    const oldUser = await this.getUserById(id);
    if (!oldUser) {
      return;
    }
    Object.assign(oldUser, user);
    setData(`users_${id}`, oldUser);
  }
}

验证

再用这段代码验证下:

userService.addUser({
  author: "张三",
  age: 20,
});
userService.updateUser(1, {
  author: "李四",
  age: 11,
});

可以看到张三添加成功了,李四的年龄也修改成功了。

作业

再思考下,上面的代码还有什么问题?还有什么能优化的吗?