ES6代理模式实现Vue数据响应系统

Vue数据响应系统的代理模式实现

1.工具准备

须要的环境以下:vue

  • Node环境(babel)
  • TypeScript

须要的知识储备:typescript

  • 《ES6标准入门》

2.思路

2.1整体结构

​ 该实践的整体结构是以一个Watcher实现类为载体,模拟Vue的方式,将须要进行响应的数据(data)、渲染函数(render)、挂载的dom节点输入进来。而后对传参送进来的data的属性进行改变的时候,会触发render函数的调用(前提是这个修改的数据有在渲染函数中被使用到)。数组

  • Watcher类的结构bash

    class Watcher {
     	// 渲染函数数组,一个数据可能不止存在于一个渲染函数当中,可能会有多个渲染函数调用
      renderList: Array<Function>;
      // 数据
      data: any;
      // 挂载的el元素
      el: String | HTMLElement;
    }
    复制代码

    上面是Watcher类的结构,将数据、渲染函数、dom元素传进来后,就会进行自动进行监测。babel

  • 代理工具实现dom

    • 对要被观察的对象添加notifySet,是一个Set。这个集合存放着哪些属性被观察到,若是被观察到的,对其进行setter调用的时候,会触发渲染函数进行渲染。
    • 将被观察的对象替换成代理后的对象,使用方式同样,只不过多了一层代理,这也就是代理模式的做用。
    • 本项目默认使用的是深度观察,其实能够多一个flag来实现是否深度观察。
  • 代理思路函数

    • gettersetter进行改写,在getter的时候进行依赖的肯定(由于在render函数使用到了,因此这个依赖应当被监控),在setter的时候对渲染函数进行调用(当值改变的时候,须要对相应的渲染内容进行更新,这也就是本文章的目的)
    • 在对notifySet进行添加属性的时候,只须要将被观察到的属性放进这个set中,无关的属性则不放进去。本项目的实现思路是以下:
      • 开启依赖添加模式
      • 建立代理对象
      • 以代理对象为数据执行渲染函数,从而实现依赖的添加
      • 关闭依赖添加模式
  • 项目结构工具

    -DataBind
    --core
     |- Proxy.ts   // 代理工具
    --utils
     |- Utils.ts   // 通用工具
    Watcher.ts
    复制代码

