新建一个 dist/arr.html
页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app" style="color: red; background: yellow">{{ arr }}</div>
<script src="vue.js"></script>
<!-- <script src="<https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js>"></script> -->
<script>
const vm = new Vue({
data: {
arr: [1, 2, 3, {a: 1}],
},
});
vm.$mount("#app");
// vm.arr[0] = 100; // 这种方式能监控到吗?=> 不能,因为只重写了数组方法
// vm.arr.length = 100; // 这种也不行 => 没有监控长度变化
vm.arr.push(100); // 这种才行, 但目前没监控(我们之前只监控了 arr 这个属性,但 push 方法没监控,还需要实现)
</script>
</body>
</html>
所以我们需要 src/observe/array.js
中监听并更新:
// 我们希望重写数组种的部分方法
let oldArrayProto = Array.prototype; // 获取数组的原型
export let newArrayProto = Object.create(oldArrayProto); // 这样就可以 newArrayProto.__proto__ = oldArrayProto 取值原来的方法
// 找到所有的能修改原数组的方法
let methods = [
'push',
'unshift',
'pop',
'shift',
'splice',
'reverse',
'sort'
]; // concat, slice... 都不会改变原数组
methods.forEach(method => {
// [].push(); this 就是这个 []
newArrayProto[method] = function(...args) { // 重写了数组的方法
const result = oldArrayProto[method].call(this, ...args); // 内部调用原来的方法, 函数的劫持(切片函数)
// 还需要对新增的数据再次进行劫持
let inserted; // 新增的数组成员
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift': // [].unshift(1,2,3); [].push(1,2,3);
inserted = args;
break;
case 'splice': // [].splice(0,0,1,2);
inserted = args.slice(2);
default:
break;
}
if (inserted) { // 对新增的内容再次进行观测
ob.observeArray(inserted);
}
**// todo: 这里更新**
return result;
}
});
所以为了对已有的复杂对象属性(数组和对象)中的值也进行通知更新,我们还需要在 src/observe/index.js
中为每个对象和数组的 observer
添加 dep
收集器:
import { newArrayProto } from "./array";
import Dep from "./dep";
class Observer {
constructor(data) {
**// 给每个对象和数组的观察者 observer 也都增加收集功能
this.dep = new Dep(); // 所有 ob 都要增加dep**
// Object.defineProperty 只能劫持已经存在的属性,后增的,或者删除的,不知道
// vue 里边会为此单独写一些 api ($set $delete)
// data.__ob__ = this; // 不但让观测的数组能在成员是对象的情况下继续调用 observeArray 方法,还给数据加了一个标识,如果数据上有 __ob__ 则说明这个属性被观测过
// 但上边这种写法会导致死循环, 因为不管是走下边的 isArray 还是 isObject, 都会因为成员中有 __ob__ 导致无限循环
// 所以需要通过 Object.defineProperty 来设置 __ob__ 的不可迭代属性
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false, // 将 __ob__ 变成不可枚举
})
if (Array.isArray(data)) {
// 监听 push、 shift 等能修改数组本身的 7 种方法
// data.__proto__ = ?; // 需要保留数组原有的特性,只重写部分方法
data.__proto__ = newArrayProto;
this.observeArray(data); // 如果数组中放的是对象,可以监控到对象的变化
} else {
this.walk(data);
}
}
walk(data) { // 循环对象,对属性依次劫持
// "重新定义"属性(vue2 的性能瓶颈,性能差)
Object.keys(data).forEach(key => defineReactive(data, key, data[key])); // 单独导出一个 defineReactive ,后续可以单独使用
}
observeArray(data) { // 观测数组
data.forEach(item => observe(item));
}
}
// 属性劫持
export function defineReactive(target, key, value) { // 闭包(value),不销毁
**let childOb** = observe(value); // 有可能 value 还是个对象, 所以需要递归劫持, 达到对所有的对象都进行属性劫持的目的(性能瓶颈)
let dep = new Dep(); // 每一个属性都有一个dep (这些 dep 收集器是不销毁的, 因为闭包的存在)
Object.defineProperty(target, key, {
get() { // 取值的时候,执行 get
// console.log(`用户取值了${key}`);
if (Dep.target) {
dep.depend();
**if (childOb) { // childOb.dep 用来收集依赖
childOb.dep.depend(); // 让数组和对象本身也实现依赖收集
}**
}
return value;
},
set(newValue) { // 修改的时候,执行 set
// console.log(`用户设置了${key}为${newValue}`);
if (newValue === value) return;
observe(newValue); // 有可能用户这样操作: vm.address = {} 给属性赋值成一个对象或者赋值成一个新对象
value = newValue;
dep.notify(); // 通知更新
}
})
}
export function observe(data) {
// 对这个对象进行劫持
if (typeof data !== "object" || data == null) {
return; // 只对对象进行劫持
}
if (data.__ob__ instanceof Observer) { // 说明这个对象被代理过了
return data.__ob__;
}
// 如果一个对象被劫持过了,那就不需要再被劫持了(要判断一个对象是否被劫持,可以增添一个实例,用实例来判断是否被劫持)
return new Observer(data);
}
收集完后我们在 src/observe/array.js
中操作数据方法后进行通知:
// 我们希望重写数组种的部分方法
let oldArrayProto = Array.prototype; // 获取数组的原型
export let newArrayProto = Object.create(oldArrayProto); // 这样就可以 newArrayProto.__proto__ = oldArrayProto 取值原来的方法
// 找到所有的能修改原数组的方法
let methods = [
'push',
'unshift',
'pop',
'shift',
'splice',
'reverse',
'sort'
]; // concat, slice... 都不会改变原数组
methods.forEach(method => {
// [].push(); this 就是这个 []
newArrayProto[method] = function(...args) { // 重写了数组的方法
const result = oldArrayProto[method].call(this, ...args); // 内部调用原来的方法, 函数的劫持(切片函数)
// 还需要对新增的数据再次进行劫持
let inserted; // 新增的数组成员
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift': // [].unshift(1,2,3); [].push(1,2,3);
inserted = args;
break;
case 'splice': // [].splice(0,0,1,2);
inserted = args.slice(2);
default:
break;
}
if (inserted) { // 对新增的内容再次进行观测
ob.observeArray(inserted);
}
**ob.dep.notify(); // 数组变化了,通知对应的 watcher 实现更新逻辑**
return result;
}
});
这样一来,我们对数组进行 push
就能被正确渲染了:
dist/arr.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app" style="color: red; background: yellow">
{{ arr }}
</div>
<!-- <script src="vue.js"></script> -->
<script src="<https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js>"></script>
<script>
const vm = new Vue({
data: {
arr: [1, 2, 3, {a: 1}], // 给数组本身增加dep 如果数组新增了某一项 我可以触发dep更新
a: { a: 1 } // 给对象也增加dep, 如果后续用户增添了属性 我可以触发dep更新
},
});
vm.$mount("#app");
// vm.arr[0] = 100; // 这种方式能监控到吗?=> 不能,因为只重写了数组方法
// vm.arr.length = 100; // 这种也不行 => 没有监控长度变化
setTimeout(() => {
vm.arr.push(999);
}, 1000);
</script>
</body>
</html>
如果你想要通过索引的方式修改数组,也可以采用 vm.arr.__ob__.dep.notify();
手动调用数组的 observer
观察者的收集器的通知的方式,换句话说就是找到这对象(或数组)上的 dep
,让它去通知页面更新:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app" style="color: red; background: yellow">
{{ arr }}
</div>
<script src="vue.js"></script>
<!-- <script src="<https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js>"></script> -->
<script>
const vm = new Vue({
data: {
arr: [1, 2, 3, {a: 1}], // 给数组本身增加dep 如果数组新增了某一项 我可以触发dep更新
a: { a: 1 } // 给对象也增加dep, 如果后续用户增添了属性 我可以触发dep更新
},
});
vm.$mount("#app");
// vm.arr[0] = 100; // 这种方式能监控到吗?=> 不能,因为只重写了数组方法
// vm.arr.length = 100; // 这种也不行 => 没有监控长度变化
setTimeout(() => {
vm.arr[0] = 999;
**vm.arr.__ob__.dep.notify();**
}, 1000);
</script>
</body>
</html>
这也是 Vue $set
的原理。
现在看着功能已经实现,但是如果数组中还有数组,并且改变子数组时,就会发现又无法更新页面了: