Vue数据劫持的实现,做一个自己的理解&简单总结。虽然Vue3.0即将到来,我想Vue2.x也不至于马上过时。
今天就从Vue2.x 与 Vue.3.0 数据劫持如何实现数据双向绑定。
数据劫持: 指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。
Vue2.x 选择的 Object.defineProperty
Object.defineProperty 对大家都来说应该不陌生了。算是面试的一道必考题?(细品:那掌握好了是不是就是一道送分题呢?)可以点击这里回顾一下 Object.defineProperty
的文档
我们来认清Object.defineProperty
的几个局限性
- 兼容性是IE8+,这也就是为什么Vue不支持IE8及以下版本的原因。
- 不能监听数组的变化,Vue通过重写数组原型的方法来实现数据劫持。
- 对于深层次嵌套对象需要做递归遍历。
- 必须遍历对象的每个属性。如果要扩展该对象,就必须手动去为新的属性设置setter、getter方法。 这也就是为什么Vue开发中的不在 data 中声明的属性无法自动拥有双向绑定效果的原因。需要我们手动去调用Vue.set()
我们做个类似Vue简易的数据劫持
- 视图更新触发的函数
// 当我们监听的数据发生变化后调用改函数
function update() {
console.log('数据变化啦,更新视图')
}
- 通过 Object.defineProperty 处理 data 中的每个属性
// 通过 Object.defineProperty 处理 target 中的每个属性 key
function defineReactive(target, key, value) {
Object.defineProperty(target, key, {
get() {
return value;
},
set(val) {
// 如果改变的数据和原来一样将不做任何处理
if (val !== value) {
// 数据更新了,调用update
update();
value = val;
}
}
})
}
- 监听data的函数
function observer(target) {
// 如果不是对象,直接返回;如果是null也直接返回
if (typeof target !== 'object' || !target) return target;
// 遍历对象obj的所有key,完成属性配置
Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
}
- 测试步骤1、2、3
// 需要监听的data对象
const data = {
level: 1,
info: {
name: 'cc'
}
}
// 调用监听函数监听 data
observer(data)
// 修改data的值 视图更新
data.level = 2
// 看到视图确实更新了
// 我们不妨尝试了一下data深层次对象的修改
data.info.name = 'yy'
// 控制台什么都是没有
- 想必你也发现了,监听data只到了对象的第一层。data深层次的数据,并没有被监听。所以我们需要对data做一个逐层遍历(递归),直到把每个对象的每个属性都调用
Object.defineProperty()
为止。
// 改改步骤二的代码
function defineReactive(target, key, value) {
// 在这里新增代码
// 当value为object我们再做一次数据监听,直到value不是object为止
if (typeof value === 'object') {
observer(value)
}
// 以下代码和步骤2没有区别
Object.defineProperty(target, key, {
get() {
return value;
},
set(val) {
// 如果改变的数据和原来一样将不做任何处理
if (val !== value) {
// 数据更新了,调用update
update();
value = val;
}
}
})
}
- 再对步骤5的修改做一次测试
const data = {
level: 1,
info: {
name: 'cc'
},
a: {
a: {
a: {
a: 1
}
}
}
}
// 我们尝试改变data.info.name的值
data.info.name = 'xy' // 视图更新了!
// 我们尝试跟深层次的修改
data.a.a.a.a = 2 // ok 视图也更新了
// 那么我再试试其他方式
// 先修改data.info的值
data.info = { name: 'cc' } // 没毛病,视图更新了,但此时data.info的指向已经发生了变化
// 然后再修改data.info.name
data.info.name = 'xy' // emmmmmm... 又是什么都没有
- 我们针对步骤5再做一次修改
// 修改步骤5的代码
function defineReactive(target, key, value) {
if (typeof value === 'object') {
observer(value)
}
Object.defineProperty(target, key, {
get() {
return value;
},
set(newVal) {
// 如果改变的数据和原来一样将不做任何处理
if (newVal !== value) {
// 在这里新增代码
// 如果设置newVal是object,对newVal做监听
if (typeof newVal === 'object') {
observer(newVal)
}
// 数据更新了,调用update
update();
value = newVal;
}
}
})
}
- 再对步骤7的修改做一次测试
const data = {
level: 1,
info: {
name: 'cc'
}
}
// 先修改data.info的值
data.info = { name: 'cc' } // 没毛病,视图更新了
// 然后再修改data.info.name
data.info.name = 'xy' // 也没毛病,视图更新了
- 我们都知道
typeof
数据返回的也是object
const data = {
arr: []
}
// 尝试对数组做更改
arr.push(1); // 然鹅,并没有任何输出
- 前面有说明Object.defineProperty 对数组是起不到任何作用的。那Vue如何实现的呢? Vue是通过修改数组的原型方法来实现数据劫持(做一些视图更新、渲染的操作)。
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 遍历methods数组
methods.forEach(method => {
// 原生Array的原型方法
const originalArray = Array.prototype[method]
// 重写Array原型上对应的方式
Array.prototype[method] = function() {
// 做视图更新或者渲染操作
update();
// 视图更新了,调用对应的原生方法
// arguments 将该有的参数也传进来
originalArray.call(this, ...arguments);
}
})
- 又到了验证一下步骤10的时候啦!
const data = {
arr: []
}
data.arr.push(1) // 视图更新了
- 看了上面的代码,可能就有疑问了。我们明显直接修改的是 Array.prototype的方法。这样会导致一个问题。没有被监听的数组,也会触发update()。如下:
var normalArray = [];
normalArray.push(1); // wtf 竟然也触发了视图更新
结果明显不是我们想要的。我们希望的是:Array原有的方法保持不变,但是又要引用到原来的方法的实现。
我们可以简单地处理下啦。
①先修改步骤10的代码
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayList = []
// 遍历methods数组
methods.forEach(method => {
// 原生Array的原型方法
const originalArray = Array.prototype[method]
// 重写Array原型上对应的方式
arrayList[method] = function() {
// 做视图更新或者渲染操作
update();
// 视图更新了,调用对应的原生方法
// arguments 将该有的参数也传进来
originalArray.call(this, ...arguments);
}
})
②再修改步骤7的代码
function defineReactive(target, key, value) {
if (typeof value === 'object') {
// 通过链去找我们定义好的方法
if (Array.isArray(value)) {
value.__proto__ = arrayList
}
observer(value)
}
Object.defineProperty(target, key, {
get() {
return value;
},
set(val) {
// 如果改变的数据和原来一样将不做任何处理
if (val !== value) {
// 在这里新增代码,如果设置val是object,对val做监听
if (typeof val === 'object') {
// 通过链去找我们定义好的方法
if (Array.isArray(val)) {
val.__proto__ = arrayList
}
observer(val)
}
// 数据更新了,调用update
update();
value = val;
}
}
})
}
- 完整代码
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayList = []
// 遍历methods数组
methods.forEach(method => {
// 原生Array的原型方法
const originalArray = Array.prototype[method]
// 重写Array原型上对应的方式
arrayList[method] = function() {
// 做视图更新或者渲染操作
update();
// 视图更新了,调用对应的原生方法
// arguments 将该有的参数也传进来
originalArray.call(this, ...arguments);
}
})
// 当我们监听的数据发生变化后调用改函数
function update() {
console.log('数据变化啦,更新视图')
}
function observer(target) {
// 如果不是对象,直接返回
if (typeof target !== 'object' || !target) return target;
// 遍历对象obj的所有key,完成属性配置
Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
}
function defineReactive(target, key, value) {
if (typeof value === 'object') {
if (Array.isArray(value)) {
value.__proto__ = arrayList
}
observer(value)
}
Object.defineProperty(target, key, {
get() {
return value;
},
set(newVal) {
// 如果改变的数据和原来一样将不做任何处理
if (newVal !== value) {
// 在这里新增代码,如果设置newVal是object,对newVal做监听
if (typeof newVal === 'object') {
if (Array.isArray(newVal)) {
newVal.__proto__ = arrayList
}
observer(newVal)
}
// 数据更新了,调用update
update();
value = newVal;
}
}
})
}
const data = {
level: 1,
info: {
name: 'cc'
},
arr: []
}
observer(data)
// 自行打开注释行测试即可
// ①
// data.level = 2
// ②
// data.info.name = 'xy'
// ③
/*
data.info = {name: 'cc'}
data.info.name = 'xy'
*/
// ④
// data.arr.push(1)
// ⑤
/*
data.arr = []
data.arr.push(1)
*/
值得注意的是:数组不支持长度的修改,也不支持通过数组的索引进行更改。例如以下方式是不会触发视图更新,只有上面列举的7个方式或者直接替换一个新的数组才会触发视图更新。数组更新检测
data.arr.length = 3
data.arr[1] = 1
Vue3.0 选择的 Proxy
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
function update() {
console.log('数据变化啦,更新视图')
}
const data = {
level: 1,
info: {
name: 'cc'
},
arr: []
}
const handler = {
get(target, property) {
// 如果值为对象,在对该值进行数据劫持
if (typeof target[property] === 'object' && target[property] !== null) {
return new Proxy(target[property], handler)
}
return Reflect.get(target, property)
},
set(target, property, value) {
if (property === 'length') {
return true
}
update()
return Reflect.set(target, property, value)
}
}
const proxy = new Proxy(data, handler)
proxy.level = 2
proxy.info.name = 'yy'
proxy.arr.push(1)
proxy.arr[1] = 1
Proxy
最大的问题应该就是兼容性了,但是3.0
都准备发布了,我们值得简单一试~