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.handler
Proxy configuration: Objects with traps (methods of intercepting operations). Such asget
The catcher is used for readingtarget
The properties of theset
The catcher is used for writingtarget
And 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 |
in The operator |
[[Delete]] |
deleteProperty |
delete The operator |
[[Call]] |
apply |
A function call |
[[Construct]] |
construct |
new The 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 | Reflect call |
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:
-
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.
-
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