2.2细节实现

  • Watcher类的具体实现ui

    • 构造器this

      interface WatcherOption {
          el: String | HTMLElement;     // 绑定现有的dom对象
          data: any;   // 数据对象
          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);  // 先将整一个数据对象深度遍历构建代理层
        this.addRender(options.render);       // 将渲染函数添加到渲染函数数组中
      }
      复制代码

      构造器传进来配置(options),配置有三个重要的属性:挂载对象、数据对象、渲染函数。具体流程以下:

      1. 将数据进行创造代理对象,而且将结果返回给data属性
      2. 进行添加渲染函数到列表中
      3. 节点的挂载
    • 渲染函数管理

      /** * @description 为渲染函数所调用到的对象查询须要代理的对象 * @param fn */
      public addRender(fn: Function): void {
        Watcher.target = this;  		// 开启代理模式,这个target对象是Watcher类的静态变量,在proxy函数里面会使用到
        this.renderList.push(fn);
        this.notify();
        Watcher.target = null;			// 关闭代理模式
      }
      复制代码

      Watcher.targetWatcher的静态属性,这个属性的做用是记录当前进行观测的对象。这个对象会在代理的时候用到。使用这个缘由是:在添加依赖的时候,先当前的Watcher设置为Watcher.target,而后调用渲染函数,渲染函数会调用响应的属性的getter,从而触发代理层进行添加依赖(后面从新渲染的时候,是不会进行依赖的重复添加,由于Watcher.target为空。这个在makeProxy函数里面能够查看到。

      因此这个函数是先将记录当前的Watcher实例,而后将渲染函数推动数组中,再进行调用渲染函数。此时会进行依赖的添加,而后将target设为空。

  • 代理层的实现

    /** * @description 这个是咱们本文章的核心代码,由于我没有设置watch、computed属性,因此也就不须要筐来存放watcher。也就不会有Dep这个类 * @param object * @param this Wacther对象 */
    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);  // 添加依赖
                }
                return target[p];
            },
            set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
                if (target[p] !== value) {
                    // 两个值不一样的时候才须要去渲染视图层
                    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++) {
            // @ts-ignore
            if (isPlainObject(object[propertyNames[i]]) && (!propertyNames[i].startsWith('__') && !propertyNames[i].endsWith('__'))) {
                object[propertyNames[i]] = makeProxy.call(this, object[propertyNames[i]]);
            }
        }
    
        return proxy;
    }
    
    复制代码

    此功能有两个特别注意的点,第一个是对object属性的添加、第二个是代理对象的细节。

    • object属性的添加:

      • __proxy__.notifySet:这是存放set实例的属性,这个set实例是进行记录哪一个属性被监听到,若是该属性被监听,那么会放到这个集合中,方便得知监听哪一个属性
      • __watcher__:这个是指向当前的wacher实例对象。
    • 代理对象的生成:

      new Proxy(object, {
        get(target: any, p: string | number | symbol, receiver: any): any {
          if (Watcher.target != null) {
            Watcher.addDep(object, p);  // 添加依赖
          }
          return target[p];
        },
        set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
          if (target[p] !== value) {
            // 两个值不一样的时候才须要去渲染视图层
            target[p] = value;
            if (target.__proxy__.notifySet.has(p)) {
              // 仅仅当notifySet拥有这个属性的时候,才进行渲染函数的执行
              target.__watcher__.notify();
            }
          }
      
          return true;
        }
      });
      复制代码
      • getter:要特别主要到一个判断语句:
      if (Watcher.target != null) {
      	Watcher.addDep(object, p);  // 添加依赖
      }
      复制代码

      还记得在添加渲染函数的时候,修改Watcher.target吗?这个条件不为空的时候就是在添加渲染函数的时候,将对象的属性添加进notifySet中,方便调用该属性的时候执行回调函数

      • setter:这个已经代码已经解释得很清楚了,就是判断这个属性有没有被渲染函数被添加至集合中,若是有的话,就进行调用渲染函数。

3.代码

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

interface WatcherOption {
    el: String | HTMLElement;     // 绑定现有的dom对象
    data: any;   // 数据对象
    render: Function;   // 渲染函数
}

/** * @description 观察者对象,因为咱们目的是用代理模式来进行模拟vue数据响应系统,那么就从简设计这个类 */
export class Watcher {
    // 全局使用到watcher实例,指向当前的watcher对象,方便proxy使用
    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);  // 先将整一个数据对象深度遍历构建代理层
        this.addRender(options.render);       // 将渲染函数添加到渲染函数数组中
    }

    // 响应而且调用观察者对象
    notify(): void {
        for (let item of this.renderList) {
            item.call(this.data, this.createElement);
        }
    }

    /** * @description 为渲染函数所调用到的对象查询须要代理的对象 * @param fn */
    public addRender(fn: Function): void {
        Watcher.target = this;  // 进行添加依赖的时候,要肯定给哪一个
        this.renderList.push(fn);
        this.notify();
        Watcher.target = null;
    }

    /** * @description 为每一个数据对象添加代理层的须要观察的观察者列表 * @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;
};

复制代码
  • Proxy
/** * 在对象上添加一个属性 __proxy__ * 这个属性表明着这个对象的代理层所存放的东西 */
import {isPlainObject} from "../utils/Utils";
import {Watcher} from "../Watcher";

/** * @description 这个是咱们本文章的核心代码,由于我没有设置watch、computed属性,因此也就不须要筐来存放watcher。也就不会有Dep这个类 * @param object * @param this Wacther对象 */
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);  // 添加依赖
            }
            return target[p];
        },
        set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
            if (target[p] !== value) {
                // 两个值不一样的时候才须要去渲染视图层
                target[p] = value;
                if (target.__proxy__.notifySet.has(p)) {
                    // 仅仅当notifySet拥有这个属性的时候,才进行渲染函数的执行
                    target.__watcher__.notify();
                }
            }

            return true;
        }
    });

    // 获取对象的全部子属性,而且对子属性进行递归代理以便实现深度观察
    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;
}


复制代码
  • utils
const _toString = Object.prototype.toString
/** * @description 用于普通的函数,提取函数的内部代码块 * @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]'
}

复制代码
相关文章
相关标签/搜索