The implementation of Vue data response system based on ES6 agent model

The implementation of Vue data response system based on ES6 agent model

1. Tool preparation

The required environment is as follows:

  • Node environment (babel)
  • TypeScript

Knowledge reserve needed:

  • Introduction to ES6 standard

2. way of thinking

2.1 overall structure

The overall structure of the practice is to take a Watcher implementation class as the carrier, simulate the way of Vue, and input the data, render function and attached dom nodes that need to respond. Then, when the attributes of the data sent by the parameters are changed, the call of the render function will be triggered (provided that the modified data is used in the render function).

  • Structure of Watcher class

    class Watcher {
     	// The array of rendering functions. A data may exist in more than one rendering function. There may be multiple rendering function calls
      renderList: Array<Function>;
      // data
      data: any;
      // Mounted el elements
      el: String | HTMLElement;
    }
    

    The above is the structure of Watcher class. After data, rendering function and dom elements are passed in, they will be monitored automatically.

  • Agent tool implementation

    • To add a flag attribute to the monitored object, which attribute will be stored and monitored
    • Replace the monitored object with the object after agent
    • Deep proxy is required
  • Agent thinking

    • Rewrite the getter and setter, determine the dependency when getter is used (because the dependency should be monitored when the render function is used), and call the rendering function when setter is used (when the value changes, the corresponding rendering content needs to be updated, which is the purpose of this article)
    • It needs to be considered that some of the data passed in has been used and some has not been used. When changing properties that are not used, the call to the rendering function is not triggered.
  • Project structure

    -DataBind
    --core
     |- Proxy.ts   // Proxy tools
    --utils
     |- Utils.ts   // General tools
    Watcher.ts
    

2.2 detailed realization

  • The concrete implementation of Watcher class

    • constructor

      interface WatcherOption {
          el: String | HTMLElement;     // Binding an existing dom object
          data: any;   // data object
          render: Function;   // Render function
      }
      
      constructor(options: WatcherOptions) {
        this.data = makeProxy.call(this, options.data);  // First, traverse the whole data object to build the agent layer
        this.addRender(options.render);       // Add a render function to the render function array
        if (typeof options.el == 'string') {
          // this.el = document.getElementById('el');
          this.el = options.el;
        } else {
          this.el = options.el;
        }
      }
      

      There are three important attributes of configuration: Mount object, data object and rendering function. The specific process is as follows:

      1. Create the proxy object with the data and return the result to the data attribute
      2. To add a rendering function to the list
      3. Mounting of nodes
    • Rendering function management

      /**
       * @description Querying the objects to be proxied for the objects called by the rendering function
       * @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;
      }
      

      Watcher.target is the static property of watcher, which is used to record the current observed object. This object will be used in proxy. The reason for this is that when the dependency is added, the current Watcher is set to Watcher.target, and then the rendering function is rendered. The rendering function calls the getter of the response property, which triggers the proxy layer to add dependency. (when it is rendered again, it will not be added repeatedly, because Watcher.target is empty, which will be in the proxy tool later. Explain).

      So this function first records the current Watcher instance, then pushes the rendering function into the array, and then calls the rendering function. The dependency is added and the target is set to null.

  • Implementation of agent layer

    function makeProxy(this: Watcher, object: any): any {
        object.__proxy__ = {};
        object.__proxy__.notifySet = new Set();
        object.__watcher__ = this;
    		
      	// Create proxy object
        let proxy = new Proxy(object, {
            get(target: any, p: string | number | symbol, receiver: any): any {
                if (Watcher.target != null) {
                    Watcher.addDep(object, p);  // Add dependency
                }
                return target[p];
            },
            set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
                if (target[p] === value) {
                    // When the two values are identical, there is no need to render the view layer
                    return false;
                } else {
                    // When the two values are different, you need to render the view layer
                    target[p] = value;
                    if (target.__proxy__.notifySet.has(p)) {
                        target.__watcher__.notify();
                    }
                }
    
                return true;
            }
        });
    
        let propertyNames = Object.getOwnPropertyNames(object);
    
        for (let i = 0; i < propertyNames.length; i++) {
            if (isPlainObject(object[propertyNames[i]]) && (!propertyNames[i].startsWith('__') && !propertyNames[i].endsWith('__'))) {
                object[propertyNames[i]] = makeProxy.call(this, object[propertyNames[i]]);
            }
        }
    
        return proxy;
    }
    

    This function has two points of special attention, the first is to add the object attribute, and the second is the details of the proxy object.

    • Add object attribute:

      • __Proxy.notifyset: This is the property that stores the set instance. This set instance records which property is monitored. Only when it is monitored can it be observed. Properties that are not monitored will not be monitored.
      • __Watcher: This is the current watcher instance object.
    • Generation of proxy objects:

      new Proxy(object, {
        get(target: any, p: string | number | symbol, receiver: any): any {
          if (Watcher.target != null) {
            Watcher.addDep(object, p);  // Add dependency
          }
          return target[p];
        },
        set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
          if (target[p] === value) {
            // When the two values are identical, there is no need to render the view layer
            return false;
          } else {
            // When the two values are different, you need to render the view layer
            target[p] = value;
            if (target.__proxy__.notifySet.has(p)) {
              target.__watcher__.notify();
            }
          }
      
          return true;
        }
      });
      
      • getter: especially a judgment statement:
      if (Watcher.target != null) {
      	Watcher.addDep(object, p);  // Add dependency
      }
      

      Remember to modify Watcher.target when adding rendering functions. This condition is to prevent dependency from being added every time you render.

      • setter: this code has been explained clearly. It is to judge whether this property is used by the rendering function. If so, call the rendering function.

3. code

  • Watcher
// @ts-ignore
import {makeProxy} from "./core/Proxy";

interface WatcherOption {
    el: String | HTMLElement;     // Binding an existing dom object
    data: any;   // data object
    render: Function;   // Render function
}

export class Watcher {
    public static target: any;
    data: any = {};
    el: String | HTMLElement;
    renderList: Array<Function> = new Array<Function>();

    constructor(options: WatcherOption) {
        this.data = makeProxy.call(this, options.data);  // First, traverse the whole data object to build the agent layer
        this.addRender(options.render);       // Add a render function to the render function array
        if (typeof options.el == 'string') {
            // this.el = document.getElementById('el');
            this.el = options.el;
        } else {
            this.el = options.el;
        }
    }

    notify(): void {
        for (let item of this.renderList) {
            item.call(this.data);
        }
    }

    /**
     * @description Querying the objects to be proxied for the objects called by the rendering function
     * @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 Add a list of observers to observe for each data object in the agent layer
     * @param object
     * @param property
     */
    static addDep(object, property): void {
        object.__proxy__.notifySet.add(property);
    }

    static removeDep(object, property): void {
        object.__proxy___.notifySet.remove(property);
    }
}
  • Proxy
