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

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

1.工具准备

需要的环境如下:

  • Node环境(babel)
  • TypeScript

需要的知识储备:

  • 《ES6标准入门》

2.思路

2.1总体结构

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

  • Watcher类的结构

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

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

  • 代理工具实现

    • 要对被监控的对象添加一个标志属性,这个属性会存放着哪一个属性被监控了
    • 将被监控的对象替换成代理后的对象
    • 必须要进行深度代理
  • 代理思路

    • gettersetter进行改写,在getter的时候进行依赖的确定(因为在render函数使用到了,所以这个依赖应当被监控),在setter的时候对渲染函数进行调用(当值改变的时候,需要对相应的渲染内容进行更新,这也就是本文章的目的)
    • 需要考虑到传进来的数据有的是有使用到,有的没有使用到。在更改没有使用到的属性的时候,不会触发渲染函数的调用。
  • 项目结构

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

2.2细节实现

  • Watcher类的具体实现

    • 构造器

      interface WatcherOption {
          el: String | HTMLElement;     // 绑定现有的dom对象
          data: any;   // 数据对象
          render: Function;   // 渲染函数
      }
      
      constructor(options: WatcherOptions) {
        this.data = makeProxy.call(this, options.data);  // 先将整一个数据对象深度遍历构建代理层
        this.addRender(options.render);       // 将渲染函数添加到渲染函数数组中
        if (typeof options.el == 'string') {
          // this.el = document.getElementById('el');
          this.el = options.el;
        } else {
          this.el = options.el;
        }
      }
      

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

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

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

      Watcher.targetWatcher的静态属性,这个属性的作用是记录当前进行观测的对象。这个对象会在代理的时候用到。使用这个原因是:在添加依赖的时候,先当前的Watcher设置为Watcher.target,然后调用渲染函数,渲染函数会调用响应的属性的getter,从而触发代理层进行添加依赖(后面重新渲染的时候,是不会进行依赖的重复添加,因为Watcher.target为空,这个待会会在代理工具里面进行讲解)。

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

  • 代理层的实现

    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);  // 添加依赖
                }
                return target[p];
            },
            set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
                if (target[p] === value) {
                    // 当两个值完全相同的时候,是不需要去渲染视图层的
                    return false;
                } else {
                    // 两个值不同的时候才需要去渲染视图层
                    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;
    }
    

    此功能有两个特别注意的点,第一个是对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) {
            // 当两个值完全相同的时候,是不需要去渲染视图层的
            return false;
          } else {
            // 两个值不同的时候才需要去渲染视图层
            target[p] = value;
            if (target.__proxy__.notifySet.has(p)) {
              target.__watcher__.notify();
            }
          }
      
          return true;
        }
      });
      
      • getter:要特别主要到一个判断语句:
      if (Watcher.target != null) {
      	Watcher.addDep(object, p);  // 添加依赖
      }
      

      还记得在添加渲染函数的时候,修改Watcher.target吗,这个条件就是防止每次渲染的时候对依赖进行添加。

      • setter:这个已经代码已经解释得很清楚了,就是判断这个属性有没有被渲染函数使用到,如果有的话,就进行调用渲染函数。

3.代码

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

interface WatcherOption {
    el: String | HTMLElement;     // 绑定现有的dom对象
    data: any;   // 数据对象
    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);  // 先将整一个数据对象深度遍历构建代理层
        this.addRender(options.render);       // 将渲染函数添加到渲染函数数组中
        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 为渲染函数所调用到的对象查询需要代理的对象
     * @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);
    }
}
  • Proxy
/**
 * 在对象上添加一个属性  __proxy__
 * 这个属性代表着这个对象的代理层所存放的东西
 */
import {isPlainObject} from "../utils/Utils";
import {Watcher} from "../Watcher";

/**
 * @description 将这个对象深度遍历,只要属性是对象的话,就要转为其代理后的对象。
 * @param object
 * @param this Wacther对象
 */
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);  // 添加依赖
            }
            return target[p];
        },
        set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
            if (target[p] === value) {
                // 当两个值完全相同的时候,是不需要去渲染视图层的
                return false;
            } else {
                // 两个值不同的时候才需要去渲染视图层
                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 用于普通的函数,提取函数的内部代码块
 * @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]'
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值