Immer.js使用教程

1,627 阅读4分钟

一、引言

immer.js是mobx作者写的一个不可变数据库,核心是使用了ES6的Proxy和ES5的defineProperty,它的基本思想是,将所有更改都应用于临时的draftState,它是currentState的代理。一旦完成所有变更,Immer将基于到draftState的变更产生nextState。这意味着可以通过简单地修改数据而与数据进行交互,同时保留不可变数据的所有优点(避免一处变更处处变更、节省内存空间)。

二、概述

1. 安装immer

// yarn

yarn add immer

// npm

npm install immer

为确保Immer尽可能小,需选择加入每个项目不需要的功能,并且必须明确启用这些功能。这样可以确保未使用的功能不会占用任何空间。(请确认immer版本大于6)

支持 ES5如果需要能够在较旧的JavaScript环境(例如Internet Explorer或React Native)上运行,请启用此功能enableES5()
支持 ES6的Map和Set如果要使Immer可以在原生 Map和Set上运行,请启用此功能enableMapSet()
支持JSON Patch在producer运行期间,Immer可以记录所有做更改的patches。如果要更改重朔到原始状态,请启用此功能。enablePatches()
支持所有功能enableAllPlugins()
 // 在项目的入口文件index.js



import {enableES5, enableMapSet} from "immer"

enableES5()

enableMapSet()

2.基础api介绍

1.produce: (currentState: T, producer: (draftState: T) => void): nextState

import produce from "immer"


const baseState = [

    {

        todo: "Learn typescript",

        done: true

    },

    {

        todo: "Try immer",

        done: false

    }

]

const nextState = produce(baseState, draftState => {

    draftState.push({todo: "Tweet about it"})

    draftState[1].done = true

})

console.log(nextState[1].done)  // true

console.log(baseState[1].done)  // false

console.log(baseState[0] === nextState[0])  // true

注:由produce产生的nextState会被自动冻结,不可直接修改,想关闭这个功能可以使用setAutoFreeze(false)

producer的返回值

producer 是否有返回值,nextState 的生成过程是不同的:

producer 没有返回值时:nextState 是根据函数内的 draftState 生成的;

producer 有返回值时:nextState 是根据函数的返回值生成的;

import produce from "immer"

const baseState = [

    {

        todo: "Learn typescript",

        done: true

    },

    {

        todo: "Try immer",

        done: false

    }

]

const nextState = produce(baseState, draftState => {

   return [{

     todo: "Try immer",

   }]

})

console.log(nextState)  // [{todo: "Try immer"}]

如果想要返回undefined,可以使用nothing

import produce, {nothing} from "immer"    
const state = {      
    hello: "world"  
}   // Produces a new state, 'undefined'  
produce(state, draft => nothing) 

2.柯里化produce

// 将函数作为第一个参数传递给produce,这样就会得到一个预绑定的producer : produce((state, index) => state),producer接收的参数会被传递给该函数  
const producer = produce((draft, index) => {      
    draft.index = index  
})   // example usage  
console.dir([{}, {}, {}].map(producer)) //[{index: 0}, {index: 1}, {index: 2}])    
// 可以给producer传递第二个参数作为初始状态值  
const byId = produce(      
    (draft, action) => {          
        switch (action.type) {              
            case RECEIVE_PRODUCTS:
                action.products.forEach(product => {
                    draft[product.id] = product                  
                })
                return
            }      
        },
        {        
            1: {id: 1, name: "product-1"}      
        }
    ) 


优化reducer

// 没用Immer
const reducer = (state, action) => {      
    switch (action.type) {          
        case RECEIVE_PRODUCTS:              
            return {                  
                ...state,                 
                ...action.products.reduce((obj, product) => {
                    obj[product.id] = product                      
                        return obj                  
                    }, 
                {})              
            }
        default:              
            return state      
    }  
}
// 用Immer  
const reducer = (state, action) => produce((state, action) => {      
    switch (action.type) {          
        case RECEIVE_PRODUCTS:             
            action.products.forEach(product => {
                draft[product.id] = product             
            })
         }
         default:
             return
     }) 

