← 返回首页
数组的读和写
发表时间:2024-01-06 17:09:48
数组的读和写

数组的读和写。

上小节我们探讨了普通对象的监听和读和写。那么数组也是对象的一种,那数组类型的监听自然和普通对象并无异同,我们重点探讨数组的读和写。

本小节的代码结构图如下:

1.数组的读操作

首先我们分别测试以下数组常见的读操作: - 下标访问 - 获取长度 - for循环 - for of循环 - includes - lastIndexOf - indexOf

import {reactive} from './reactive.js'

const arr= ['zhangsan','lisi','wangwu'];

const state = reactive(arr);

function fn(){
    state[0];//下标访问
    state.length; //获取长度
    for(let i=0;i<state.length;i++){ //for循环
        state[i]
    }

    for(const i of state){ //for of循环

    }

    state.includes('zhangsan') //includes判断某个元素是否存在
    state.lastIndexOf('lisi') //获取某个元素最后一次出现的下标
    state.indexOf('lisi') //获取某个元素第一次出现的下标
}

fn();

注意:使用for of循环遍历出的元素是Symbol.iterator类不能转换为字符串,因此effect.js修改如下:

import {TrackTypes} from "./operations.js";

export function track(target,type,key){

    if(type === TrackTypes.ITERATE){
        console.log(`%c依赖收集:[${type}]`,'color:#f00');
        return;
    }
    console.log(`%c依赖收集:[${type}]`,'color:#f00',key);
}
export function trigger(target,type,key){
    console.log(`%c派发更新:[${type}]`,'color:#00f',key);
}

输出结果:

依赖收集:[get] 0
依赖收集:[get] length
依赖收集:[get] length
依赖收集:[get] 0
依赖收集:[get] length
依赖收集:[get] 1
依赖收集:[get] length
依赖收集:[get] 2
依赖收集:[get] length
依赖收集:[get] Symbol(Symbol.iterator)
依赖收集:[get] length
依赖收集:[get] 0
依赖收集:[get] length
依赖收集:[get] 1
依赖收集:[get] length
依赖收集:[get] 2
依赖收集:[get] length
依赖收集:[get] includes
依赖收集:[get] length
依赖收集:[get] 0
依赖收集:[get] lastIndexOf
依赖收集:[get] length
依赖收集:[has] 2
依赖收集:[get] 2
依赖收集:[has] 1
依赖收集:[get] 1
依赖收集:[get] indexOf
依赖收集:[get] length
依赖收集:[has] 0
依赖收集:[get] 0
依赖收集:[has] 1
依赖收集:[get] 1

如果数组中包含对象类型呢?看下面代码:

import {reactive} from './reactive.js'

const obj = {name: 'baobao',age:18};
const arr = ['zhangsan','lisi',obj,'wangwu'];

const state = reactive(arr);

function fn(){
   let index = state.indexOf(obj);
   console.log(index);
}

fn();

运行结果:

依赖收集:[get] indexOf
依赖收集:[get] length
依赖收集:[has] 0
依赖收集:[get] 0
依赖收集:[has] 1
依赖收集:[get] 1
依赖收集:[has] 2
依赖收集:[get] 2
依赖收集:[has] 3
依赖收集:[get] 3
-1

我们发现返回 -1,并未找到obj元素,除此之外includes,lastIndexOf都无法找到元素obj。原因是执行indexOf是state对象,已经是一个代理对象了,而不是原始对象。

我们可以测试输出代理之前和之后的数组第三个元素。

import {reactive} from './reactive.js'

const obj = {name: 'baobao',age:18};
const arr = ['zhangsan','lisi',obj,'wangwu'];
const state = reactive(arr);

function fn(){
   let index = state.indexOf(obj);
   console.log(index);

   console.log(state[2],arr[2]);
}
fn();

输出结果:

...
-1
依赖收集:[get] 2
Proxy(Object) {name: 'baobao', age: 18} {name: 'baobao', age: 18}

解决这个问题有以下两种思路: 1. 把传入的原始对象转换为代理对象。 2. 当无法从代理对象中找到时,去元素数组对象中重新再找一次。

vue的作者采纳了第二种方案。handlers.js修改如下:

import {track, trigger} from "./effect.js";
import {hasChanged, isObject} from "./utils.js";
import {reactive} from "./reactive.js";
import {TrackTypes, TriggerTypes} from "./operations.js";
/*
const arrayInstrumentation = {
    'indexOf':()=>{},
    'includes':()=>{},
    'lastIndexOf':()=>{}
}*/
const RAW = Symbol('raw');

const arrayInstrumentation = {};
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
    arrayInstrumentation[key] = function (...args) {
        //1.第一步正常查找
        const result = Array.prototype[key].apply(this, args); //这里this是代理对象。
        //2.找不到从元素对象中重新找一遍
        if(result<0 || result === false){
           return Array.prototype[key].apply(this[RAW], args);
        }
        return result;
    }
})

function get(target, key, receiver) {
    if(key === RAW){
        return target; //返回原始对象
    }
    //依赖收集
    track(target, TrackTypes.GET, key);
    /*
    if(key==='indexOf'||key==='includes'||key==='lastIndexOf'){
        return '改动之后的代码'
    }*/
    if (arrayInstrumentation.hasOwnProperty(key) && Array.isArray(target)) {
        return arrayInstrumentation[key];
    }

    //return target[key];
    //return Reflect.get(target,key,receiver); //receiver其实就是代理对象
    const result = Reflect.get(target, key, receiver);
    if (isObject(result)) {
        //如果是对象需要再次调用reactive返回代理对象。
        return reactive(result);
    }
    return result;
}

function set(target, key, value) {
    //target[key]=value;
    //return true; //不要忘了这里要返回一个true;表示赋值是否成功, 推荐使用反射Reflect,它本身返回就是布尔值。
    const oldValue = target[key];//获取旧的值。
    //派发更新
    const type = target.hasOwnProperty(key) ? TriggerTypes.SET : TriggerTypes.ADD;//获取操作类型
    const result = Reflect.set(target, key, value);//获取更新结果。
    if (!result) { //如果更新失败,直接返回。
        return result;
    }
    if (hasChanged(oldValue, value || type === TriggerTypes.ADD)) {
        trigger(target, type, key);
    }
    return result;
}

function has(target, key) {
    track(target, TrackTypes.HAS, key);
    return Reflect.has(target, key);
}

function ownKeys(target) {
    track(target, TrackTypes.ITERATE);
    return Reflect.ownKeys(target);
}

function deleteProperty(target, key) {
    const hasKey = target.hasOwnProperty(key); //存在要删除的属性
    const result = Reflect.deleteProperty(target, key); //并且删除成功了。
    if (hasKey && result) {
        trigger(target, TriggerTypes.DELETE, key);
    }
    return result;
}

export const handlers = {
    get,
    set,
    has,
    ownKeys,
    deleteProperty
}

运行结果:

2
依赖收集:[get] 2
Proxy(Object) {name: 'baobao', age: 18} {name: 'baobao', age: 18}

2.数组的写操作

首先看下面常见的写操作: - 通过下标修改元素 - 设置超出数组长度下标元素的元素值

注意:当你设置超出数组长度下标的元素值,这个操作在javascript中是合法的,相当于先修改了数组的长度,再添加一个新元素。

import {reactive} from './reactive.js'

const obj = {name: 'baobao',age:18};
const arr = ['zhangsan','lisi',obj,'wangwu'];
const state = reactive(arr);

function fn(){

   state[0]='张三丰'; //修改元素
   state[5] = 100;
}
fn();

运行结果:

派发更新:[set] 0
派发更新:[add] 5

但是我们发现,并没有触发修改数组长度length的派发更新。因此需要手工派发更新length属性,handlers.js修改如下:

import {track, trigger} from "./effect.js";
import {hasChanged, isObject} from "./utils.js";
import {reactive} from "./reactive.js";
import {TrackTypes, TriggerTypes} from "./operations.js";
/*
const arrayInstrumentation = {
    'indexOf':()=>{},
    'includes':()=>{},
    'lastIndexOf':()=>{}
}*/
const RAW = Symbol('raw');

const arrayInstrumentation = {};
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
    arrayInstrumentation[key] = function (...args) {
        //1.第一步正常查找
        const result = Array.prototype[key].apply(this, args); //这里this是代理对象。
        //2.找不到从元素对象中重新找一遍
        if(result<0 || result === false){
           return Array.prototype[key].apply(this[RAW], args);
        }
        return result;
    }
})

function get(target, key, receiver) {
    if(key === RAW){
        return target; //返回原始对象
    }
    //依赖收集
    track(target, TrackTypes.GET, key);
    /*
    if(key==='indexOf'||key==='includes'||key==='lastIndexOf'){
        return '改动之后的代码'
    }*/
    if (arrayInstrumentation.hasOwnProperty(key) && Array.isArray(target)) {
        return arrayInstrumentation[key];
    }

    //return target[key];
    //return Reflect.get(target,key,receiver); //receiver其实就是代理对象
    const result = Reflect.get(target, key, receiver);
    if (isObject(result)) {
        //如果是对象需要再次调用reactive返回代理对象。
        return reactive(result);
    }
    return result;
}

function set(target, key, value) {
    //target[key]=value;
    //return true; //不要忘了这里要返回一个true;表示赋值是否成功, 推荐使用反射Reflect,它本身返回就是布尔值。
    const oldValue = target[key];//获取旧的值。
    const oldLen = Array.isArray(target)? target.length: undefined;

    //派发更新
    const type = target.hasOwnProperty(key) ? TriggerTypes.SET : TriggerTypes.ADD;//获取操作类型
    const result = Reflect.set(target, key, value);//获取更新结果。
    if (!result) { //如果更新失败,直接返回。
        return result;
    }

    const newLen = Array.isArray(target)? target.length: undefined;
    if (hasChanged(oldValue, value || type === TriggerTypes.ADD)) {
        trigger(target, type, key);
        //如果是数组,并且旧的长度不等于新的长度;
        if(Array.isArray(target) && oldLen!== newLen ){
            //并且操作的不是length属性。
            if(key !=='length'){
                trigger(target,TriggerTypes.SET,'length'); //重新设置length的值
            }
        }
    }
    return result;
}

function has(target, key) {
    track(target, TrackTypes.HAS, key);
    return Reflect.has(target, key);
}

function ownKeys(target) {
    track(target, TrackTypes.ITERATE);
    return Reflect.ownKeys(target);
}

function deleteProperty(target, key) {
    const hasKey = target.hasOwnProperty(key); //存在要删除的属性
    const result = Reflect.deleteProperty(target, key); //并且删除成功了。
    if (hasKey && result) {
        trigger(target, TriggerTypes.DELETE, key);
    }
    return result;
}

export const handlers = {
    get,
    set,
    has,
    ownKeys,
    deleteProperty
}

运行结果:

派发更新:[set] 0
派发更新:[add] 5
派发更新:[set] length

如果我们直接修改数组的长度也能触发length属性的派发更新。但是我们一旦减小了数组长度则出问题了!如下:

import {reactive} from './reactive.js'

const obj = {name: 'baobao',age:18};
const arr = ['zhangsan','lisi',obj,'wangwu'];
const state = reactive(arr);

function fn(){
   state.length=10;
   state.length=3; //减小数组长度
   console.log(state);
}
fn();

运行结果:

派发更新:[set] length
派发更新:[set] length
Proxy(Array) {0: 'zhangsan', 1: 'lisi', 2: {…}}

数组长度从10减小到3,相当于把后七个元素都删除了,但是我们发现并没有触发delete元素操作的派发更新。 handlers.js修改如下:

import {track, trigger} from "./effect.js";
import {hasChanged, isObject} from "./utils.js";
import {reactive} from "./reactive.js";
import {TrackTypes, TriggerTypes} from "./operations.js";
/*
const arrayInstrumentation = {
    'indexOf':()=>{},
    'includes':()=>{},
    'lastIndexOf':()=>{}
}*/
const RAW = Symbol('raw');

const arrayInstrumentation = {};
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
    arrayInstrumentation[key] = function (...args) {
        //1.第一步正常查找
        const result = Array.prototype[key].apply(this, args); //这里this是代理对象。
        //2.找不到从元素对象中重新找一遍
        if(result<0 || result === false){
           return Array.prototype[key].apply(this[RAW], args);
        }
        return result;
    }
})

function get(target, key, receiver) {
    if(key === RAW){
        return target; //返回原始对象
    }
    //依赖收集
    track(target, TrackTypes.GET, key);
    /*
    if(key==='indexOf'||key==='includes'||key==='lastIndexOf'){
        return '改动之后的代码'
    }*/
    if (arrayInstrumentation.hasOwnProperty(key) && Array.isArray(target)) {
        return arrayInstrumentation[key];
    }

    //return target[key];
    //return Reflect.get(target,key,receiver); //receiver其实就是代理对象
    const result = Reflect.get(target, key, receiver);
    if (isObject(result)) {
        //如果是对象需要再次调用reactive返回代理对象。
        return reactive(result);
    }
    return result;
}

function set(target, key, value) {
    //target[key]=value;
    //return true; //不要忘了这里要返回一个true;表示赋值是否成功, 推荐使用反射Reflect,它本身返回就是布尔值。
    const oldValue = target[key];//获取旧的值。
    const oldLen = Array.isArray(target)? target.length: undefined;

    //派发更新
    const type = target.hasOwnProperty(key) ? TriggerTypes.SET : TriggerTypes.ADD;//获取操作类型
    const result = Reflect.set(target, key, value);//获取更新结果。
    if (!result) { //如果更新失败,直接返回。
        return result;
    }

    const newLen = Array.isArray(target)? target.length: undefined;
    if (hasChanged(oldValue, value || type === TriggerTypes.ADD)) {
        trigger(target, type, key);
        //如果是数组,并且旧的长度不等于新的长度;
        if(Array.isArray(target) && oldLen!== newLen ){
            //并且操作的不是length属性。
            if(key !=='length'){
                trigger(target,TriggerTypes.SET,'length'); //重新设置length的值
            }else{
                //找到哪些被删除的下标,依次触发派发更新
                for(let i=newLen;i<oldLen;i++){
                    trigger(target,TriggerTypes.DELETE,i.toString());
                }
            }
        }
    }
    return result;
}

function has(target, key) {
    track(target, TrackTypes.HAS, key);
    return Reflect.has(target, key);
}

function ownKeys(target) {
    track(target, TrackTypes.ITERATE);
    return Reflect.ownKeys(target);
}

function deleteProperty(target, key) {
    const hasKey = target.hasOwnProperty(key); //存在要删除的属性
    const result = Reflect.deleteProperty(target, key); //并且删除成功了。
    if (hasKey && result) {
        trigger(target, TriggerTypes.DELETE, key);
    }
    return result;
}

export const handlers = {
    get,
    set,
    has,
    ownKeys,
    deleteProperty
}

运行结果:

派发更新:[set] length
派发更新:[set] length
派发更新:[delete] 3
派发更新:[delete] 4
派发更新:[delete] 5
派发更新:[delete] 6
派发更新:[delete] 7
派发更新:[delete] 8
派发更新:[delete] 9
index.js:10 Proxy(Array) {0: 'zhangsan', 1: 'lisi', 2: {…}}

下来咱们测试数组的一些其它常用方法,例如: push,pop,slice等等,我们以push为例。

import {reactive} from './reactive.js'

const obj = {name: 'baobao',age:18};
const arr = ['zhangsan','lisi',obj,'wangwu'];
const state = reactive(arr);

function fn(){
   state.push(100);
}
fn();

运行结果:

依赖收集:[get] push
依赖收集:[get] length
派发更新:[add] 4
派发更新:[set] length

我们发现派发更新都正常,但是并不需要的对length属性进行依赖收集。解决这个问题有以下两种思路: 1. 把那些会对数组产生改动的方法(包括:push/pop/split/slice...)全部重写。 2. 暂停依赖收集。

vue作者这里采纳了第二种方式。

在effect.js里设计一个属性shouldTrack 用来控制是否需要暂停依赖收集。

import {TrackTypes} from "./operations.js";

let shouldTrack = true;
export function pauseTrack(){
    shouldTrack = false;
}

export function resumeTrack(){
    shouldTrack = true;
}

