响应式就是 组件data的数据一旦发生变化,立刻触发视图的更新。
Vue2.x 中使用 Object.defineProperty
实现响应式,Object.defineProperty
存在一些缺点(后面会提到),因此 Vue3.0 中改用 Proxy
实现响应式。虽然 Vue3.0 来了,但是目前也不可能完全舍弃 Object.defineProperty
,因为 Proxy
的兼容性不好,且无法使用 polyfill
,Vue2.x 肯定还会存在一段时间,因此我们还是需要好好学习 Vue2.x 中是如何实现响应式的。
前面提到,Vue2.x 实现响应式的核心 API 是 Object.defineProperty
。因为访问器属性不能直接定义,必须使用 Object.defineProperty
方法。这个方法接收三个参数:属性所在对象、属性的名字、一个描述符对象。看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| var book = { _year: 2021 };
Object.defineProperty(book, 'year', { get: function() { return this._year; }, set: function(newValue) { if (newValue > 2021) { this._year = newValue; } } })
book.year = 2010; console.log(book.year);
|
首先看一下如何监听对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| function updataView() { console.log('视图更新'); }
function defineReactive(target, key, value) { Object.defineProperty(target, key, { get:function() { return value; }, set: function(newVal) { if (newVal != value) { value = newVal;
updataView(); } } }) }
function observer(target) { if (typeof target !== 'object' || target === null) { return target; }
for(let key in target) { defineReactive(target, key, target[key]) } }
const data = { name: 'lily', age: 20, info: { address: '北京' } }
observer(data);
data.name = 'tom'; data.age = 24; data.x = 100; delete data.name; data.info.address = '上海'
|
以上可以对数据对象进行浅监听。我们发现这个这个方式无法监听到新增属性和删除属性,这是 Object.defineProperty
的缺点之一(因此 Vue 中 还提供了 Vue.set 和 Vue.delete)。
以上代码无法深度监听,但是我们只需要改造以下 defineReactive 函数,就可以实现深度监听:
再测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const data = { info: { address: '北京' }, nums: [1, 2, 3] }
observer(data);
data.info.address = '上海' data.nums.push(4)
|
我们递归地对对象进行监听,这样就实现了深度监听。这里也就可以看出 Object.defineProperty
的第二个缺点:深度监听是需要递归到底,一次性计算量很大。
Object.defineProperty
的第三个缺点是无法原生监听数组,如果要监听数组,需要再进行特殊处理。具体方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const oldArrayPrototype = Array.prototype;
const arrProto = Object.create(oldArrayPrototype);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => { arrProto[methodName] = function() { updataView(); oldArrayPrototype[methodName].call(this, ...arguments) } })
|
再改造一下 observer 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function observer(target) { if (typeof target !== 'object' || target === null) { return target }
if (Array.isArray(target)) { target.__proto__ = arrProto }
for (let key in target) { defineReactive(target, key, target[key]) } }
|
一定不可以直接修改 Array 原型!这样会造成全局的 Array 原型被污染!因此我们才需要重新定义一个数组原型。此时就可以实现对数组的监听了。
总结一下
Object.defineProperty
的缺点总结:① 深度监听,需要递归到底,一次性计算量大;② 无法监听新增属性、删除属性(所以有 Vue.set、Vue.delete);③ 无法原生监听数组,需要特殊处理。
实现深度监听对象与监听数组的完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| function updataView() { console.log('视图更新'); }
const oldArrayPrototype = Array.prototype;
const arrProto = Object.create(oldArrayPrototype);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => { arrProto[methodName] = function() { updataView(); oldArrayPrototype[methodName].call(this, ...arguments) } })
function defineReactive(target, key, value) { observer(value)
Object.defineProperty(target, key, { get: function () { return value; }, set: function (newVal) { if (newVal != value) { observer(value)
value = newVal;
updataView(); } } }) }
function observer(target) { if (typeof target !== 'object' || target === null) { return target; } if (Array.isArray(target)) { target.__proto__ = arrProto; }
for (let key in target) { defineReactive(target, key, target[key]) } }
const data = { name: 'lily', age: 20, info: { address: '北京' }, nums: [1, 2, 3] }
observer(data);
data.name = 'tom' data.info.address = '上海' data.nums.push(4)
|