前言
Vue 最显著的一个特点就是数据响应式,通过改变 Model(JavaScript 对象) 中数据的值,可以自动触发相应试图的更新。这也是 MVVM 框架的好处。
那如何实现数据响应式呢?
在 vue2.0 时,尤大使用了 ES5 Object.defineProperty
方法实现。
Object.defineProperty
**Object.defineProperty()**
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
就是说使用了 **Object.defineProperty()**
方法,允许添加和修改该对象的属性,这些属性的值可以被改变,也可以被删除。
// 使用方法
Object.defineProperty(obj, prop, description)
// obj: 要在其上定义属性的对象
// prop: 要定义或者修改的属性的名称
// description: 将被定义或者修改的属性描述符
// 属性描述符(数据描述符和存取描述符(get、set))
响应式代码实现
基础版(对象只有一层属性)
// 数据响应式
// 更新试图方法
function updateView () {
console.log('试图更新')
}
function observer (target) {
// 如果不是对象,则返回
if (typeof target !== 'object' || target === null) {
return target
}
// 遍历对象枚举
for (let key in target) {
// 重新定义属性
defineProperty(target, key, target[key])
}
}
// 重新定义属性
function defineProperty (target, key, value) {
Object.defineProperty(target, key, {
get () {
return value
},
set (newValue) {
if (newValue !== value) {
updateView()
value = newValue
}
}
})
}
// 定义对象
let data = {
name: 'zhoujiawei'
}
// 添加侦听器
observer(data)
// 改变属性值
data.name = 'new allen'
console.log(data.name) // new allen
对象属性多层嵌套
比如 data = { ``name: 'zhoujiawei', msg: { age: 12 }``}
// 数据响应式
// 更新试图方法
function updateView () {
console.log('试图更新')
}
function observer (target) {
// 如果不是对象,则返回
if (typeof target !== 'object' || target === null) {
return target
}
// 遍历对象枚举
for (let key in target) {
// 重新定义属性
defineProperty(target, key, target[key])
}
}
// 重新定义属性
function defineProperty (target, key, value) {
// ************ 递归遍历 *************
observer(value)
// *********************************
Object.defineProperty(target, key, {
get () {
return value
},
set (newValue) {
if (newValue !== value) {
updateView()
value = newValue
}
}
})
}
// 定义对象
let data = {
name: 'zhoujiawei',
msg: {
age: 12
}
}
// 添加侦听器
observer(data)
// 改变属性值
// data.name = 'new allen'
data.msg.age = 13
console.log(data.msg.age) // 14
属性特殊赋值操作
❌错误代码
// 数据响应式
// 更新试图方法
function updateView () {
console.log('试图更新')
}
function observer (target) {
// 如果不是对象,则返回
if (typeof target !== 'object' || target === null) {
return target
}
// 遍历对象枚举
for (let key in target) {
// 重新定义属性
defineProperty(target, key, target[key])
}
}
// 重新定义属性
function defineProperty (target, key, value) {
// ************ 递归遍历 *************
observer(value)
// *********************************
Object.defineProperty(target, key, {
get () {
return value
},
set (newValue) {
if (newValue !== value) {
updateView()
value = newValue
}
}
})
}
// 定义对象
let data = {
name: 'zhoujiawei',
msg: {
age: 12
}
}
// 添加侦听器
observer(data)
// *********** 改变属性值 ***********
data.msg = {
sex: 'man'
}
data.msg.sex = 'woman'
// ********************************
console.log(data.msg.sex) // man(这边应该是 woman 才是对的,但实际是 man)
这时候需要在 set 中添加 observer 方法。
Object.defineProperty(target, key, {
get () {
return value
},
set (newValue) {
if (newValue !== value) {
// *********************
observer(newValue)
// *********************
updateView()
value = newValue
}
}
})
改变数组
我们知道在 Vue2.0 中,改变数组比如:myArray[2] = 2
。给数组 myArray 添加一个值是不会触发视图更新的。但是使用一些能改变原数组的数组对象方法,则可以更新视图。
列举一下:push、pop、shift、unshift、reverse、sort、splice
Vue2.0 中通过对这几个数组方法的重写,进行函数劫持,在调用原函数之前,触发视图更新。
// 数据响应式
let ArrayProperty = Array.prototype; // Array的原型
let proto = Object.create(ArrayProperty) // 继承
;['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach((method) => {
proto[method] = function () {
// 更新视图
updateView()
ArrayProperty[method].call(this, ...arguments)
}
})
// 更新试图方法
function updateView () {
console.log('试图更新')
}
function observer (target) {
// 如果不是对象,则返回
if (typeof target !== 'object' || target === null) {
return target
}
if (Array.isArray(target)) {
// 如果是数组,将 target 的链指向 proto
target.__proto__ = proto
}
// 遍历对象枚举
for (let key in target) {
// 重新定义属性
defineProperty(target, key, target[key])
}
}
// 重新定义属性
function defineProperty (target, key, value) {
// ************ 递归遍历 *************
// 考虑对象属性多层嵌套
observer(value)
// *********************************
Object.defineProperty(target, key, {
get () {
return value
},
set (newValue) {
if (newValue !== value) {
// 考虑属性特殊赋值操作
observer(newValue)
updateView()
value = newValue
}
}
})
}
// 定义对象
let data = {
name: 'zhoujiawei',
msg: {
age: 12
}
}
// 添加侦听器
observer(data)
// *********** 改变属性值 ***********
data.msg = {
sex: 'man',
log: [1, 2, 3]
} // 更新第一次
data.msg.log.push(4) // 更新第二次
// ********************************
完整代码
// 数据响应式
let ArrayProperty = Array.prototype; // Array的原型
let proto = Object.create(ArrayProperty) // 继承
;['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach((method) => {
proto[method] = function () {
// 更新视图
updateView()
ArrayProperty[method].call(this, ...arguments)
}
})
// 更新试图方法
function updateView () {
console.log('试图更新')
}
function observer (target) {
// 如果不是对象,则返回
if (typeof target !== 'object' || target === null) {
return target
}
if (Array.isArray(target)) {
// 如果是数组,将 target 的链指向 proto
target.__proto__ = proto
}
// 遍历对象枚举
for (let key in target) {
// 重新定义属性
defineProperty(target, key, target[key])
}
}
// 重新定义属性
function defineProperty (target, key, value) {
// ************ 递归遍历 *************
// 考虑对象属性多层嵌套
observer(value)
// *********************************
Object.defineProperty(target, key, {
get () {
return value
},
set (newValue) {
if (newValue !== value) {
// 考虑属性特殊赋值操作
observer(newValue)
updateView()
value = newValue
}
}
})
}
// 定义对象
let data = {
name: 'zhoujiawei',
msg: {
age: 12
}
}
// 添加侦听器
observer(data)
// *********** 改变属性值 ***********
data.msg = {
sex: 'man',
log: [1, 2, 3]
} // 更新第一次
data.msg.log.push(4) // 更新第二次
// ********************************
Vue2.0 响应式缺点
- Vue 实例化后新增的属性,不会被监听。但可以使用
Vue.set(data, 'a', 1)
设置新属性(对象不存在的属性不能被拦截)。 - Object.defineProperty的一个缺陷是无法监听数组变化(数组的7个改变原数组本身能被监听)。同样可以使用
Vue.set
来设置数组项。 - 默认递归,有性能问题。
注:Vue3.0 将会使用 Proxy 来实现数据绑定响应式,将解决上述的缺点。