vue中的数据劫持
在浏览一篇博文的时候,看到里面提到了vue中数据劫持的概念,之前只是知道有这个东西,知道这个东西是vue的核心之一,是实现数据双向绑定的重要原理,但并未深入研究,那么今天就借这篇文章学习整理一下vue中的数据劫持到底是什么。
在面经中最常见的问题之一就是,你知道双向绑定吗,知道什么是MVVM吗?
学术性的回答模板有很多,其实简单来说就是数据和视图其中一方做出修改,另一方也随之变动。视图能够驱动数据,数据也能驱动视图。
视图驱动数据可以通过事件绑定来实现,那么数据驱动视图呢?
方法就是,给数据添加监听,一旦数据发生变化,就执行视图的修改操作,这个过程就是数据劫持。
一段简单的vue代码
<template>
<div>
<input v-model="message">
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello World'
};
}
};
</script>
那么vue中的数据劫持究竟是怎么实现的呢?
其实就是通过 Object.defineProperty
Object.defineProperty
先来了解一下Object.defineProperty
:
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
语法:
Object.defineProperty(obj, prop, descriptor)
参数:
obj
: 要定义属性的对象。
prop
:要定义或修改的属性的名称或Symbol
。
descriptor
:要定义或修改的属性描述符。
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter
函数和 setter
函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
var message = 'hello world';
const data = {};
Object.defineProperty(data, 'message', {
get() {
return message;
},
set(newVal) {
message = newVal;
}
});
data.message // 'hello world'
data.message = 'test' // 'test'
那么在vue2.x中,要想实现data中所有属性都实现数据劫持,就要先遍历data中的所有属性,对每一个属性都使用Object.defineProperty
,当属性的值发生变化时,就执行视图渲染操作。
参考一个vue数据劫持的简单实现(原理示例,非源码)
const data = {
name: '数据劫持',
info: {
address: '北京'
},
numbers: [1, 2, 3, 4]
}
在observerObject
中对传入的每一个属性使用 Object.defineProperty
进行监听,如果传入的是一个对象则递归调用 observe
遍历对象的每一个属性,确保 data
中的所有属性都加入监听。
function observerObject(target, name, value) {
if (typeof value === 'object' || Array.isArray(target)) {
observer(value);
}
Object.defineProperty(target, name, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
if (typeof value === 'object' || Array.isArray(value)) {
observer(value)
}
value = newVal
}
renderView() //模拟视图渲染操作
}
})
}
observe中遍历对象中的所有属性。
function observer(target) {
if (typeof target !== 'object' || !target) {
return target
}
for (const key in target) {
if (target.hasOwnProperty(key)) {
const value = target[key]
observerObject(target, key, value)
}
}
}
observer(data)
可以看到,由于 Object.defineProperty
每次只能设置一个具体的属性,因此需要进行递归遍历操作,如果数据层级很深,就会造成性能隐患。
另外,Object.defineProperty
只能作用在对象上,那么对于数组数据应该如何处理呢?
数组其实也可以看作一宗特殊的对象,其下标就是对应的属性,理论上也可以使用Object.defineProperty
进行数据劫持,但在vue中并没有选择这么做。而是选择劫持一些数组的常用操作方法,通过修改数组的原型方法,达到监听数组数据变化的目的。
//不能直接篡改Array.prototype对象,这样会对所有的数组实例都产生影响,需要通过原型继承得到一个新的原型对象
const oldArrayProperty = Array.prototype
const newArrayProperty = Object.create(oldArrayProperty)
const methods = ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse']
methods.forEach((method) => {
newArrayProperty[method] = function() {
renderView()
oldArrayProperty[method].call(this, ...arguments)
}
})
// 在observer函数中加入数组的判断,如果传入的是数组,则改变数组的原型对象为我们修改过后的原型。
if (Array.isArray(target)) {
target.__proto__ = newArrayProperty
}
在vue3版本中,选择使用了proxy
去实现对象的监听,避免了一些Object.defineProperty
方法本身带来的问题。