vaakian / vaakian.github.io

some notes
https://vaakian.github.io
3 stars 0 forks source link

响应式数据之:Object.defineProperty(es5) && Proxy(es6) #23

Open vaakian opened 2 years ago

vaakian commented 2 years ago

DefineProperty

defineProperty会直接改变原对象,所以要注意避免循环触发getset,即在set中不要直接set该对象,在get中不直接访问该对象,而是用一个变量来作为替代。 其实就是相当于原来的property被一个内部变量完全替代。

const watch1 = function (obj, property, fn) {
    // defineProperty需要储存 obj.value 的值,以后默认就是返回这个value
    let value = obj[property];
    Object.defineProperty(obj, property, {
        get: function () {
            // return obj[property]; 这会循环触发get
            return value;
        },
        set: function (newValue) {
            fn(value, newValue);
            value = newValue;

        }
    });
}

let data = {
    a: 5
}
watch2(data, 'a', function (oldValue, newValue) {
    console.log({ oldValue, newValue })
})
data.a = 7

Proxy

ProxydefineProperty的区别在于,他不直接侵入愿原对象,返回一个新的对象。

const watch = function (obj, proxyProperty, fn) {
    // Proxy可以通用,不需要传入property
    return new Proxy(obj, {
        set(target, prop, value) {
            if (proxyProperty === prop) fn(target[prop], value);
            // Proxy不会导致循环set
            target[prop] = value;
        }
    });
}
proxiedData = watch({a: 5}, 'a', function (oldValue, newValue) {
    console.log({ oldValue, newValue })
})

proxiedData.a = 7
vaakian commented 2 years ago

深层响应式数据——defineReactive

DefineProperty代理的是单个属性,对新增属性无能为力。 Proxy代理的是整个对象,对新增属性依然有效。

两者都是浅层次代理,即本层代理,对象里面嵌套的对象都无效。 要实现响应式,两者都需要递归代理。

这里用两种方法实现defineReactive,将数据进行深层递归代理。

Proxy实现

function defineReactive(obj) {
    return new Proxy(obj, {
        set(target, k, v) {
            console.log(`set key:${k} -> ${v}`)
            return target[k] = v
        },
        get(target, k) {
            console.log(`get key: ${k}`)
            let result = target[k]
            // 如果是对象,递归返回一个被代理的对象
            if (typeof result === 'object' && result !== null) {
                result = defineReactive(result)
            }
            return result
        }
    })
}

let person = defineReactive({ name: 'xwj', family: { count: 5, list: [1, 2, 3, 4] } })

// 先得到person.family,触发get,得到一个被代理的family
// 再触发set方法
person.family.count = 66

console.log(person.family.count)

Object.defineProperty实现

2021-10-23 16:58 新增访问键链,可以知道访问从哪里开始。

function defineReactive(target, parent = []) {
    let _target = {}

    for (let prop of Object.keys(target)) {
        // 存储键链
        let keyChain = [...parent, prop]
        // 劫持后的真实数据存储对象_target
        _target[prop] = target[prop]
        // 如果是对象,则递归劫持
        if (typeof _target[prop] === 'object' && _target[prop] !== null) {
            defineReactive(target[prop], keyChain)
        }
        // 属性不是对象,或其子属性劫持完毕,再劫持本次
        // 本质上是一个前序遍历
        Object.defineProperty(target, prop, {
            set(newVal) {
                console.log(`set: ${keyChain.join('.')} -> ${newVal}`)
                return _target[prop] = newVal
            },
            get() {
                console.log(`get: ${keyChain.join('.')}`)
                return _target[prop]
            }
        })
    }
    return target
}

let obj = defineReactive({ name: 'XWJ', family: { count: 5, list: [1, 2, 3, 4] } })
obj.name = '熊维建'
console.log(obj.family.list[0])

image

vaakian commented 2 years ago

通过以上定义的defineReactive,能够一窥pushshift等函数内部到底发生了什么。

let arr = defineReactive([1, 2, 3, 4])

arr.push(123)
arr.shift()

image

sort干了什么?

let arr = defineReactive([1, 2, 3, 4, -5, 125, -1221])

arr.sort((a, b) => a - b)
console.log(arr)

image

通过输出可以看出,首先将数组复制了一份,在复制的数组上进行排序,最后统一赋值到原数组上去。