理解不可变状态Immer.js

2,213 阅读4分钟

以前经常听到js不可变状态,傻乎乎不理解是什么意思。最近刚还有时间,基于Immer.js 系统学习了一下不可变状态,总结一下与大家分享。

先来看看什么叫可变状态

let objA = { name: '爸爸' };
let objB = objA;
objB.name = '妈妈';
console.log(objA.name); // objA 的name也变成了妈妈

代码解析: 我们明明只修改代码objB的name,发现objA也发生了改变。这个就是可变状态。

可变状态的缺点:

  1. 间接修改了其他对象的值,在复杂的代码逻辑中,会造成代码隐患。

解决方案:

  • 深度拷贝
  • JSON.parse(JSON.stringify(objA)) 。 我常用的方案
  • immutable-js

不可变状态immer.js

Immer 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。

案例分析:

const {produce} = require('immer')
// const {produce} = require('./immer')
let baseState = {
  home:{name:'爸爸',arr:[1]},
  b:{}
}

let nextState = produce(baseState, (draft) => {
    draft.home.name = '妈妈';
})
console.log(baseState.home === nextState.home); // false
console.log(baseState.home.arr === nextState.home.arr) // true
console.log(baseState.b === nextState.b); // true

特点:

  1. 子节点被修改,那么父节点,或者父父节点被重新创建。
  2. 兄弟节点或者其他与修改节点无关的节点,兄弟节点是被复用的节点,不会重新创建。

其形成特点如下: immergraph.gif

immer.js 原理讲解

前置知识proxy的讲解

let baseState = {
  home:{name:'爸爸',arr:[1]},
  b:{}
}
let p = new Proxy(baseState,{
  get(tartget, key) {
      console.log('get', tartget, key)
      return tartget[key]
  },
  set(tartget,key,value){
    console.log('set', tartget, key, value)
    tartget[key]=value
  }
})
p.home = 'mama1'
// 打印结果
// set { home: { name: '爸爸', arr: [ 1 ] }, b: {} } home mama1
p.home.name = 'mama1'
// 打印结果
// get { home: { name: '爸爸', arr: [ 1 ] }, b: {} } home

结论:

  1. 赋值的时候会触发,set的方法。
  2. proxy 不会深度代码,只代理一层,如果想深度代理的话,需要递归代理。

immer 源码实现流程

  1. 辅助方法。
var is = {
  isObject: (val) => Object.prototype.toString.call(val) === '[object Object]',
  isArray: (val) => Array.isArray(val),
  isFunction: (val) => typeof val === 'function'
}
  1. 对象进行浅复制。用于产生草稿对象。
// 浅复制
function createDraftState(baseState) {
  if (is.isArray(baseState)) {
    return [...baseState];
  } else if (is.isObject(baseState)) {
    return Object.assign({}, baseState);
  } else {
    return baseState;
  }
}
  1. 对象进行代理,并增加标识位,用户判断是否发生修改。
 var internal = {
    draftState: createDraftState(baseState),
    mutated: false
  }
  1. set 方法实现
function set(target, key, value) {
      internal.mutated = true;
      let {draftState}= internal;
      draftState[key] = value;
      valueChange && valueChange()
      return true
}
  1. get 方法实现
function get(target, key) {
      if (key === INTERNAL) {
        return internal
      }
      const value = target[key];
      if (is.isObject(value) || is.isArray(value)) {
        if (key in keyToProxy){
            return keyToProxy[key];
        }else{
          keyToProxy[key] = toProxy(value,()=>{
            internal.mutated = true;
            const proxyOfChild = keyToProxy[key];
            var {draftState} = proxyOfChild[INTERNAL];
            internal.draftState[key] = draftState;
            valueChange && valueChange()
          })
          return keyToProxy[key];
        }
      }

      return internal.mutated ? internal.draftState[key] : baseState[key];
 }

思路梳理完毕,接下来组装所有代码。

var is = {
  isObject: (val) => Object.prototype.toString.call(val) === '[object Object]',
  isArray: (val) => Array.isArray(val),
  isFunction: (val) => typeof val === 'function'
}
// 定义一个唯一变量
const INTERNAL = Symbol('INTERNAL');

// 对外暴露的核心方法
function produce(baseState, producer) {
  // 代理传入的初始对象
  const proxy = toProxy(baseState);

  producer(proxy) // 将代理对象
  const internal = proxy[INTERNAL];
  return internal.mutated ? internal.draftState : baseState;
}

function toProxy(baseState,valueChange) {
  var keyToProxy = {};
  var internal = {
    draftState: createDraftState(baseState), // 创建一个草稿对象
    mutated: false // 标记位,标记节点是否修改了。
  }
  return new Proxy(baseState, {
    get(target, key) {
      if (key === INTERNAL) { // 用户获取草稿对象
        return internal
      }
      const value = target[key];
      // 判断是不是,对象或者数组,如果是,则进行递归代理
      if (is.isObject(value) || is.isArray(value)) {
        // 如果已经代理过了,就不进行代理了,直接返回。
        if (key in keyToProxy){
            return keyToProxy[key];
        }else{
          // 递归代理
          keyToProxy[key] = toProxy(value,()=>{//子节点发生变化,通知父节点的回掉函数
            // 子节点,改变了,父节点也要标记改变了。
            internal.mutated = true;
            const proxyOfChild = keyToProxy[key];
            // 拿到子节点的草稿对象,
            var {draftState} = proxyOfChild[INTERNAL];
            // 父节点执行,字节点的草稿对象。
            internal.draftState[key] = draftState;
            // 然后在通知父节点。
            valueChange && valueChange()
          })
          return keyToProxy[key];
        }
      }
			// 如果没有发生修改,则返回原始对象,修改了,则返回草稿对象
      return internal.mutated ? internal.draftState[key] : baseState[key];
    },
    set(target, key, value) {
      // 进行复制操作毕竟发生了修改。
      internal.mutated = true;
      // 如果发生了修改,不能修改原始对象,只能修改草稿对象。
      let {draftState}= internal;
      // 修改草稿对象
      draftState[key] = value;
      //通知父节点进行父节点变更。
      valueChange && valueChange()
      return true
    }
  })
}

// 浅复制
function createDraftState(baseState) {
  if (is.isArray(baseState)) {
    return [...baseState];
  } else if (is.isObject(baseState)) {
    return Object.assign({}, baseState);
  } else {
    return baseState;
  }
}

module.exports = {
  produce
}

代码已经实现,但是感觉不是特别好理解。我这里举一个生活动比较常见的例子。希望能帮助大家理解。

小A,有一个父亲,有一个爷爷。 有一天小b告诉小a,你不是你爸爸亲生的(相当于修改了小a)。这个时候小a去找它新的爸爸,找到新的爸爸后,新爸爸带它去见新的页面,重新组建了一个新的家庭关系。(上诉代码就是这个实现逻辑)。