/**
 * Add a property "proxy" to the object__
 * This property represents what is stored in the agent layer of this object
 */
import {isPlainObject} from "../utils/Utils";
import {Watcher} from "../Watcher";

/**
 * @description To traverse this object in depth, as long as the property is an object, it must be converted to the object after its proxy.
 * @param object
 * @param this Wacther object
 */
export function makeProxy(this: Watcher, object: any): any {
    object.__proxy__ = {};
    object.__proxy__.notifySet = new Set();
    object.__watcher__ = this;

    let proxy = new Proxy(object, {
        get(target: any, p: string | number | symbol, receiver: any): any {
            if (Watcher.target != null) {
                Watcher.addDep(object, p);  // Add dependency
            }
            return target[p];
        },
        set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
            if (target[p] === value) {
                // When the two values are identical, there is no need to render the view layer
                return false;
            } else {
                // When the two values are different, you need to render the view layer
                target[p] = value;
                if (target.__proxy__.notifySet.has(p)) {
                    target.__watcher__.notify();
                }
            }

            return true;
        }
    });

    let propertyNames = Object.getOwnPropertyNames(object);

    for (let i = 0; i < propertyNames.length; i++) {
        if (isPlainObject(object[propertyNames[i]]) && (!propertyNames[i].startsWith('__') && !propertyNames[i].endsWith('__'))) {
            object[propertyNames[i]] = makeProxy.call(this, object[propertyNames[i]]);
        }
    }

    return proxy;
}
  • utils
const _toString = Object.prototype.toString
/**
 * @description For ordinary functions, extract the internal code block of functions
 * @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]'
}
Published 8 original articles, won praise 7, visited 1469
Private letter follow

Tags: Attribute Vue TypeScript

Posted on Sat, 18 Jan 2020 07:22:30 -0500 by ethan6