Implementation of proxy mode for Vue data response system
1. Prepare tools
The required environment is as follows:
- Node environment (Babel)
- TypeScript
Knowledge required:
- Introduction to ES6 Standards
2. The train of thought
2.1 Overall Structure
The overall structure of this practice is a Watcher implementation class that simulates Vue and inputs data, render, and mounted DOM nodes that need to be responded to. The render function is called when the data attribute is changed (provided that the modified data is used in the render function).
-
Structure of the Watcher class
class Watcher { // Render function array, a data may exist in more than one render function, there may be multiple render function calls renderList: Array<Function>; / / data data: any; // Mount the EL element el: String | HTMLElement; } Copy the code
Above is the structure of the Watcher class, which automatically monitors data, rendering functions, and DOM elements once they are passed in.
-
Proxy tool implementation
- Add to the object to be observed
notifySet
, it is aSet
. This collection holds which attributes are observed and, if observed, what is done about themsetter
When called, the render function is triggered to render. - Replacing the observed object with the proxy object works the same way, but with an extra layer of proxy, which is what the proxy pattern does.
- This project uses depth observation by default, but there could be one more
flag
To achieve depth observation.
- Add to the object to be observed
-
Agent thinking
- right
getter
andsetter
I’m going to rewrite it ingetter
When determining dependencies (because inrender
Function is used, so this dependency should be monitored), insetter
(The purpose of this article is to update the render content when the value changes) - In the
notifySet
When you add attributes, you just put the observed attributes into thisset
In, extraneous attributes are not included. The realization idea of this project is as follows:- Enable dependency mode
- Creating a proxy object
- Add dependencies by performing render functions on proxy objects as data
- Turn off dependency add mode
- right
-
The project structure
- DataBind -- core | - Proxy. Ts / / Proxy tools - utils | - utils. Ts / / universal tool Watcher. TsCopy the code
2.2 Details
-
A concrete implementation of the Watcher class
-
The constructor
interface WatcherOption { el: String | HTMLElement; // Bind an existing DOM object data: any; // Data object render: Function; // Render function } constructor(options: WatcherOptions) { if (typeof options.el === 'string') { this.el = document.getElementById(options.el); } else { // @ts-ignore this.el = options.el; } this.data = makeProxy.call(this, options.data); // Build the proxy layer by deeply traversing the entire data object this.addRender(options.render); // Add the render function to the render function array } Copy the code
The constructor passes in the options configuration, which has three important properties: mount object, data object, and render function. The specific process is as follows:
- Create a proxy object from the data and return the result to
data
attribute - Add the render function to the list
- Node mounting
- Create a proxy object from the data and return the result to
-
Rendering function Management
/** * @param fn */ public addRender(fn: Function) :void { Watcher.target = this; // Enable the proxy mode. The target object is a static variable of the Watcher class and is used in the proxy function this.renderList.push(fn); this.notify(); Watcher.target = null; // Close the proxy mode } Copy the code
Watcher. Target is a static property of Watcher. This property records the object being observed. This object will be used in the proxy. The reason for using this is: To add a dependency, the current Watcher is set to watcher. target, and then the render function is called. The render function calls the getter of the responding property, thus triggering the proxy layer to add the dependency. Because Watcher. Target is empty. This can be viewed in the makeProxy function.
So this function records the current Watcher instance, pushes the render function into the array, and then calls the render function. Dependencies are added and target is set to null.
-
-
Implementation of the proxy layer
/** * @description This is the core code for this article, because I don't have watch, computed properties, so I don't need a basket to store watcher. There would be no Dep class @param object @param this Wacther object */ export function makeProxy(this: Watcher, object: any) :any { object.__proxy__ = {}; // @ts-ignore object.__proxy__.notifySet = new Set<string | number | symbol>(); object.__watcher__ = this; // @ts-ignore let proxy = new Proxy(object, { get(target: any, p: string | number | symbol, receiver: any) :any { if(Watcher.target ! =null) { Watcher.addDep(object, p); // Add dependencies } return target[p]; }, set(target: any, p: string | number | symbol, value: any, receiver: any) :boolean { if(target[p] ! == value) {// Render the view layer only when the two values are different target[p] = value; if(target.__proxy__.notifySet.has(p)) { target.__watcher__.notify(); }}return true; }});// Get all the child attributes of the object, and recursively proxy the child attributes for deep observation let propertyNames = Object.getOwnPropertyNames(object); for (let i = 0; i < propertyNames.length; i++) { // @ts-ignore if(isPlainObject(object[propertyNames[i]]) && (! propertyNames[i].startsWith('__') && !propertyNames[i].endsWith('__'))) { object[propertyNames[i]] = makeProxy.call(this, object[propertyNames[i]]); }}return proxy; } Copy the code
There are two points of special attention to this feature, the first is the addition of object attributes and the second is the details of the proxy object.
-
Add the object attribute:
__proxy__.notifySet
: This is storageset
Instance properties, this oneset
The instance is to record which attribute is being listened on, and if the attribute is being listened on, it will be put into this collection, which attribute is being listened on conveniently__watcher__
: This is pointing to the currentwacher
Instance object.
-
Proxy object generation:
new Proxy(object, { get(target: any, p: string | number | symbol, receiver: any) :any { if(Watcher.target ! =null) { Watcher.addDep(object, p); // Add dependencies } return target[p]; }, set(target: any, p: string | number | symbol, value: any, receiver: any) :boolean { if(target[p] ! == value) {// Render the view layer only when the two values are different target[p] = value; if (target.__proxy__.notifySet.has(p)) { // Render is executed only when notifySet has this propertytarget.__watcher__.notify(); }}return true; }});Copy the code
getter
: To specify a judgment statement:
if(Watcher.target ! =null) { Watcher.addDep(object, p); // Add dependencies } Copy the code
Remember changing watcher.target when adding the render function? When this condition is not null, the attribute of the object is added to the notifySet when the render function is added, so that the callback function can be executed when the attribute is called
setter
This is explained in the code, which checks whether the attribute is added to the collection by the render function, and if so, calls the render function.
-
3. Code
Watcher
// @ts-ignore
import {makeProxy} from "./core/Proxy";
interface WatcherOption {
el: String | HTMLElement; // Bind an existing DOM object
data: any; // Data object
render: Function; // Render function
}
/** * @description observer object, since we want to simulate the VUE data response system in proxy mode, we will design this class simply */
export class Watcher {
// Use the watcher instance globally, pointing to the current Watcher object, convenient proxy use
public static target: any;
data: any = {};
el: HTMLElement;
renderList: Array<Function> = new Array<Function> ();constructor(options: WatcherOption) {
if (typeof options.el === 'string') {
this.el = document.getElementById(options.el);
} else {
// @ts-ignore
this.el = options.el;
}
this.data = makeProxy.call(this, options.data); // Build the proxy layer by deeply traversing the entire data object
this.addRender(options.render); // Add the render function to the render function array
}
// Respond and call the observer object
notify(): void {
for (let item of this.renderList) {
item.call(this.data, this.createElement); }}/** * @param fn */
public addRender(fn: Function) :void {
Watcher.target = this; // When adding dependencies, determine which one to give
this.renderList.push(fn);
this.notify();
Watcher.target = null;
}
/** * @description adds a proxy layer list of observers for each data object * @param object * @param property */
static addDep(object, property): void {
object.__proxy__.notifySet.add(property);
}
static removeDep(object, property): void {
object.__proxy___.notifySet.remove(property);
}
private createElement(innerHTML: string) {
_createElement(this.el, innerHTML); }}const _createElement = (dom: HTMLElement, innerHtml: string) = > {
dom.innerHTML = innerHtml;
};
Copy the code
Proxy
/** * Adds an attribute to the object __proxy__ * that represents what the object's proxy layer holds */
import {isPlainObject} from ".. /utils/Utils";
import {Watcher} from ".. /Watcher";
/** * @description This is the core code for this article, because I don't have watch, computed properties, so I don't need a basket to store watcher. There would be no Dep class @param object @param this Wacther object */
export function makeProxy(this: Watcher, object: any) :any {
object.__proxy__ = {};
// @ts-ignore
object.__proxy__.notifySet = new Set<string | number | symbol>();
object.__watcher__ = this;
// @ts-ignore
let proxy = new Proxy(object, {
get(target: any, p: string | number | symbol, receiver: any) :any {
if(Watcher.target ! =null) {
Watcher.addDep(object, p); // Add dependencies
}
return target[p];
},
set(target: any, p: string | number | symbol, value: any, receiver: any) :boolean {
if(target[p] ! == value) {// Render the view layer only when the two values are different
target[p] = value;
if (target.__proxy__.notifySet.has(p)) {
// Render is executed only when notifySet has this propertytarget.__watcher__.notify(); }}return true; }});// Get all the child attributes of the object, and recursively proxy the child attributes for deep observation
let propertyNames = Object.getOwnPropertyNames(object);
for (let i = 0; i < propertyNames.length; i++) {
// @ts-ignore
if(isPlainObject(object[propertyNames[i]]) && (! propertyNames[i].startsWith('__') && !propertyNames[i].endsWith('__'))) {
object[propertyNames[i]] = makeProxy.call(this, object[propertyNames[i]]); }}return proxy;
}
Copy the code
utils
const _toString = Object.prototype.toString
/** * @description is used for ordinary functions, extracting the internal code block of function * @param func */
export function getFunctionValue(func: Function) :string {
let funcString: string = func.toLocaleString();
let start: number = 0;
for (let i = 0; i < funcString.length; i++) {
if (funcString[i] == '{') {
start = i + 1;
break; }}return funcString.slice(start, funcString.length - 1);
}
export function isPlainObject (obj: any) :boolean {
return _toString.call(obj) === '[object Object]'
}
Copy the code