ES6 Proxy實現Vue的變化檢測

Vue變化檢測Object使用DefineProperty、數組使用方法攔截實現。最近,Vue3.0將採用ES6 Proxy的形式從新實現Vue的變化檢測,在官方還沒給出新方法以前,咱們先實現一個基於Proxy的變化檢測。數組

模塊劃分

參照以前Vue變化檢測的代碼,將Vue 變化檢測的功能分爲如下幾個部分。bash

  • Observer
  • Dep
  • Watcher
  • Utils

首先,咱們要肯定的問題是,將Dep依賴蒐集存在哪裏。Vue 2.x裏,Object的依賴收集放在defineRactive,Array的依收集存入到Observer中。ES6 Proxy裏,考慮到讓handler訪問dep,咱們將依賴放入到Observer中。測試

Observer

observer.js功能代碼以下:ui

import Dep from './dep';
import { isObject } from './utils';

export default class Observer {
    constructor (value) {
        // 遞歸處理子元素
        this.obeserve(value);
        // 實現當前元素的代理
        this.value = this.proxyTarget(value);
    }
    
    proxyTarget (targetBefore, keyBefore) {
        const dep = new Dep();
        targetBefore.__dep__ = dep;
        let self = this;
        const filtersAtrr = val => ['__dep__', '__parent__'].indexOf(val) > -1;
        return new Proxy(targetBefore, {
            get: function(target, key, receiver){
                if (filtersAtrr(key)) return Reflect.get(target, key, receiver);
                if (!Array.isArray(target)) {
                    dep.depend(key);
                }
                // sort/reverse等不改變數組長度的,在get裏觸發
                if (Array.isArray(target)) {
                    if ((key === 'sort' || key === 'reverse') && target.__parent__) {
                        target.__parent__.__dep__.notify(keyBefore);
                    }
                } 
                return Reflect.get(target, key, receiver);
            },
            set: function(target, key, value, receiver){
                if (filtersAtrr(key)) return Reflect.set(target, key, value, receiver);
                // 新增元素,須要proxy
                const { newValue, isChanged } = self.addProxyTarget(value, target, key, self);
                // 設置key爲新元素
                Reflect.set(target, key, newValue, receiver);
                // notify
                self.depNotify(target, key, keyBefore, dep, isChanged);
                return true;
            },
        });
    }

    addProxyTarget(value, target, key, self) {
        let newValue = value;
        let isChanged = false;
        if (isObject(value) && !value.__parent__) {
            self.obeserve(newValue);
            newValue = self.proxyTarget(newValue, key);
            newValue.__parent__ = target;
            isChanged = true;
        }
        return {
            newValue,
            isChanged,
        }
    }

    depNotify(target, key, keyBefore, dep, isChanged) {
        if (isChanged && target.__parent__) {
            target.__parent__.__dep__.notify(keyBefore);
            return;
        }
        if (Array.isArray(target)) {
            if (key === 'length' && target.__parent__) {
                target.__parent__.__dep__.notify(keyBefore);
            }
        } else {
            dep.notify(key);
        }
    }

    obeserve(target) {
        // 只處理對象類型,包括數組、對象
        if (!isObject(target)) return;
        for (let key in target) {
            if (isObject(target[key]) && target[key] !== null) {
                this.obeserve(target[key]);
                target[key] = this.proxyTarget(target[key], key);
                // 設置__parent__,方便子元素調用
                target[key].__parent__ = target;
            }
        }
    }
}


複製代碼

在Observer中,針對對象,只須要執行dep.depend(key)dep.notify(key)便可。添加key是爲了能正確的觸發收集,不知道怎麼說明白爲何要這樣作,只能一切盡在不言中了。this

Array, 如何實現依賴的收集和觸發那。依賴收集與Object相似,dep.depend(key)完成數組的收集。關於觸發,能夠分爲兩個方面,一是改變數組長度、二未改變數組長度的。改變數組長度的,在set裏,經過長度屬性的設置觸發父級元素的notify。爲何要使用父級元素的notify那?咱們能夠分析如下,在你設置數組的長度時,這時候的target\key\value分別是[]\length*, 這個時候,數組的依賴收集是沒有的,你watcher的是數組,並非數組自己。這個時候只能經過target.__parent__.__dep__.notify(keyBefore)觸發父級的收集,完成數據變化的檢測。二對於未改變數組長度的,這裏的作法,雖然是直接target.__parent__.__dep__.notify(keyBefore)觸發依賴,可是有個嚴重的問題,實際上更新的數據不是最新的,這個地方暫時還沒想到比較好的方法,歡迎你們討論。spa

Dep

Dep.jsprototype

let uid = 0;

export default class Dep {
    constructor () {
        this.subs = {};
        this.id = uid++;
    }

    addSub(prop, sub) {
        this.subs[prop] = this.subs[prop] || [];
        this.subs[prop].push(sub);
    }

    removeSub(prop, sub) {
        this.remove(this.subs[prop] || [], sub);
    }

    depend(prop) {
        if (Dep.target) {
            // 傳入的是當前依賴
            Dep.target.addDep(prop, this)
        }
    }

    notify(prop) {
        const subs = (this.subs[prop] || []).slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }

    remove(arr, item) {
        if (arr.length) {
            const index = arr.indexOf(item);
            if (index > -1) {
                return arr.splice(index, 1);
            }
        }
    }
}

Dep.target = null
const targetStack = []

export function pushTarget (_target) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
}

export function popTarget () {
    Dep.target = targetStack.pop()
}
複製代碼

dep 添加prop實現類型的綁定,爲何要這麼作那?使用proxy代理後,你假如wahcter對象下的幾個元素,此時的deps將同時存在這幾個元素,你觸發依賴的時候,這些依賴都會執行。所以,經過key值綁定觀察事件,觸發時,能完成對象的正確觸發。代理

watcher、utils

import { parsePath } from './utils';
import { pushTarget, popTarget } from './dep'

export default class Watcher {
    constructor(vm, expOrFn, cb) {
        // dep id集合
        this.depIds = new Set();
        
        this.vm = vm;
        this.getter = parsePath(expOrFn);
        this.cb = cb;
        this.value = this.get();
    }

    get () {
        pushTarget(this);

        let value = this.getter.call(this.vm, this.vm);
        popTarget();

        return value;
    }

    update() {
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
    }

    addDep (prop, dep) {
        const id = dep.id;
        if (!this.depIds.has(id)) {
            this.depIds.add(id);
            dep.addSub(prop, this);
        }
    }
}
複製代碼

utils.jscode

/** * 解析簡單路徑 */

const bailRE = /[^\w.$]/;
export function parsePath (path) {
    if (bailRE.test(path)) {
        return;
    }
    const segments = path.split('.');
    return function (obj) {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return;
            obj = obj[segments[i]];
        }
        return obj;
    };
}

/** * Define a property. */
export function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}

/** * Quick object check - this is primarily used to tell * Objects from primitive values when we know the value * is a JSON-compliant type. */
export function isObject (obj) {
    return obj !== null && typeof obj === 'object'
}

/** * Check whether an object has the property. */
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}
複製代碼

Utils.js/Watchers.js與Vue 2.x相似,這裏就很少介紹了。regexp

測試一下

test.js

import Observer from './observer';
import Watcher from './watcher';

let data = {
    name: 'lijincai',
    password: '***********',
    address: {
        home: '安徽亳州譙城區',
    },
    list: [{
        name: 'lijincai',
        password: 'you know it Object',
    }], 
};

const newData = new Observer(data);

let index = 0;
const watcherName = new Watcher(newData, 'value.name', (newValue, oldValue) => {
    console.log(`${index++}: name newValue:`, newValue, ', oldValue:', oldValue);
});

const watcherPassword = new Watcher(newData, 'value.password', (newValue, oldValue) => {
    console.log(`${index++}: password newValue:`, newValue, ', oldValue:', oldValue);
});

const watcherAddress = new Watcher(newData, 'value.address', (newValue, oldValue) => {
    console.log(`${index++}: address newValue:`, newValue, ', oldValue:', oldValue);
});

const watcherAddressHome = new Watcher(newData, 'value.address.home', (newValue, oldValue) => {
    console.log(`${index++}: address.home newValue:`, newValue, ', oldValue:', oldValue);
});

const watcherAddProp = new Watcher(newData, 'value.addProp', (newValue, oldValue) => {
    console.log(`${index++}: addProp newValue:`, newValue, ', oldValue:', oldValue);
});

const watcherDataObject = new Watcher(newData, 'value.list', (newValue, oldValue) => {
    console.log(`${index++}: newValue:`, newValue, ', oldValue:', oldValue);
});

newData.value.name = 'resetName';
newData.value.password = 'resetPassword';
newData.value.name = 'hello world name';
newData.value.password = 'hello world password';
newData.value.address.home = 'hello home';
newData.value.address.home = 'hello home2';
newData.value.addProp = 'hello addProp';
newData.value.addProp ={
    name: 'ceshi',
};
newData.value.addProp.name = 'ceshi2';

newData.value.list.push('1');
newData.value.list.splice(0, 1);
newData.value.list.sort();
newData.value.list.reverse();
newData.value.list.push('1');
newData.value.list.unshift({name: 'nihao'});
newData.value.list[0] = {
    name: 'lijincai',
    password: 'you know it Array',
};
newData.value.list[0].name = 'you know it array after';
newData.value.list.pop();
newData.value.list.shift();
newData.value.list.length = 1;
複製代碼

咱們使用對象、數組測試一下咱們的ES6 Proxy檢測。

20:17:44.725 index.js?afc7:20 0: name newValue: resetName , oldValue: lijincai
20:17:44.725 index.js?afc7:24 1: password newValue: resetPassword , oldValue: ***********
20:17:44.725 index.js?afc7:20 2: name newValue: hello world name , oldValue: resetName
20:17:44.725 index.js?afc7:24 3: password newValue: hello world password , oldValue: resetPassword
20:17:44.726 index.js?afc7:32 4: address.home newValue: hello home , oldValue: 安徽亳州譙城區
20:17:44.726 index.js?afc7:32 5: address.home newValue: hello home2 , oldValue: hello home
20:17:44.726 index.js?afc7:36 6: addProp newValue: hello addProp , oldValue: undefined
20:17:44.727 index.js?afc7:36 7: addProp newValue: Proxy {name: "ceshi", __dep__: Dep, __parent__: {…}} , oldValue: hello addProp
20:17:44.727 index.js?afc7:40 0: newValue: Proxy {0: Proxy, 1: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: Proxy, 1: "1", __dep__: Dep, __parent__: {…}}
20:17:44.728 index.js?afc7:40 1: newValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}}
20:17:44.729 index.js?afc7:40 2: newValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}}
20:17:44.731 index.js?afc7:40 3: newValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}}
20:17:44.734 index.js?afc7:40 4: newValue: Proxy {0: "1", 1: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: "1", 1: "1", __dep__: Dep, __parent__: {…}}
20:17:44.735 index.js?afc7:40 5: newValue: Proxy {0: Proxy, 1: "1", 2: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: Proxy, 1: "1", 2: "1", __dep__: Dep, __parent__: {…}}
20:17:44.735 index.js?afc7:40 6: newValue: Proxy {0: Proxy, 1: "1", 2: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: Proxy, 1: "1", 2: "1", __dep__: Dep, __parent__: {…}}
20:17:44.736 index.js?afc7:40 7: newValue: Proxy {0: Proxy, 1: "1", 2: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: Proxy, 1: "1", 2: "1", __dep__: Dep, __parent__: {…}}
20:17:44.737 index.js?afc7:40 8: newValue: Proxy {0: Proxy, 1: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: Proxy, 1: "1", __dep__: Dep, __parent__: {…}}
20:17:44.738 index.js?afc7:40 9: newValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}}
20:17:44.738 index.js?afc7:40 10: newValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}} , oldValue: Proxy {0: "1", __dep__: Dep, __parent__: {…}}
複製代碼

咱們看到了ES6 Proxy後實現了Object/Array的檢測,雖然還存在一些問題,可是基本的偵測變化的功能都已經具有了。

相關文章
相關標籤/搜索