Vue 的核心之一就是响应式系统,通过侦测数据的变化,来驱动更新视图。
实现可响应对象的方式
通过可响应对象,实现对数据的侦测,从而告知外界数据变化。实现可响应对象的方式:
1.getter 和 setter
2.defineProperty
3.Proxy
关于前两个 API 的使用方式不多赘述,单一的访问器 getter/setter
功能相对简单,而作为 Vue2.x 实现可响应对象的 API - defineProperty
, API 本身存在较多问题。
Vue2.x 中,实现数据的可响应,需要对 Object
和 Array
两种类型采用不同的处理方式。 Object
类型通过 Object.defineProperty
将属性转换成 getter/setter
,这个过程需要递归侦测所有的对象 key
,来实现深度的侦测。
为了感知 Array
的变化,对 Array
原型上几个改变数组自身的内容的方法做了拦截,虽然实现了对数组的可响应,但同样存在一些问题,或者说不够方便的情况。 同时,defineProperty
通过递归实现 getter/setter
也存在一定的性能问题。
更好的实现方式是通过 ES6
提供的 Proxy API
。
Proxy API 的一些细节
Proxy API 具有更加强大的功能, 相比旧的 defineProperty
API ,Proxy
可以代理数组,并且 API 提供了多个 traps
,可以实现诸多功能。
这里主要说两个trap: get
、 set
, 以及其中的一些比较容易被忽略的细节。
细节一:trap 默认行为
let data = { foo: 'foo' }
let p = new Proxy(data, {get(target, key, receiver) {return target[key]},set(target, key, value, receiver) {console.log('set value')target[key] = value // ?}
})
p.foo = 123
// set value
通过 proxy
返回的对象 p
代理了对原始数据的操作,当对 p
设置时,便可以侦测到变化。但是这么写实际上是有问题, 当代理的对象数据是数组时,会报错。
let data = [1,2,3]
let p = new Proxy(data, {get(target, key, receiver) {return target[key]},set(target, key, value, receiver) {console.log('set value')target[key] = value}
})
p.push(4) // VM438:12 Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'
将代码更改为:
let data = [1,2,3]
let p = new Proxy(data, {get(target, key, receiver) {return target[key]},set(target, key, value, receiver) {console.log('set value')target[key] = valuereturn true}
})
p.push(4)
// set value // 打印2次
实际上,当代理对象是数组,通过 push
操作,并不只是操作当前数据,push
操作还触发数组本身其他属性更改。
let data = [1,2,3]
let p = new Proxy(data, {get(target, key, receiver) {console.log('get value:', key)return target[key]},set(target, key, value, receiver) {console.log('set value:', key, value)target[key] = valuereturn true}
})
p.push(1)
// get value: push
// get value: length
// set value: 3 1
// set value: length 4
先看 set
操作,从打印输出可以看出,push
操作除了给数组的第 3
位下标设置值 1
,还给数组的 length
值更改为 4
。 同时这个操作还触发了 get 去获取 push
和 length
两个属性。
我们可以通过 Reflect
来返回 trap 相应的默认行为,对于 set 操作相对简单,但是一些比较复杂的默认行为处理起来相对繁琐得多,Reflect
的作用就显现出来了。
let data = [1,2,3]
let p = new Proxy(data, {get(target, key, receiver) {console.log('get value:', key)return Reflect.get(target, key, receiver)},set(target, key, value, receiver)