在Observer的伪代码中我们模拟了如下代码:
class Observer { constructor() { // 响应式绑定数据通过方法 observe(this.data); } } export function observe (data) { const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { // 将data中我们定义的每个属性进行响应式绑定 defineReactive(obj, keys[i]); } } export function defineReactive () { // ...省略 Object.defineProperty get-set }
今天我们就进一步了解Observer里还做了什么事。
Array的变化如何监听?
data 中的数据如果是一个数组怎么办?我们发现Object.defineProperty对数组进行响应式化是有缺陷的。
虽然我们可以监听到索引的改变。
function defineReactive (obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: () => { console.log('我被读了,我要不要做点什么好?'); return val; }, set: newVal => { if (val === newVal) { return; } val = newVal; console.log("数据被改变了,我要渲染到页面上去!"); } }) } let data = [1]; // 对数组key进行监听 defineReactive(data, 0, 1); console.log(data[0]); // 我被读了,我要不要做点什么好? data[0] = 2; // 数据被改变了,我要渲染到页面上去!
但是defineProperty不能检测到数组长度的变化,准确的说是通过改变length而增加的长度不能监测到。这种情况无法触发任何改变。
data.length = 0; // 控制台没有任何输出
而且监听数组所有索引的的代价也比较高,综合一些其他因素,Vue用了另一个方案来处理。
首先我们的observe需要改造一下,单独加一个数组的处理。
// 将data中我们定义的每个属性进行响应式绑定 export function observe (data) { const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { // 如果是数组 if (Array.isArray(keys[i])) { observeArray(keys[i]); } else { // 如果是对象 defineReactive(obj, keys[i]); } } } // 数组的处理 export function observeArray () { // ...省略 }
那接下来我们就应该考虑下Array变化如何监听?
Vue 中对这个数组问题的解决方案非常的简单粗暴,就是对能够改变数组的方法做了一些手脚。
我们知道,改变数组的方法有很多,举个例子比如说push方法吧。push存在Array.prototype上的,如果我们能
能拦截到原型上的push方法,是不是就可以做一些事情呢?
Object.defineProperty
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。存取描述符是由getter-setter函数对描述的属性,也就是我们用来给对象做响应式绑定的。
虽然我们无法使用Object.defineProperty将数组进行响应式的处理,也就是getter-setter,但是还有其他的功能可以供我们使用。就是数据描述符,数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable
当且仅当该属性的writable为true时,value才能被改变。默认为 false。
因此我们只要把原型上的方法,进行value的重新赋值。
如下代码,在重新赋值的过程中,我们可以获取到方法名和所有参数。
function def (obj, key) { Object.defineProperty(obj, key, { writable: true, enumerable: true, configurable: true, value: function(...args) { console.log('key', key); console.log('args', args); } }); } // 重写的数组方法 let obj = { push() {} } // 数组方法的绑定 def(obj, 'push'); obj.push([1, 2], 7, 'hello!'); // 控制台输出 key push // 控制台输出 args [Array(2), 7, "hello!"]
通过如上代码我们就可以知道,用户使用了数组上原型的方法以及参数我们都可以拦截到,这个拦截的过程就可以做一些变化的通知。
Vue监听Array三步曲
接下来,就看看Vue是如何实现的吧~
第一步:先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。
第二步:对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
第三步:把需要被拦截的 Array 类型的数据原型指向改造后原型。
我们将代码进行下改造,拦截的过程中还是要将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变,然后我们再去做视图的更新等操作。