export function track(target,type,key){
    if(!shouldTrack){
        return;
    }

    if(type === TrackTypes.ITERATE){
        console.log(`%c依赖收集:[${type}]`,'color:#f00');
        return;
    }
    console.log(`%c依赖收集:[${type}]`,'color:#f00',key);
}
export function trigger(target,type,key){
    console.log(`%c派发更新:[${type}]`,'color:#00f',key);
}

handlers.js修改如下:

import {track, trigger,pauseTrack,resumeTrack} from "./effect.js";
import {hasChanged, isObject} from "./utils.js";
import {reactive} from "./reactive.js";
import {TrackTypes, TriggerTypes} from "./operations.js";
/*
const arrayInstrumentation = {
    'indexOf':()=>{},
    'includes':()=>{},
    'lastIndexOf':()=>{}
}*/
const RAW = Symbol('raw');

const arrayInstrumentation = {};
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
    arrayInstrumentation[key] = function (...args) {
        //1.第一步正常查找
        const result = Array.prototype[key].apply(this, args); //这里this是代理对象。
        //2.找不到从元素对象中重新找一遍
        if(result<0 || result === false){
           return Array.prototype[key].apply(this[RAW], args);
        }
        return result;
    }
});

['push','pop','shift','unshift','splice'].forEach(key=>{
    arrayInstrumentation[key]=function (...args){
        pauseTrack(); //暂停依赖收集
        const result = Array.prototype[key].apply(this,args);
        resumeTrack(); //恢复依赖收集
    }
})

function get(target, key, receiver) {
    if(key === RAW){
        return target; //返回原始对象
    }
    //依赖收集
    track(target, TrackTypes.GET, key);
    /*
    if(key==='indexOf'||key==='includes'||key==='lastIndexOf'){
        return '改动之后的代码'
    }*/
    if (arrayInstrumentation.hasOwnProperty(key) && Array.isArray(target)) {
        return arrayInstrumentation[key];
    }

    //return target[key];
    //return Reflect.get(target,key,receiver); //receiver其实就是代理对象
    const result = Reflect.get(target, key, receiver);
    if (isObject(result)) {
        //如果是对象需要再次调用reactive返回代理对象。
        return reactive(result);
    }
    return result;
}

function set(target, key, value) {
    //target[key]=value;
    //return true; //不要忘了这里要返回一个true;表示赋值是否成功, 推荐使用反射Reflect,它本身返回就是布尔值。
    const oldValue = target[key];//获取旧的值。
    const oldLen = Array.isArray(target)? target.length: undefined;

    //派发更新
    const type = target.hasOwnProperty(key) ? TriggerTypes.SET : TriggerTypes.ADD;//获取操作类型
    const result = Reflect.set(target, key, value);//获取更新结果。
    if (!result) { //如果更新失败,直接返回。
        return result;
    }

    const newLen = Array.isArray(target)? target.length: undefined;
    if (hasChanged(oldValue, value || type === TriggerTypes.ADD)) {
        trigger(target, type, key);
        //如果是数组,并且旧的长度不等于新的长度;
        if(Array.isArray(target) && oldLen!== newLen ){
            //并且操作的不是length属性。
            if(key !=='length'){
                trigger(target,TriggerTypes.SET,'length'); //重新设置length的值
            }else{
                //找到哪些被删除的下标,依次触发派发更新
                for(let i=newLen;i<oldLen;i++){
                    trigger(target,TriggerTypes.DELETE,i.toString());
                }
            }
        }
    }
    return result;
}

function has(target, key) {
    track(target, TrackTypes.HAS, key);
    return Reflect.has(target, key);
}

function ownKeys(target) {
    track(target, TrackTypes.ITERATE);
    return Reflect.ownKeys(target);
}

function deleteProperty(target, key) {
    const hasKey = target.hasOwnProperty(key); //存在要删除的属性
    const result = Reflect.deleteProperty(target, key); //并且删除成功了。
    if (hasKey && result) {
        trigger(target, TriggerTypes.DELETE, key);
    }
    return result;
}

export const handlers = {
    get,
    set,
    has,
    ownKeys,
    deleteProperty
}

至此,关于数组的读和写的依赖收集和派发更新我们就基本实现了,关于其它对象类型比如:Map,Set等等其实现思路是相似的。