前言
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 = 13console.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 的链指向 prototarget.__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 的链指向 prototarget.__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 来实现数据绑定响应式,将解决上述的缺点。

