跳到主要内容

Vue响应式数据之Object

在阅读《深入浅出 Vue.js》与《Vue.js 设计与实现》,了解到 vue 是如何侦测数据,同时自己在接触 js 逆向时也常常会用到。于是就准备写篇 js 如何监听数据变化,这篇为监听 Object 数据。

Object.defineproperty

const data = {
username: 'wenhao',
password: 'a123456',
}

function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log('GET', val)
return val
},
set(newVal) {
if (val === newVal) return

val = newVal
console.log('SET', val)
},
})
}

function observe(data) {
Object.keys(data).forEach(function (key) {
defineReactive(data, key, data[key])
})
}

observe(data)

data.username
data.username = '文浩'

从上面的代码中就可以发现,只要取值与赋值就会进入 get 和 set 函数内,在这里面便可以实现一些功能,例如 Vue 中收集依赖,在想监听浏览器中 cookies 的取值与赋值,就可以使用如下代码

!(function () {
let cookie = document.cookie
Object.defineProperty(document, 'cookie', {
get() {
console.log('cookie get', cookie)
return cookie
},
set(newVal) {
cookie = newVal
console.log('cookie set', cookie)
},
})
})()

使用 object.defineproperty 能监听对象上的某个属性修改与获取,但是无法监听到对象属性的增和删。这在 es5 是无法实现的,因为还不支持元编程。这也就是为什么 Vue2 中对于对象无法监听到 data 的某个属性增加与删除了

var vm = new Vue({
data: {
a: 1,
},
})

// `vm.a` 是响应式的

vm.b = 2
// `vm.b` 是非响应式的

Proxy 与 Reflect

但在 ES6 中提供了 Proxy 可以实现元编程,同时 Vue3 也使用 Proxy 来重写响应式系统。所以就很有必要去了解该 API

function reactive(target) {
return new Proxy(target, {
get(target, key) {
const res = target[key]
console.log('GET', key, res)
return res
},
set(target, key, newValue) {
target[key] = newValue
console.log('SET', key, newValue)
},
deleteProperty(target, key) {
console.log('DELETE', key)
delete target[key]
},
})
}

但上述写法中使用了target[key] 是能获取到 target 的值,但可能会存在一定隐患(如 this 问题),所以更推荐使用Reflect对象的方法,如下

function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log('GET', key, res)
return res
},
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver)
console.log('SET', key, newValue)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log('DELETE', key)

return res
},
})
}

调用如下

const target = {
foo: 1,
bar: 1,
}

let p = reactive(target)
p.foo++
delete p.bar

console.log(target)

输出内容如下

GET foo 1
SET foo 2
DELETE bar
{ foo: 2 }

其中这里的 get,set,deleteProperty 可以拦截到对象属性的取值,赋值与删除的操作。相比 Object.defineproperty 除了好用外,可操作空间也大。

this 问题

如果 target 对象存在 this,那么不做任何拦截的情况下,target 的 this 所指向的是 target,而不是代理对象 proxy

const target = {
m: function () {
console.log(this === proxy)
},
}
const handler = {}

const proxy = new Proxy(target, handler)

target.m() // false
proxy.m() // true

具体可看:this 问题

区别增加和修改

对象属性增加还是修改都会触发 set,所以需要在 set 中区别增加和修改,

function reactive(target) {
return new Proxy(target, {
set(target, key, newVal, receiver) {
const oldVal = target[key]

const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
const res = Reflect.set(target, key, newVal, receiver)

if (oldVal !== newVal) {
console.log(type, key, newValue)
}

return res
},
})
}

深响应

如果数据含多层对象,像

const p = reactive({ foo: { bar: 1 } })

// 将不会触发
p.foo.bar = 2

需要将 get 中包装为

function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)

if (typeof res === 'object' && res !== null) {
// 将其包装成响应式数据
return reactive(res)
}

console.log('GET', key, res)
return res
},
})
}

最终代码

在稍加对 console.log 进行封装,最终实现对 Object 代理的代码如下

const target = {
foo: 1,
bar: 1,
}

function log(type, key, val) {
console.log(type, key, val)
}

function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)

if (typeof res === 'object' && res !== null) {
return reactive(res)
}

log('GET', key, res)
return res
},
set(target, key, newVal, receiver) {
const oldVal = target[key]

const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
const res = Reflect.set(target, key, newVal, receiver)

if (oldVal !== newVal) {
log(type, key, newVal)
}

return res
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)

const res = Reflect.deleteProperty(target, key)

if (res && hadKey) {
log('DELETE', key, res)
}

return res
},
})
}

const p = reactive(target)
p.a = 1
p.foo++
delete p.bar

console.log(target)

当然,可以将 log 函数的进一步的封装,如 Vue3 中 get 方法的track,set 方法中的trigger。更好的监听数据变化以及执行自定义函数等等,这里只谈论监听数据变化。

此外 Proxy 还不只有监听对象的属性,还可以监听对象方法等等,具体可在MDN中查询相对于的拦截器。

参考

Proxy - ECMAScript 6 入门 (ruanyifeng.com)

Proxy() 构造器 - JavaScript | MDN (mozilla.org)

《Vue.js 设计与实现》

《深入浅出 Vue.js》