react immer 不可变数据

为什么强调不可变数据

1import React from 'react'
2
3export default () => {
4  const [obj, setObj] = useState({ name: 'alvin', others: { age: 18 } })
5  const [count, setCount] = useState(0)
6
7  const hanleClick = () => {
8    obj.others.age = 99
9    setCount(prev => prev + 1)
10  }
11
12  return (
13    <>
14      <button onClick={hanleClick}>click</button>
15      {JSON.stringify(obj, null, 2)}
16    </>
17  )
18}

如上,点击后就会发现 obj 引用没变,但是 obj.others.age 修改了,然后也被重新渲染了,这是由于这就是引用类型的副作用导致的。

解决方案

  1. 浅复制:只能复制一层
  2. 深克隆:我们不仅要考虑到正则、Symbol、Date 等特殊类型,还要考虑到原型链和循环引用的处理,性能消耗大!
  3. 深克隆的的性能相比于浅克隆大打折扣,但是浅克隆又不能从根本上杜绝引用类型的副作用,使用 immutable:
    • 即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

实现简单的 immutable

immer 用法举例: const nextState = produce(state, (draft) => {});

1/** 浅复制 */
2function shallowCopy(value) {
3  if (Array.isArray(value))
4    return value.slice()
5  if (value.__proto__ === undefined)
6    return Object.assign(Object.create(null), value)
7  return Object.assign({}, value)
8}
9
10function createState(target) {
11  this.modified = false // 是否被修改
12  this.target = target // 目标对象
13  this.copy = undefined // 拷贝的对象
14}
15
16createState.prototype = {
17  // 对于get操作,如果目标对象没有被修改直接返回原对象,否则返回拷贝对象
18  get(key) {
19    if (!this.modified)
20      return this.target[key]
21    return this.copy[key]
22  },
23
24  // 对于set操作,如果目标对象没被修改那么进行修改操作,否则修改拷贝对象
25  set(key, value) {
26    if (!this.modified)
27      this.markChanged()
28    return (this.copy[key] = value)
29  },
30
31  // 标记状态为已修改,并拷贝
32  markChanged() {
33    if (!this.modified) {
34      this.modified = true
35      this.copy = shallowCopy(this.target)
36    }
37  },
38}
39
40const PROXY_STATE = Symbol('proxy-state')
41
42// 接受一个目标对象和一个操作目标对象的函数
43function produce(state, producer) {
44  const store = new createState(state)
45  const proxy = new Proxy(store, {
46    get(target, key) {
47      if (key === PROXY_STATE)
48        return target
49      return target.get(key)
50    },
51    set(target, key, value) {
52      return target.set(key, value)
53    },
54  })
55
56  producer(proxy)
57
58  const newState = proxy[PROXY_STATE]
59  if (newState.modified)
60    return newState.copy
61  return newState.target
62}
63
64const baseState = [
65  { todo: 'Learn typescript', done: true },
66  { todo: 'Try immer', done: false },
67]
68
69const nextState = produce(baseState, (draft) => {
70  draft.push({ todo: 'Tweet about it', done: false })
71  draft[1].done = true // 这里会改到原属性
72})
73
74console.log(baseState, nextState)

执行结果:

1[
2  { todo: 'Learn typescript', done: true },
3  { todo: 'Try immer', done: true },
4][
5  ({ todo: 'Learn typescript', done: true }, { todo: 'Try immer', done: true }, { todo: 'Tweet about it', done: false })
6]

defineProperty vs Proxy

这个没特别的深入,

  1. defineProperty 无法监听数组变化 比如 arr[1] = 2; 如果需要监听
  2. defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。
  3. Proxy 可以直接监听对象而非属性,Proxy 的劣势就是兼容性问题,而且无法用 polyfill 磨平