提示💡

  • Vue2不能监听数组下标变化(数组API重写),对象属性新增(set API)和删除(delete API),只能监听对象的属性,需要深度遍历。
  • Vue3代理整个对象,IE8以下不支持,this指向代理对象。

Object. defineProperty

vue2.x 就是通过 Object.defineProperty 实现数据劫持,把 data 中绑定的属性全部转换成 getter()/setter(),然后结合订阅发布者模式实现响应式。getter()可以读取数据,收集依赖,setter()可以改写数据,在数据发生改变时发布消息通知订阅者,触发监听回调,更新视图

响应式函数

定义一个响应式函数defineReactive

function update() {
    app.innerText = obj.foo
}
 
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                val = newVal
                update()
            }
        }
    })
}

调用 defineReactive,数据发生变化触发 update 方法,实现数据响应式。

const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(()=>{
    obj.foo = new Date().toLocaleTimeString()
},1000)

在对象存在多个 key 情况下,需要进行遍历。

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return
    }
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
    })
}

如果存在嵌套对象的情况,还需要在 defineReactive 中进行递归。

function defineReactive(obj, key, val) {
    observe(val)
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                val = newVal
                update()
            }
        }
    })
}

当给 key 赋值为对象的时候,还需要在 set 属性中进行递归。

set(newVal) {
    if (newVal !== val) {
        observe(newVal) // 新值是对象的情况
        notifyUpdate()
    }
}

定义一个响应式函数defineReactive

Object. defineProperty 的缺陷

  • Object.defineProperty 不能监控数组下标的变化,导致通过数组下标添加的元素不能实时响应,并且- 数组 API 方法无法监听到。
const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
    defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop()  // no ok
arrDate[0] = 99 // ok
  • 无法劫持到对象的删除与添加属性操作。
const obj = {
    foo: "foo",
    bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok
  • Object. defineProperty 只能监听对象的属性,导致需要对每个对象、每个属性都进行遍历,如果属性值也是对象,则需要进行深度遍历,造成性能问题

Vue2 中,增加了 setdelete API,并且对数组 API 方法重写进行一个重写。

也可以使用 v-ifthis.$forceUpdate() 进行强制刷新,但不推荐经常使用。

Proxy

在 Vue3.x 中使用了 Proxy 代替了 Object.defineProperty,因为它是对整个对象进行代理,所以可以监听对象某个属性值的变化,还可以监听对象属性的新增和删除,而且还可以监听数组

注意❗

Proxy 不兼容 IE,也没有 polyfilldefineProperty 能支持到 IE 9 。

响应式方法

定义一个响应式方法 reactive

function reactive(obj) {
    if (typeof obj !== 'object' && obj != null) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(`设置${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(`删除${key}:${res}`)
            return res
        }
    })
    return observed
}

测试一下简单数据的操作,发现都能劫持。

const state = reactive({
    foo: 'foo'
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok

再测试嵌套对象情况,这时候发现就不那么 OK 了。

const state = reactive({
    bar: { a: 1 }
})
 
// 设置嵌套对象属性
state.bar.a = 10 // no ok

如果要解决,需要在 get 之上再进行一层代理。

function reactive(obj) {
    if (typeof obj !== 'object' && obj != null) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return isObject(res) ? reactive(res) : res
        },
    return observed
}

总结

Object. defineProperty  的缺点。

  • 不能监控数组下标的变化
  • 无法劫持到对象的删除与添加 属性操作。
  • 只能监听对象的属性 ,需要深度遍历。

Vue2 中的兼容处理。

  • 增加了 setdelete API
  • 数组 API 方法重写进行一个重写。
  • 也可以使用 v-ifthis.$forceUpdate() 进行强制刷新,但不推荐经常使用。

由上可知,Object.defineProperty 与 Proxy 的区别如下:

Object.definePropertyProxy
定义代理对象某个属性代理整个对象
对象新增属性
数组新增修改和 length
this指向目标对象指向代理对象
兼容性兼容 IE不兼容 IE8 及以下
使用上考虑因素多更方便,方法更多(13 种 handler

扩展阅读