之前的流程已经是对对象
进行了监听、读、写
,那数组也是数据,也需要被监听,那数组的监听和普通的监听有什么区别呢?那就慢慢试吧!
分析分析读
。
- 下标
js
function fn() {
state[1]
}
fn()//【get】 1
读下标没问题。
- length
js
function fn() {
state.lenght
}
fn()//【get】 length
读length没问题
- for 循环
js
function fn() {
for(let i =0;i<state.length;i++){
state[i]
}
}
遍历读到了length
和下标
,没问题。依赖重复收集的事后面再说。
- for of
js
for (const item of state) {
}
到现在也没毛病。
那读
还有什么呢? 数组的方法,那一大堆算不算读呢?
- includes
js
function fn() {
state.includes(1);
}
现在分析一下这些收集对不对。includes
是一个方法,如果将来给这个方法改成新的东西,会不会影响函数的运行结果?肯定会,所以应该被收集。length
不用说了,现在判断1在不在数组了,数组长度变为0,那就直接影响函数了,所以应该被收集。0
下标也应该收集。
- lastIndexOf
js
function fn() {
state.lastIndexOf(1);
}
前面两个不用看了,那has
应该不应该收集呢?它在内部做了一个判断,判断某个下标在这个数组里存不存在,那这个收集是有意义的,因为当稀疏数组的时候,下标是不存在的。
现在看起来数组的读
好像没啥了,但如果数组里面有对象呢?
js
const obj = {};
const arr = [1, {}, 3];
const state = reactive(arr);
function fn() {
var i = state.indexOf(obj);
}
结果是 -1
,没找到?这是为什么呢?在调用方法的时候,是在源对象里查找还是代理对象里查找?很明显是代理对象啊,也就意味着这个方法里面的this
指向的是代理对象。先输出来看看。
js
function fn() {
var i = state.indexOf(obj);
console.log('state[1]',state[1]);
console.log('arr[1]',arr[1]);
}
我们发现代理对象的那一项是个代理对象,因为我们之前有这么一个处理,当得到的结果是对象,那又进行一次响应式处理。
js
function get(target, key, receiver) {
track(target, TrackOpTypes.GET, key); //依赖收集
const result = Reflect.get(target, key, receiver); //返回对象属性值
if (isObject(result)) {
return reactive(result);
}
return result;
}
所以,当在代理对象里去查找的时候,找不到,因此我们可以有两个方案。
- 把传入的对象转化为代理对象。
- 当在代理对象里找不到时,再去原始数组里找一次。
这里vue
官方使用了第二种。那我就来看看如何修改数组的方法
。
分析一下,通过依赖收集也发现了,在使用这个方法的时候,会掉进get
陷阱,那就可以在get
那里处理。
js
//handlers.js
const arrayInstrumentations = {
includes: () => {},
indexOf: () => {},
lastIndexOf: () => {},
};
//读取
function get(target, key, receiver) {
track(target, TrackOpTypes.GET, key); //依赖收集
//如果是数组,且调用了数组方法
if (arrayInstrumentations.hasOwnProperty(key) && Array.isArray(target)) {
return arrayInstrumentations[key]
}
const result = Reflect.get(target, key, receiver); //返回对象属性值
if (isObject(result)) {
return reactive(result);
}
return result;
}
这就get
里面的判断了,让它去执行我们修改过的数组方法。现在就是来修改数组的方法了,不过这些数组每一个的处理逻辑都一样。
js
const arrayInstrumentations = {};
["includes", "indexOf", "lastIndexOf"].forEach((key) => {
arrayInstrumentations[key] = function (...args) {
//1.正常查找 在原型上找
//2.找不到 在原始对象上找
};
});
共两个步骤,先正常找,找不到再在原始对象上找。
- 正常找
原型上有数组的方法。
js
["includes", "indexOf", "lastIndexOf"].forEach((key) => {
arrayInstrumentations[key] = function (...args) {
//1.正常查找 在原型上找
const res = Array.prototype[key].apply(this, args);
};
});
现在是includes
方法,所以res = false
。
- 在原始对象上找 其实还是执行上面的代码,不过需要修改
this
的指向,让它指向原始对象,那这里如何拿到原始对象呢?读属性是不是会进入get
陷阱,而get
陷阱里是不是有原始对象?那就好办了啊。例如:
js
if (res < 0 || res === false) {
Array.prototype[key].apply(this.fff, args); //读属性 触发`get`
}
//读取
function get(target, key, receiver) {
console.log("key", key); //fff
}
所以,先有一个特殊的属性名,让原始对象的方法去读。
js
["includes", "indexOf", "lastIndexOf"].forEach((key) => {
arrayInstrumentations[key] = function (...args) {
console.log("args", args);
//1.正常查找 在原型上找
const res = Array.prototype[key].apply(this, args);
//找不到 在原始对象上找
if (res < 0 || res === false) {
return Array.prototype[key].apply(this[sy], args); //读属性 触发`get`
}
return res;
};
});
//读取
function get(target, key, receiver) {
if (key === sy) {
return target;
}
}
到这里读
就差不多了,下面来看看写
。
分析分析写
在数组里有哪些会改动数组?最直接的就是改动下标了。
js
function fn() {
state[0] = 4; // set 0
}
没毛病,那如果是超过数组的长度呢?
js
function fn() {
state[5] = 4; // add 0
}
合理吧,但是不完整,整个数组的长度变了,其中有些是稀疏项。那长度变了怎么没触发 set length
呢?官方文档说了,当设置的的下标大于数组的长度,那就会执行一个Object.defineProperty(obj,'length',value)
,这并没触发length
属性,而是隐式修改,所以不会触发set
的执行,所以我们得自己处理了。
得满足几个条件:
- 设置的对象是一个数组。
- 设置前后数组的length有变化。
- 设置的不是length属性。
当三个条件都满足了,手动触发length属性的变化。
js
//修改
function set(target, key, value, receiver) {
const type = target.hasOwnProperty(key)
? TriggerOpTypes.SET
: TriggerOpTypes.ADD;
const oldValue = target[key];
const oldLen = Array.isArray(target) ? target.length : undefined; //获取旧数组长度
const result = Reflect.set(target, key, value, receiver);
//赋值失败
if (!result) {
return result;
}
const newLen = Array.isArray(target) ? target.length : undefined;
//当属性值发生变化 或 新增属性 时
if (hasChange(oldValue, value) || type === TriggerOpTypes.ADD) {
trigger(target, type, key); //派发更新
//手动触发更新 set
if (Array.isArray(target) && oldLen !== newLen) {
if (key !== "length") {
trigger(target, TriggerOpTypes.SET, "length");
}
}
}
return result;
}
修改数组下标是没问题,那看看直接修改数组的length
。 当把length放大
,得到的是一个稀疏数组,并且触发了set
,数组原来值不变,没毛病。当把length缩小
呢?触发了set
,同时把数组后几项给干掉了,属性发生了改变,但没有触发delete
啊。所以还是得手动触发。
js
//手动触发更新 set
if (Array.isArray(target) && oldLen !== newLen) {
if (key !== "length") {
trigger(target, TriggerOpTypes.SET, "length");
} else {
//找到哪些被删除的下标,依次触发配发更新
for (let i = newLen; i < oldLen; i++) {
trigger(target, TriggerOpTypes.DELETE, i.toString());
}
}
}
在调用push
方法时,派发更新是合理的,触发了add 3
和set length
。但进行了两个依赖收集get push
和get length
。我的目的就是为了改动这个数组,去派发更新。我不需要知道内部是怎么实现的,就是我添加了,就要派发更新,但现在却进行了依赖收集,这超出了开发者的预期。
这就难搞了,数组变动的话我只想派发更新。那就有两种方法:
- 把会对数组产生改动的方法全部重写
- 调用这些会改动数组的方法期间,停止依赖收集。
那vue
使用的是第二种,因为第一种重写是完全的重写,太麻烦了哈。
js
["pop", "push", "shift", "unshift", "splice"].forEach((key) => {
arrayInstrumentations[key] = function (...args) {
pauseTracking(); //暂停依赖收集
let res = Array.prototype[key].apply(this, args);
resumeTracking(); //回复依赖收集
return res;
};
});
js
//effect.js
let shouldTrack = true;
export function pauseTracking() {
shouldTrack = false;
}
export function resumeTracking() {
shouldTrack = true;
}
export function track(target, type, key) {
//停止依赖收集
if (!shouldTrack) {
return;
}
if (type === TrackOpTypes.INTERATE) {
console.log(`【${type}】`);
return;
}
console.log(`【${type}】`, key);
}
到现在为止,包括对象、数组的监听以及读和写,也就是差不多了。