Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue 2.x 响应数组的更新 #34

Open
lovelmh13 opened this issue Feb 12, 2020 · 0 comments
Open

Vue 2.x 响应数组的更新 #34

lovelmh13 opened this issue Feb 12, 2020 · 0 comments

Comments

@lovelmh13
Copy link
Owner

lovelmh13 commented Feb 12, 2020

代码接上一篇,Vue的响应式原理

我们已经做好了数据的响应式了,但是如果我们对数组进行这样的操作vm.arr.push(4);push会改变数组本身,按理说数组被改变了,应该会触发视图的更新。但是我们会发现,现在没有触发Object.defineProperty set()的视图更新的。

我们需要对可以改变数组的方法进行重写。这里使用函数劫持、切片编程的思想来做。

首先,需要先对数组进行处理。回到class Observe中来

import {arrayMethods} from './array';

class Observe {
	constructor(data) {
		if (Array.isArray(data)) {	// 如果是数组,需要重写那些可以改变数组本身的方法,才能实现响应式
			data.__proto__ = arrayMethods	// 如果是数组,改变数组原型,让数组去我重写的原型上找方法
		} else {
			this.walk(data);
		}
	}

	// 遍历data里的属性,给每个属性都用defineProperty重新定义
	walk(data) {
		let keys = Object.keys(data);
		for (let i = 0; i < keys.length; i++) {
			let key = keys[i];
			let value = data[key];
			defineReactive(data, key, value);
		}
	}
}

在这里,我们改变了数组的原型链,当vue.data中的数组调用方法的时候,会从我们自己定义的arrayMethods上面来查找。

let oldArrayProtoMethods = Array.prototype;  // 1. 先保存数组本来的那些方法

// 2. 创建一个新对象(为的是只改变vue.data里的数组的方法,不改变所有Array的方法),使其还可以找到老对象的方法(在他的__proto__上,通过原型链还可以找的到)
export let arrayMethods = Object.create(oldArrayProtoMethods); // Object.create会把oldArrayProtoMethods的属性,挂在到arrayMethods的__proto__上

let methods = [	// 只需要重写这些函数,因为只有这些才会改变数组本身
	'push',
	'pop',
	'unshift',
	'shift',
	'slot',
	'splice',
	'reverse'
]

// 3. 改变 arrayMethods 中的这些个方法
methods.forEach((method) => {
	// 直接给arrayMethods上添加以上方法,就不会去原型链上找老的方法了。但是其他方法依然可以通过原型链找的到
	arrayMethods[method] = function(...args) {	// 这叫:函数劫持,切片编程
		// 重写这些个函数,还是先执行原函数,再在后面可以加我自己的逻辑
		let r = oldArrayProtoMethods[method].apply(this, args)	// 改变上下文,变成this(当前的数组)来调用这个方法,不然oldArrayProtoMethods[method]只是一个函数,没有人来调用它
		console.log('调用数组更新方法');
		return r;
	}
});

现在,如果我们再用方法改变数组vm.arr.push(4), 就会打印出来'调用数组更新方法',可见,我们已经打入了数组内部。

但是,如果我们新加到数组里的是一个对象呢:vm.arr.push({age: 11}),我们在获取arr[3].age的时候,就会发现,新加的age没有触发监听。

所以,需要给新加的数组的项,也进行observe观察。

function observerArray(inserted) {	// 循环新增的数组,给每一项进行observe检查
	inserted.forEach((item) => {
		observe(item);
	})
}

methods.forEach((method) => {
	arrayMethods[method] = function(...args) {
		let r = oldArrayProtoMethods[method].apply(this, args)
		console.log('调用数组更新方法');
		// 如果新增的元素是对象,那么还需要对对象进行观察
		let inserted = undefined;
		switch(method) {	// 找出可以让数组新增的方法,来给新增的数据加观察
			case 'push':
			case 'unshift':
				inserted = args;
				break;
			case 'splice':
				inserted = args.splice(2);
				break;
			default:
				break;
		}
		if (inserted) {
			observerArray(inserted);
		}
		return r;
	}
});

如上代码,修改一下methods.forEach,给每个新增的项,加上observe观察。就可以监听到新加的对象的获取和修改了。

这就是为什么要切片编程,改变原来数组的方法的原因,我们可以控制数组里的方法。

还有一个问题,如果我们传入的arr里本来就有数组呢?

data() {
	return {
		arr: [{ name: 'lmh', age: 24 }, 1, 2, 3]
	}
}

console.log(vm.arr[0].name = 'zt')

会发现,更改了name的值,并不会触发视图的更新。
所以,还需要对数组本身的值,进行observe观察

看回class Observe

import { arrayMethods, observerArray } from './array';
class Observe {
	constructor(data) {
		if (Array.isArray(data)) {
			// 如果是数组,需要重写那些可以改变数组本身的方法,才能实现响应式
			data.__proto__ = arrayMethods; // 如果是数组,改变数组原型,让数组去我重写的原型上找方法
			// 但是,上面那句话只能拦截数组方法,如果数组里是对象,那么还需要得数组里的每一项做观察
			observerArray(data);	// 观测数组中的每一项
		} else {
			this.walk(data);
		}
	}

	// 遍历data里的属性,给每个属性都用defineProperty重新定义
	walk(data) {
		let keys = Object.keys(data);
		for (let i = 0; i < keys.length; i++) {
			let key = keys[i];
			let value = data[key];
			defineReactive(data, key, value);
		}
	}
}

只需要在原来的基础上,把刚才写过的observerArray,在这里执行以下,就可以对数组本身的每一项进行观测了

再补充一下,如果我们设置的值,是对象,那么还应该进行监控:

import { observe } from './index';

// 定义响应式的数据变化,响应式最核心的地方
export function defineReactive(data, key, value) {
	observe(value); // 递归,如果value还是对象,那么同样给里面的属性都重新定义成响应式的
	Object.defineProperty(data, key, {
		// 定义、修改data上的,key属性
		get() {
			console.log('获取数据');
			return value;
		},
		set(newValue) {
			if (newValue === value) {
				return;
			}
			// 这里新加一个对设置的值的监控
			observe(newValue);	// 如果设置的值,是一个对象,还应该进行监控
			value = newValue;
			console.log('更新视图');
		}
	});
}

缺陷

虽然做了以上的处理,但是我们知道,有两种方式还是不能触发观察

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

比如:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

解决第一类问题,可以用this.$set来实现

vm.$set(vm.items, indexOfItem, newValue)

也可以用splice来实现,因为this.$set就是利用splice实现的,我们改变了vm下的splice方法,使其可以被观察

vm.items.splice(newLength)

第二类问题,可以用splice来实现

vm.items.splice(newLength)

分清

数组并不是响应式的,我们只是拦截了那些会改变数组本身的方法和监控了数组里的每一项而已

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant