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]'
}

複製代碼
相關文章
相關標籤/搜索