响应式数据

2022-12-28  本文已影响0人  欢西西西

1. Object.defineProperty

1.1 监听特定属性

const pager = { pageSize: 50 };

Object.defineProperty(PAGER, 'pageIndex', {
    get: function () {
        return this._value;
    },
    set: function (v) {
        this._value = v;
        loadCurPageData(v); // 监听到pageIndex属性设置的时候去做其他相关操作
    }
});

1.2 监听对象每个已有属性

const pager = {
    pageIndex: 0,
    pageSize: 50,
    total: 0
};

Object.keys(pager).forEach(key => {
    let val = pager[key];
    Object.defineProperty(pager, key, {
        get: function () {
            return val;
        },
        set: function (v) {
            val = v;
            console.log('设置属性:', key, v);
        }
    });
});

1.3 拦截方法的调用

let hasHeaderCondition = false;

// mainGrid对象有个clearFilter方法,我需要在它调用这个方法之后做点自己的逻辑
let fnOnClear = mainGrid.clearFilter; 
Object.defineProperty(mainGrid, 'setFocusFilter', {
    value() { // 重新定义value,在内部执行一下原方法,再做自己的逻辑
        fnOnFilter.apply(this, arguments);
        hasHeaderCondition = true;
    }
});

1.4 拦截数组方法的调用

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
];

methodsToPatch.forEach(methodName => {
    Object.defineProperty(arrayMethods, methodName, {
    value: function () {
        var original = arrayProto[methodName];
        var result = original.apply(this, arguments); // 先执行原方法

        // 做一些自己想做的逻辑

        return result;
    }
    })
});

1.5 深层对象

// 需要递归监听每个对象属性值
function defineReactive(object) {
    if (!object || typeof object !== 'object') {
        return;
    }
    let keys = Object.keys(object);
    for (let key of keys) {
        let value = object[key];
        Object.defineProperty(object, key, {
            set: function (v) {
                if (v === value) { return; }
                value = v;
                console.log('对象修改', key, v);
                defineReactive(v); // 新属性值继续设置监听
            },
            get: function () {
                return value;
            }
        });
        defineReactive(value); // 属性值继续设置监听
    }
}

1.6 Object.defineProperty的缺点

2. ES6 Proxy

2.1 优点

new Proxy(terget, {
    get() { }, // 拦截属性读取
    set() { }, // 拦截属性值设置
    deleteProperty() { }, // 拦截对属性的delete操作
});

2.2 代理对象

const p = new Proxy(pager, {
    set(target, key, newValue) {
        target[key] = newValue;
        console.log('设置属性:', key, newValue);
    },
    deleteProperty(target, key) {
        if (key in target) {
            delete target[key];
            return true;
        }
        console.log('监听到属性删除', arguments)
        return false;
    }
});

p.newAttr = 'wxm'; // 可以监听到新增属性
delete p.newAttr;

我们发现,拦截属性不需要枚举所有属性名去遍历,同时也能拦截到新增属性和delete操作。

2.3 代理数组

var arr = [1, 2, 3];
var proxyArr = new Proxy(arr, {
    set(target, prop, value, proxy) {
        target[prop] = value;
        console.log('拦截到更改', arguments)
        return true; // return 一个布尔值表示是否设置成功
    }
});
// 以下情况都能被拦截到:
// 通过index来修改项
// 使用超出的index设置值以扩充数组长度
// 通过设置length来扩充或缩小数组长度,只会被拦截到1次(length属性的更改),将项设置为empty不会拦截
// 通过push新增项:会被拦截到2次,一次是index项的更改,一次是数组length属性的更改
// 通过unshift新增项:会被拦截到arr.length(因为每项都会被更改)+1(length属性更改)次,
// 通过reverse翻转顺序,会被拦截到arr.length次,因为每项都会被更改
// 通过pop删除元素,拦截到length更改
// 通过shift删除元素:拦截到第一项后面每项的更改和length的更改
// 通过splice删除和新增元素,会被拦截N次,项的更改和length的更改
// 通过sort排序,拦截到项的更改

2.4 代理函数 handler.apply

function add(a, b) {
    return a + b + this.name;
}

var proxyAdd = new Proxy(add, {
    // 拦截函数执行
    apply(func, ctx, args) {
        console.time("run");
        let val = func.apply(ctx, args);
        console.timeEnd("run");
        return val;
    }
});

// 跟普通函数调用方法相同,这些调用方式都能被拦截到
proxyAdd(1, 2);
proxyAdd.call({name: 'wxm'}, 1, 2);
proxyAdd.apply({name: 'wxm'}, [1,2]);

