In the age of hooks on React, MOBx is as simple as ever, easy to learn and use. I have been reading mobx source code for a while, and I plan to publish a new version of the source code

1. Pre-api knowledge

The current version of Mobx is in the 6.3+ era, there are still a lot of API changes, and mobx has done a small refactoring due to the decorator proposal changes. Since version 5, Mobx has been using the Proxy API to listen. Here’s a brief introduction to Proxy

1.1 the Proxy agent

let proxy = new Proxy(target, handler)
Copy the code
  • target— is the object to wrap, which can be anything, including functions.
  • handlerProxy configuration: Objects with traps (methods of intercepting operations). Such asgetThe catcher is used for readingtargetThe properties of thesetThe catcher is used for writingtargetAnd so on.

Operate on the proxy, and if a corresponding catcher exists in handler, it will run and the Proxy will have a chance to process it, otherwise target will be processed directly.

For most operations on objects, there is a so-called “inner method” in the JavaScript specification that describes how the lowest level of operations work. For example, [[Get]], internal methods for reading properties, [[Set]], internal methods for writing properties, and so on. These methods are only used in the specification and we cannot call them directly by method name.

For each internal method, there is a catcher in this table: method names that can be added to the handler parameter of the new Proxy to intercept operations:

Internal methods Handler method When the trigger
[[Get]] get Reads the properties
[[Set]] set Write attributes
[[HasProperty]] has inThe operator
[[Delete]] deleteProperty deleteThe operator
[[Call]] apply A function call
[[Construct]] construct newThe operator
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor.for.. in.Object.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames.Object.getOwnPropertySymbols.for.. in.Object/keys/values/entries

1.2 Reflect the reflection

Reflect is a built-in object that acts like a proxy cousin and can be used to simplify proxy handler creation.

The internal methods mentioned earlier, such as [[Get]] and [[Set]], are normative only and cannot be called directly.

The Reflect object makes it possible to call these internal methods. Its methods are minimal wrappers of internal methods.

Here is an example of the same action and Reflect call:

operation Reflectcall Internal methods
obj[prop] Reflect.get(obj, prop) [[Get]]
obj[prop] = value Reflect.set(obj, prop, value) [[Set]]
delete obj[prop] Reflect.deleteProperty(obj, prop) [[Delete]]
new F(value) Reflect.construct(F, value) [[Construct]]

Let’s do it:

  let proxyObj = new Proxy(obj, {
  
    get(target, propKey, receiver) {
      
      console.log(`GET ${propKey}`);
      
      return Reflect.get(target, propKey, receiver);
    },
    
    set(target, propKey, value, receiver) {
      
      console.log(`SET ${propKey} = ${value}`);
      
      return Reflect.set(target, propKey, value, receiver); }})Copy the code

2. Related design patterns

The publish-subscribe mode is similar to the Observer mode, but mobx’s internal implementation is more complex and more like publish-subscribe mode, with an additional intermediary to communicate with.

In the simulation below we use the Observer mode to simplify the process of creating a Mobx Observer.

The observer pattern defines a one-to-many dependency that allows multiple observer objects to listen to a target object at the same time. When the state of the target object changes, all observer objects are notified so that they can update automatically.

Observer mode has two roles to complete the process:

  • The target
  • The observer

The simple process is as follows:

Target <=> observer, observer observes target (listening target) -> Target changes -> Target actively notifies observer.

3. The implementation

This time we’ll simply implement a core API, makeObservable, formerly @Observable;

Create a function makeObservable(Target) that “makes the object observable” by returning a proxy.

/ / sample

let user = {};

user = makeObservable(user); // It can be observed after packaging

user.observe((key, value) = > {  // Add the trigger method
    alert(`SET ${key}=${value}`); 
}); 

user.name = "John"; // Changing attributes triggers alerts: SET name=John

Copy the code

3.1 Solutions

The makeObservable returns an object just like the original, but with the observe(handler) method, which sets the handler function to be called when any property is changed.

Whenever a property is changed, the handler(key, value) function is called with the name and value of the property.

The solution consists of two parts:

  1. Whenever.observe(handler) is called, we need to remember handler somewhere so that we can call it later. We can use Symbol as the property key to store handlers directly in the object.

  2. We need a proxy with a set catcher to call handler when any changes occur.

3.2 Actual code

let handlers = Symbol('handlers'); // Take a globally unique attribute to prevent overwriting

function makeObservable(target) {
  // 1. Initialize handler storage
  target[handlers] = [];

  // Store the handler function in an array for later invocation
  target.observe = function(handler) {
    this[handlers].push(handler);
  };

  // 2. Create a proxy to handle changes
  return new Proxy(target, {
    set(target, property, value, receiver) {
      let success = Reflect.set(... arguments);// Forward the operation to the object
      if (success) { // If there is no error when setting the property
        // Call all handlers
        target[handlers].forEach(handler= > handler(property, value));
      }
      returnsuccess; }}); }Copy the code