面试常考:Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?

94 阅读4分钟

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

Object.defineProperty为什么能实现响应式

通过defineProperty 两个属性,getset

  • get

属性的 getter 函数,当访问该属性时,会调用此函数。 执行时不传入任何参数。该函数的返回值会被用作属性的值。

  • set

属性的 setter 函数,当属性值被修改时,会调用此函数。 该方法接受一个参数,默认返回 undefined。

下面通过代码展示:

定义一个响应式函数Reactive

function update() {
    app.innerText = obj.foo
}

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

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

const obj = {}
Reactive(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 => {
        Reactive(obj, key, obj[key])
    })
}

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

function Reactive(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()
            }
        }
    })
}

上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题,例如对一个对象进行删除与添加属性操作,则会发现无法劫持到。

const obj = {
    foo: "foo",
    bar: "bar"
}
observe(obj)
delete obj.foo
obj.jar = 'xxx' 

当我们对一个数组进行监听的时。数据的api无法劫持到,从而无法实现数据响应式,

const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
    defineProperty(arrData,index,val)
})
arrData.push()
arrData.pop()
arrDate[0] = 99 

所以在Vue2中,实现了一些额外的API,如Vue.setVue.delete来处理对象属性的动态添加和删除,并且对数组的一些方法进行了重写来触发响应式更新。

// 数组重写
//创建了一个新对象 `arrayProto`,继承 `Array.prototype`。
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
  arrayProto[method] = function () {
    originalProto[method].apply(this.arguments)
    dep.notice()
  }
});

// set、delete
Vue.set(obj,'bar','newbar')
Vue.delete(obj),'bar')

当对象嵌套层级较深时,每一层对象的属性都需要通过defineProperty进行劫持,这会导致劫持操作的递归调用,使得性能受到影响。而且,Vue 2对于深层嵌套对象的监听是通过递归遍历对象的属性来实现的,这在数据量较大时会导致性能下降。

小结

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题。

proxy

Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了。

下面通过代码进行展示:

定义一个响应式方法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'
// 3.设置不存在属性
state.dong = 'dong' 
// 4.删除属性
delete state.dong 

再测试嵌套对象情况,发现不能劫持

const state = reactive({
    bar: { a: 1 }
})

// 设置嵌套对象属性
state.bar.a = 10 

如果要解决,需要在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只能遍历对象属性进行劫持。

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

Proxy直接可以劫持整个对象,并返回一个新对象。

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
}

Proxy可以直接监听数组的变

const obj = [1,2,3,4]
const proxtObj = reactive(obj)
obj.psuh(5) // ok

Proxy有多达13种拦截方法,不限于applyownKeysdeletePropertyhas等等,这是Object.defineProperty不具备的