3. 使用Proxy做响应式

  • get:在访问对象属性的时候收集依赖:在属性维度上收集函数,比如说在computed或template模板中访问了哪个对象的哪些属性
  • set:当写入属性值的时候,如果存在这个属性的依赖函数,就去通知这些依赖,以做相应的更新
  1. 拦截对象的读和写
function reactive(data) {
    return new Proxy(data, {
        get(target, key, receiver) {
            track(target, key); // 在访问对象属性的时候收集依赖
            return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver);
            trigger(target, key); // 当写入属性值的时候通知依赖
            return res;
        }
    });
}
  1. 收集依赖。将依赖收集到bucket里:key为原对象,value(Map类型)存各个属性的effect函数(key为属性名,value为Set类型)
let bucket = new WeakMap();
let curEffect;

// 收集依赖,收集的时候需要将依赖函数赋值给全局的curEffect
function track(target, key) {
    if (!curEffect) {
        return;
    }
    let depsMap = bucket.get(target); // 这个对象所有属性的依赖
    if (!depsMap) {
        depsMap = new Map();
        bucket.set(target, depsMap);
    }
    let deps = depsMap.get(key); // 当前属性的所有依赖
    if (!deps) {
        deps = new Set();
        depsMap.set(key, deps);
    }
    deps.add(curEffect); // 在set里加入当前effect函数
}
  1. 通知依赖
// 通知依赖
function trigger(target, key) {
    let depsMap = bucket.get(target); // 这个对象所有属性的依赖
    if (!depsMap) {
        return;
    }
    let deps = depsMap.get(key); // 当前属性的所有依赖
    if (!deps) {
        return;
    }
    // 通知deps里的所有依赖函数
    deps.forEach(effect => {
        effect();
    });
}
  1. 假设我们需要在修改title的时候执行effectTitle,修改name的时候执行effectName
let data = reactive({ title: '详情', name: '姓名' });

function effectTitle() { // 依赖data.title的函数
    console.log(data.title + ':title附加操作')
}

function effectName() { // 依赖data.name的函数
    console.log(data.name + ':name附加操作')
}
  1. 我们还没有触发属性的get,所以还没有收集依赖。接下来触发get,并在合适的时机给curEffect赋值 ,以把effectTitle和effectName加到bucket中去
function effect(fn) {
    const effectFn = () => {
        curEffect = effectFn; // 给curEffect赋值
        fn();
    };
    effectFn();
}

effect(effectTitle); // effect内部会立即执行effectTitle,而effectTitle内部读取了title,会触发收集依赖
effect(effectName);
  1. 执行依赖函数,依赖函数内部读取属性值会触发get,get里面会收集依赖。但是依赖函数会被执行多次,而只需要收集一次。但是目前track里面,我们并没有手动判断是否重复收集了,而是每次都执行了deps.add(curEffect);但是由于Set的特性,同一个依赖函数并不会被重复添加,这也是使用了Set而非Array来存储的原因。

  2. 此时还不能满足深层对象的监控,例如,在data.info.qty改变时同时renderQty

let data = reactive({ info: { qty: 10 } });
function renderQty() {
    console.log(data.info.qty + '元');
}
effect(renderQty);


data.info = {qty: 30}; // 这种情况会触发renderQty
data.info.qty = 20; // 这种情况不会触发renderQty

所以依赖在访问属性值的时候,如果发现值isObject,就返回经过reactive处理的代理对象:

const isObject = (val) => val !== null && typeof val === 'object';

const proxyMap = new Map();
function reactive(data) {
    if (!isObject(data)) {
        console.warn(`value cannot be made reactive: ${String(target)}`);
        return data;
    }
    const existingProxy = proxyMap.get(data);
    if (existingProxy) {
        return existingProxy;
    }
    const p = new Proxy(data, {
        get(target, key, receiver) {
            track(target, key); // 在访问对象属性的时候收集依赖
            let res = Reflect.get(target, key, receiver);
            return isObject(res) ? reactive(res) : res; // 如果值是对象则返回代理对象
        },
        set(target, key, value, receiver) {
            let res = Reflect.set(target, key, value, receiver);
            trigger(target, key); // 当写入属性值的时候通知依赖
            return res;
        }
    });
    proxyMap.set(data, p);
    return p;
}

再次测试:

data.info = {qty: 30}; // 会触发renderQty
data.info.qty = 20; // 会触发renderQty

4. Reflect

const p = new Proxy(pager, {
    get(target, key) {
        return target[key]; // 对比下面的使用Reflect.get
    },
});

const p1 = new Proxy(pager, {
    get(target, key, receiver) {
        return Reflect.get(target, key, receiver);
    },
});

上一篇 下一篇

猜你喜欢

热点阅读