This week’s storehouse of intensive reading is immer.

1 the introduction

Immer is one of the most recent projects to take off, developed by Mobx author Mweststrate.

For those of you who know Mobx, Immer is a lower-level Mobx that takes mobX’s features and makes it very elegant to use when combined with any data flow framework.

2 an overview

The trouble Immutable

The problem Immer is trying to solve is to use metaprogramming to simplify the complexity of using Immutable. For example, let’s write a pure function:

const addProducts = products= > {
  const cloneProducts = products.slice()
  cloneProducts.push({ text: "shoes" })
  return cloneProducts
}
Copy the code

The code isn’t complicated, but it still hurts to write. We must make a copy of Products, call push to modify the new cloneProducts, and return it.

If JS native supports Immutable, you can use push directly! Yes, Immer now has JS support for:

const addProducts = produce(products= > {
  products.push({ text: "shoes"})})Copy the code

Isn’t it interesting that the two addProducts functions are exactly the same and both are pure functions?

Awkward setState

As we all know, the React framework supports setState as a function:

this.setState(state= > ({
  ...state,
  isShow: true
}))
Copy the code

With deconstructed grammar, it’s still so elegant to write. What about a little more complicated data? We are to endure bad Immutable in silence:

this.setState(state= > {
  const cloneProducts = state.products.slice()
  cloneProducts.push({ text: "shoes" })
  return {
    ...state,
    cloneProducts
  }
})
Copy the code

With Immer, however, things are different:

this.setState(produce(state= > (state.isShow = true)))

this.setState(produce(state= > state.products.push({ text: "shoes" })))
Copy the code

Convenient Currization

The benefits of Immer support for Currization are described above. Therefore, we can also directly consume the two parameters in a lump sum:

const oldObj = { value: 1 }
const newObj = produce(oldObj, draft= > (draft.value = 2))
Copy the code

This is Immer: Create the next immutable state by mutating the current one.

3 intensive reading

Although the author has done some research in this area before, such as making a Mutable to Immutable library: Dob-redux, but Immer is really too amazing, Immer is the lower layer of the puzzle, it can be inserted into any data flow framework as a function enhancement, have to admire Mweststrate is really very promising.

So I read its source code carefully, take you to understand Immer from the perspective of principle.

Immer is a tool that supports Currization and only synchronous calculation, so it is ideally suited to reducer use as redux.

Immer also supports direct return value, which is relatively simple, so we’ll skip all return value processing in this article. It is not possible to return different objects at the same time, otherwise it is not clear which modifications are valid.

Currization is not covered here. See Curry for more details. Let’s look at the produce callback section:

produce(obj, draft= > {
  draft.count++
})
Copy the code

Obj is a normal object, so dark magic must appear on the Draft object. Immer listens for all properties of the Draft object.

So here’s the whole idea:draftobjAgent, rightdraftMutable changes flow into customizationssetterFunction, which does not modify the value of the original object, but returns a new top-level object by iterating through the parentproduceThe return value of the function.

Generated proxy

In the first step, converting OBj to draft, we need some additional information to make Immutable efficient, so we wrap OBj into a proxy object that contains the additional information:

{
  modified, // Whether it has been modified
  finalized, // Is it done (all setters are done and a copy has been generated)
  parent, // Parent object
  base, // The original object (obj)
  copy, // Shallow copy of base (obj) using object.assign (object.create (null), obj)
  proxies, // Stores proxy objects for each propertyKey, using a lazy initialization strategy
}
Copy the code

On this proxy object, you bind your custom getter setter and throw it directly to Produce for execution.

getter

The produce callback contains the mutable code for the user. So now the entry becomes a getter and setter.

Getters are used to lazily initialize proxy objects, that is, proxy objects are generated when their child properties are accessed.

For example, here is the original obj:

{
  a: {},
  b: {},
  c: {}
}
Copy the code

In the initial case, draft is the proxy of OBj. Therefore, when accessing draft.a draft.b draft.c, the getter setter is triggered to enter the custom processing logic. You cannot listen on draft.a.x because the agent can only listen on one layer.

Proxy lazy initialization is intended to solve this problem. When draft.a is accessed, the custom getter has secretly generated a new draftA proxy for draft.a objects, so draft.a.x accesses drafta. x, So you can recursively listen for all the properties of an object.

Also, if only draft.a is accessed in the code, then only draftA agents are generated in memory, and the b and C properties are not accessed, so there is no need to waste resources to generate draftB draftC agents.

Of course, Immer has made some performance optimizations, as well as obtaining a copy of the object after it has been modified. In order to ensure that the base is immutable, it is not expanded here.

setter

When draft is modified, a shallow copy of the base, or original value, is made and saved to the Copy property with the Modified property set to true. This accomplishes the most important Immutable process, and shallow copy is not very performance-intensive, plus it is shallow copy on demand, so Immer performance is ok.

In addition, to ensure that all objects on the whole link are new objects, the system uses theparentProperty recursive parent, shallow copy, until the entire link object from the leaf node to the root node is updated.

When the modified object is completed and the property is modified, the new value is saved to the Copy object.

Generate an Immutable object

When produce is executed, all of the user’s modifications are complete (so Immer does not support async). If the modified property is false, the user did not modify the object at all, so simply return the original Base property.

If the modified property is true, the object has been modified and the copy property is returned. But the setter process is recursive, and the children of Draft are also draft (proxies that contain additional properties like Base Copy Modified), so we have to recurse layer by layer to get the real value.

In this stage, all Draft 1600s are false. A large number of draft attributes may also exist in copy, so recursive base and copy’s subattributes will be returned if they are identical. If not, recurse the entire process once (starting from the first line of this section).

The final object returned is a final concatenation of some properties of base (the unmodified part) and some properties of copy (the modified part). Finally, use freeze to freeze the Copy property and set the King of France property to true.

At this point, the return value is generated, and we save the final value on the copy property and freeze it, returning Immutable values.

Immer thus achieves the unthinkable: Create the next immutable state by mutating the current one.

Here, Immer can actually support asynchracy, as long as produce returns promises. The biggest problem is that the final revoke cleaning of the proxy requires global variables, which hinders Immer’s support for async.

4 summarizes

Redux-box resolves the reducer redundant return problem using immer + redux.

At the same time, we also started to think and design new data flow framework. The author will share “MVVM Front-end Data Flow Framework Introduction” in Ctrip Technology Salon on March 24, 2018, and share the research experience of various data flow technology schemes emerging in recent years. Interested students are welcome to register and participate.

5 More Discussions

Close read immer.js source code · Issue #68 · dt-fe/weekly

If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released every Friday.