Immer简单解析

immer基础介绍
github主页 https://github.com/immerjs/immer
文档 https://immerjs.github.io/immer

为什么immer让人眼前一亮

immer是一个简单易用的immutable结构生成库,在react的生态里,需要通过immutable来驱动组件更新,然而immutable数据生成的操作较为繁琐。

const state = {
    a: {
        x: 1,
        y: 2,
    },
    b: {
        x: 2,
        y: 4,
    }.
};

// 将a的x改为2 
const newState = {
    ...state,
    a: {
        ...state[a],
        x: 2,
    },
};

相比mutable的操作,immutable的操作就繁琐得多,数据结构复杂起来,可能出更多的问题。immer却能用mutable的操作生成immutable的数据。

import { produce } from 'immer';

const newState = produce(state, draft => {
    draft.a.x = 2;
});

简单又优雅,棒~

其他解决方案——immutablejs、deepClone

没有immer前,有两个比较流行的方案immutablejs、deepClone。先说deepClone吧,就是把对象深度拷贝一遍。优点是简单粗暴,缺点是性能差,同时原本数据没变的组件也会重新渲染。immer的更新则只改变更新的部分结构。

image

immutable.js则是一套比较重的方案,体积大概20+kb,有自己的一套api。简单来说,就是先把数据转化为自己的类型对象,再通过一系列api进行操作。优点是性能高、api操作高效(这里指代码短),缺点是代码侵入性强、重、对ts支持不够完善。

const { Map } = require('immutable');
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = Map({ a: 1, b: 2, c: 3 });
map1.equals(map2); // true
map1 === map2; // false

总结

immer简直完美。

immer的两板斧——produce的两种用法

immer也有一些高级的玩法,比如脏值记录,但produce的两种玩法足够覆盖99%的需求了。

最基础的玩法就是直接生成新的对象

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
})

没有改过的对象是不变的

// the new item is only added to the next state,
// base state is unmodified
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)

// same for the changed 'done' prop
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)

// unchanged data is structurally shared
expect(nextState[0]).toBe(baseState[0])
// changed data not (dûh)
expect(nextState[1]).not.toBe(baseState[1])

柯里化

produce函数支持柯里化。

import produce from "immer"

const INITIAL_STATE = {
    x: 3,
    y: 4,
}

const precess = producer(draft => {
    draft.x = draft.x + 1;
});

甚至可以直接变成reducer

import produce from "immer"

// Reducer with initial state
const INITIAL_STATE = {}

const byId = produce((draft, action) => {
 switch (action.type) {
 case RECEIVE_PRODUCTS:
            action.products.forEach(product => {
                draft[product.id] = product
            })
 break
    }
}, INITIAL_STATE)

immer源码解析

原理解析

produce函数的逻辑很简单,不考虑柯里化可以看作三个步骤:生成draft对象,对draft对象进行修改操作,生成新的结果。

function produce(baseState, recipe) {
    const draft = createProxy(baseState);
    recipe(draft);
    const result = finalize(draft);
}

其中关键在于生成的draft对象,draft通过拦截取值操作和赋值操作将变化前后状态都记录下来。
image

finalize是一个递归的过程,如果modified属性没变返回原对象,否则遍历所有子属性,如果是值类型返回改变后的值,如果是对象重复上面步骤。最后会生成一个最小变化的新对象。

找了个demo,便于理解

Demo 仅用作理解原理,不支持监听数组

function produce (base, producer) {
  const baseProxy = createProxy(undefined, base) /* 创建代理 */
   producer.call(baseProxy, baseProxy) /* 执行 mutable 操作 */
   return finalize(baseProxy) /* 递归合成新对象 */
}

function createState (parent, base) { /* base 对象代理 */
  return {
    modified: false, /* 表示该对象是否已经被更改 */
    base, /* 原始对象*/
    parent,
    copy: undefined, /* 对原始对象的改动最终都会在这里保存一份 */
    proxies: {} /* 存储对象子节点的代理 */
  }
}

function markChanged (state) {
  state.modified = true;
  state.copy = Object.assign(Object.create(null), state.base)
  Object.assign(state.copy, state.proxies)
  if (state.parent) markChanged(state.parent)
}

const _STATE_ = '_______'

function createProxy (parent, base) {
  const state = createState(parent, base)
  const proxy = Proxy.revocable(state, {
    get: function (state, prop) {
      if (prop === _STATE_) return state /* 如果能通过 _STATE_ 拿到数据,表明它是一个 proxy */
      if (state.modified) {
        const value = state.copy[prop]
        /* 只有 base 的才有必要去 proxy,否则如果是新增的的对象则直接返回 */
        if (value === state.base[prop] && toString.call(value) === '[object Object]') {
          state.copy[prop] = createProxy(state, value)
          return state.copy[prop]
        }
        return value
      } else {
        /* 如果没有改动过数据,那仅仅是创建一个代理 */
        if (state.proxies[prop]) return state.proxies[prop]
        const value = state.base[prop]
        if (toString.call(value) === '[object Object]') {
          state.proxies[prop] = createProxy(state, value)
          return state.proxies[prop]
        }
        return value
      }
    },
    set: function (state, prop, value) {
      if (!state.modified) {
        markChanged(state)
      }
      state.copy[prop] = value /* 修改对象都在 copy 上执行 */
    }
  })
  return proxy.proxy
}

function finalize(rootProxy) {
  if (!!rootProxy && rootProxy[_STATE_]) {
    const state = rootProxy[_STATE_]
    for (let key in state.copy) {
      if (state.copy[key] !== state.base[key]) {
        state.copy[key] = finalize(state.copy[key])
      }
    }
    return state.copy
  } else {
    return rootProxy
  }
}

const a = { b: 1 }
const c = { d: 2 }
const state = {
  a: a,
  c: c,
  d: {
    c: { e: 4 },
    f: 2
  }
}
var state2 = produce(state, draft => {
  draft.a.b = 21222
  draft.a.f = 3
})
console.log(state2.d === state.d, true)
console.log(state2 === state, false)
console.log(state2.a.f, 3)
console.log(state.a.b, 1)
console.log(state2.a.b, 21222)

posted @ 2022-09-07 18:36  Tommy_marc  阅读(544)  评论(0编辑  收藏  举报