优化setState

// 没用Immer 
this.setState(state => ({     
    x: {          
        ...x,         
        y: 1,      
    }  
}))    
// 用immer
this.setState(produce(state => {      
    state.x.y = 1  
})) 

3.original和current

original 和 current会拷贝draft的原始状态和当前状态并缓存下来,由original和current生成的状态不会被自动冻结

import {current, original} from 'immer'    
const base = {      
    x: 0  
}  
const next = produce(base, draft => {     
    draft.x++      
    const orig = original(draft)      
    const copy = current(draft)      
    console.log(orig.x)      
    console.log(copy.x)      
    setTimeout(() => {         
        // this will execute after the produce has finised!
        console.log(orig.x)         
        console.log(copy.x)      
    }, 100)      
    draft.x++      
    console.log(draft.x) 
})  
console.log(next.x)   
// This will print  
// 0 (orig.x)  
// 1 (copy.x)  
// 2 (draft.x)   
// 2 (next.x)  
// 0 (after timeout, orig.x)  
// 1 (after timeout, copy.x) 

4.patches 补丁功能

通过此功能,可以方便进行详细的代码调试和跟踪,可以知道 producer 内的做的每次修改。注:需要在入口文件开启enablePatches()

// patch对象

interface Patch {
  op: "replace" | "remove" | "add" // 一次更改的动作类型
  path: (string | number)[] // 此属性指从树根到被更改树杈的路径
  value?: any // op为 replace、add 时,才有此属性,表示新的赋值
}
 // 通过 patchListener 函数,暴露正向和反向的补丁数组
produce(
  currentState, 
  producer,
  patchListener: (patches: Patch[], inversePatches: Patch[]) => void
)

// 应用补丁功能
applyPatches(currentState, changes: (patches | inversePatches)[]): nextState

例子:

import produce, { applyPatches, Patch } from 'immer'    
interface State {    
    readonly x: number    
    y?: number  
}  
const state: State = {   
    x: 0,  
}   
let replace: Patch[] = []  
let inverseReplaces: Patch[] = []  
let newState = produce(      
    state,      
    draft => {        
        // `x` can be modified here        
        draft.x += 1
        draft.y = 2     
    },      
    (patches, inversePatches) => {      
        replace = patches        
        inverseReplaces = inversePatches      
    }  
)  
console.log('state1', newState) 
// { x: 1, y: 2 }  
newState = applyPatches(newState, inverseReplaces)  
console.log('state2', newState) 
// {x: 0}  
console.log(replace, inverseReplaces)  
// patches   
[    
    {      
        op: "replace",      
        path: ["x"],      
        value: 1    
    },    
    {      
        op: "add",     
        path: ["y"],    
        value: 2    
    }, 
]  
// inversePatches  
[    
    {
        op: "replace",  
        path: ["x"],   
        value: 0    
    },   
    {    
        op: "remove",    
        path: ["y"]    
    },  
 ] 

3.use-immer: Immer 同时提供了一个 React hook 库 use-immer 用于以 hook 方式使用 immer

// 更新时将函数作为参数  
function App() {    
    const [person, updatePerson] = useImmer({      
        name: "Michel",     
        age: 33    
    });     
    function updateName(name) {     
        updatePerson(draft => {        
            draft.name = name;      
        });
    }
}
// 接受值作为参数,类似useState  
function App(){   
    const [age, setAge] = useImmer(20);      
    function birthDay(event){     
        setAge(age + 1);    
    }
} 

三、对比immutable.js

immer.js相较于immutable.js的优点

1: 体积更小

2: 语法更简单,不需要重新学习新的语法,这很容易和原生Map的语法混淆

3: 不需要再使用toJS()将数据转换为js格式,有时候忘记这一步会引入bug