不可变数据库之immer

252 阅读3分钟

前言

  • immer 的作者同时也是 mobx 的作者。mobx 又像是把 Vue 的一套东西融合进了 React
  • 安装: npm install immer

例子

需要更新第二个 todo,并添加第三个。但是,我们不想改变原始的 currentState,我们也想避免深度克隆(以保留第一个 todo)

const currentState = [
  {
    title: 'Learn TypeScript',
    done: true
  },
  {
    title: 'Try Immer',
    done: false
  }
]

不使用 Immer

const nextState = currentState.slice() // 浅拷贝数组
// 更新第二个 todo
nextState[1] = {
  ...nextState[1], // 浅拷贝第一层元素
  done: true // 期望的更新
}
// 添加第三个
// 因为 nextState 是新拷贝的, 所以使用 push 方法是安全的,
// 但是在未来的任意时间做相同的事情会违反不变性原则并且导致 bug!
nextState.push({title: 'Tweet about it'})

使用 Immer

  • 利用 produce 函数,它将我们要更改的 state 作为第一个参数
  • 对于第二个参数,我们传递一个名为 recipe 的函数,该函数传递一个 draftState 参数,我们可以对其应用直接的 mutations
  • 一旦 recipe 执行完成,这些 mutations 被记录并用于产生下一个状态 nextState
  • produce 将负责所有必要的复制,并通过冻结数据来防止未来的意外修改
import produce from 'immer'

const nextState = produce(currentState, draftState => {
  draftState[1].done = true
  draftState.push({title: 'Tweet about it'})
})

基本概念

  • currentState: 被操作对象的最初状态
  • draftState: 根据currentState生成的草稿、是currentState的代理、对draftState所有的修改都被记录并用于生成nextState。在此过程中,currentState不受影响
  • nextState: 根据draftState生成的最终状态
  • produce: 用于生成nextState或者producer的函数
  • Producer: 通过produce生成,用于生产nextState,每次执行相同的操作
  • recipe: 用于操作draftState的函数

结合react使用

useState + Immer

import React, { useCallback, useState } from 'react'
import produce from 'immer'

const TodoList = () => {
  const [todos, setTodos] = useState([
    {
      id: 'React',
      title: 'Learn React',
      done: true
    },
    {
      id: 'Immer',
      title: 'Try Immer',
      done: false
    }
  ])

  const handleToggle = useCallback((id) => {
    setTodos(
      produce(draft => {
        const todo = draft.find((todo) => todo.id === id)
        todo.done = !todo.done
      })
    )
  }, [])

  const handleAdd = useCallback(() => {
    setTodos(
      produce(draft => {
        draft.push({
          id: 'todo_' + Math.random(),
          title: 'A new todo',
          done: false
        })
      })
    )
  }, [])

  return (<div>{*/ See CodeSandbox */}</div>)
}

useImmer

import React, { useCallback } from 'react'
import { useImmer } from 'use-immer'

const TodoList = () => {
  const [todos, setTodos] = useImmer([
    {
      id: 'React',
      title: 'Learn React',
      done: true
    },
    {
      id: 'Immer',
      title: 'Try Immer',
      done: false
    }
  ])

  const handleToggle = useCallback((id) => {
    // 相对于useState + Immer,简化了produce
    setTodos(draft => {
      const todo = draft.find((todo) => todo.id === id)
      todo.done = !todo.done
    })
  }, [])

  const handleAdd = useCallback(() => {
    // 相对于useState + Immer,简化了produce
    setTodos(draft => {
      draft.push({
        id: 'todo_' + Math.random(),
        title: 'A new todo',
        done: false
      })
    })
  }, [])

useReducer + Immer

import React, {useCallback, useReducer} from 'react'
import produce from 'immer'

const TodoList = () => {
  const [todos, dispatch] = useReducer(
    produce((draft, action) => {
      switch (action.type) {
        case 'toggle':
          const todo = draft.find(todo => todo.id === action.id)
          todo.done = !todo.done
          break
        case 'add':
          draft.push({
            id: action.id,
            title: 'A new todo',
            done: false
          })
          break
        default:
          break
      }
    }),
    [
      /* initial todos */
    ]
  )

  const handleToggle = useCallback(id => {
    dispatch({
      type: 'toggle',
      id
    })
  }, [])

  const handleAdd = useCallback(() => {
    dispatch({
      type: 'add',
      id: 'todo_' + Math.random()
    })
  }, [])
}

useImmerReducer

import React, { useCallback } from 'react'
import { useImmerReducer } from 'use-immer'

const TodoList = () => {
  const [todos, dispatch] = useImmerReducer(
    // 相对于useReducer + Immer,简化了produce
    (draft, action) => {
      switch (action.type) {
        case 'toggle':
          const todo = draft.find((todo) => todo.id === action.id)
          todo.done = !todo.done
          break
        case 'add':
          draft.push({
            id: action.id,
            title: 'A new todo',
            done: false
          })
          break
        default:
          break
      }
    },
    [ /* initial todos */ ]
  )
}

Redux + Immer

import produce from 'immer'

// 初始 state
const INITIAL_STATE = [
  /* 一系列 todos */
]

const todosReducer = produce((draft, action) => {
  switch (action.type) {
    case 'toggle':
      const todo = draft.find(todo => todo.id === action.id)
      todo.done = !todo.done
      break
    case 'add':
      draft.push({
        id: action.id,
        title: 'A new todo',
        done: false
      })
      break
    default:
      break
  }
})

Immer 和 Immutable 的比较

Immer

  • 优点
    • 上手容易: API 简单上手容易,同时项目迁移改造也比较容易
    • 体积小: 仅有12KB
  • 缺点
    • 兼容性差一点: 需要环境支持 Proxy 和 defineProperty,否则无法使用
    • 运行效率不稳定: 运行效率受到环境因素影响较大,对于不支持 proxy 的浏览器使用 defineProperty 实现,在性能上为 proxy 的两倍

Immutable

  • 优点
    • 兼容性好: 支持编译为 ES3 代码,适合所有 JS 环境
    • 运行效率稳定: 效率整体来说比较平稳,但是在转化过程中要先执行 fromJS 和 toJS,所以需要一部分前期效率的成本
  • 缺点
    • 上手难一点: API 丰富,上手复杂的多,使用 immutable 做项目迁移或者改造起来会稍微复杂一些
    • 体积大一点: 库的体积大约在 63KB
    • JavaScript 的数据类型和 Immutable 需要相互转换,有入侵性,开发中需要时刻关注操作的是原生对象还是 Immutable对象 返回的结果

参考

官网