Js immutable state is an immutable state. Recently, I have time to learn immutable state based on immer.js system. I will summarize and share with you.

So what is mutable state

let objA = { name: 'daddy' };
let objB = objA;
objB.name = 'mother';
console.log(objA.name); // objA's name also becomes mother
Copy the code

Code parsing: We changed objB’s name and objA changed too. This is the mutable state.

Disadvantages of mutable state:

  1. Indirectly modifying the values of other objects can cause code hazards in complex code logic.

Solution:

  • Deep copy
  • JSON. Parse (JSON. Stringify (objA)). My usual solution
  • immutable-js

Immutable state immer.js

Immer is an IMmutable library written by the author of Mobx. The core implementation is to use ES6 proxy to implement JS immutable data structure with minimal cost. It is easy to use, small size, ingenious design, and meets our needs for JS immutable data structure.

Case Study:

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

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

Copy the code

Features:

  1. When the child node is modified, the parent node, or parent node, is recreated.
  2. Sibling nodes or other nodes unrelated to the modified node. Sibling nodes are reused and will not be recreated.

Its formation characteristics are as follows:

Immer.js principle explanation

Pre-knowledge proxy explanation

let baseState = {
  home: {name:'daddy'.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'
// Print the result
// set {home: {name: 'dad ', arr: [1]}, b: {}} home mama1
p.home.name = 'mama1'
// Print the result
/ / get {home: {name: 'father, arr: [1]}, b: {}} home
Copy the code

Conclusion:

  1. When you assign, it fires the set method.
  2. Proxy does not deep code, only one layer of proxy, if you want to deep proxy, you need a recursive proxy.

Immer source code implementation process

  1. Auxiliary methods.
var is = {
  isObject: (val) = > Object.prototype.toString.call(val) === '[object Object]'.isArray: (val) = > Array.isArray(val),
  isFunction: (val) = > typeof val === 'function'
}
Copy the code
  1. Object for shallow copy. Used to produce draft objects.
/ / shallow copy
function createDraftState(baseState) {
  if (is.isArray(baseState)) {
    return [...baseState];
  } else if (is.isObject(baseState)) {
    return Object.assign({}, baseState);
  } else {
    returnbaseState; }}Copy the code
  1. Object to proxy and add identifier bits, and the user determines whether changes have occurred.
 var internal = {
    draftState: createDraftState(baseState),
    mutated: false
  }
Copy the code
  1. Set method implementation
function set(target, key, value) {
      internal.mutated = true;
      let {draftState}= internal;
      draftState[key] = value;
      valueChange && valueChange()
      return true
}
Copy the code
  1. Get method implementation
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()
          })
          returnkeyToProxy[key]; }}return internal.mutated ? internal.draftState[key] : baseState[key];
 }
Copy the code

With that thought sorted out, let’s assemble all the code.

var is = {
  isObject: (val) = > Object.prototype.toString.call(val) === '[object Object]'.isArray: (val) = > Array.isArray(val),
  isFunction: (val) = > typeof val === 'function'
}
// Define a unique variable
const INTERNAL = Symbol('INTERNAL');

// The core method of exposure
function produce(baseState, producer) {
  // The initial object passed in by the proxy
  const proxy = toProxy(baseState);

  producer(proxy) // Will proxy the object
  const internal = proxy[INTERNAL];
  return internal.mutated ? internal.draftState : baseState;
}

function toProxy(baseState,valueChange) {
  var keyToProxy = {};
  var internal = {
    draftState: createDraftState(baseState), // Create a draft object
    mutated: false // Flag bit to indicate whether the node has been modified.
  }
  return new Proxy(baseState, {
    get(target, key) {
      if (key === INTERNAL) { // The user gets the draft object
        return internal
      }
      const value = target[key];
      // Determine if it is an object or an array, and if so, recursively proxy it
      if (is.isObject(value) || is.isArray(value)) {
        // If the proxy has already been used, the proxy is not performed, and directly returns.
        if (key in keyToProxy){
            return keyToProxy[key];
        }else{
          // Recursive proxy
          keyToProxy[key] = toProxy(value,() = >{// When the child node changes, notify the parent node's callback function
            // The child node is marked as changed, and the parent node is marked as changed.
            internal.mutated = true;
            const proxyOfChild = keyToProxy[key];
            // Get the draft object of the child node,
            var {draftState} = proxyOfChild[INTERNAL];
            // The parent node executes the draft object of the byte point.
            internal.draftState[key] = draftState;
            // Then notify the parent node.
            valueChange && valueChange()
          })
          returnkeyToProxy[key]; }}// If no changes are made, the original object is returned, and if changes are made, the draft object is returned
      return internal.mutated ? internal.draftState[key] : baseState[key];
    },
    set(target, key, value) {
      // Make a copy after all.
      internal.mutated = true;
      // If a change occurs, the original object cannot be modified, only the draft object can be modified.
      let {draftState}= internal;
      // Modify the draft object
      draftState[key] = value;
      // Notify the parent of the parent change.
      valueChange && valueChange()
      return true}})}/ / shallow copy
function createDraftState(baseState) {
  if (is.isArray(baseState)) {
    return [...baseState];
  } else if (is.isObject(baseState)) {
    return Object.assign({}, baseState);
  } else {
    returnbaseState; }}module.exports = {
  produce
}
Copy the code

The code has been implemented, but it doesn’t feel particularly easy to understand. Let me give you a common example of a life activity. Hopefully that helps you understand.

“A” has A father and A grandfather. One day little B told little A that you are not your father’s biological child. At this time, little A went to find its new father. After finding the new father, the new father took it to the new page and re-established a new family relationship. The appeal code is the implementation logic.