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 observednotifySet, it is aSet. This collection holds which attributes are observed and, if observed, what is done about themsetterWhen 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 moreflagTo achieve depth observation.
  • Agent thinking

    • rightgetterandsetterI’m going to rewrite it ingetterWhen determining dependencies (because inrenderFunction 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 thenotifySetWhen you add attributes, you just put the observed attributes into thissetIn, 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
  • 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:

      1. Create a proxy object from the data and return the result todataattribute
      2. Add the render function to the list
      3. Node mounting
    • 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 storagesetInstance properties, this onesetThe 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 currentwacherInstance 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

      • setterThis 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