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

首先我们分别测试以下数组常见的读操作: - 下标访问 - 获取长度 - 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}
首先看下面常见的写操作: - 通过下标修改元素 - 设置超出数组长度下标元素的元素值
注意:当你设置超出数组长度下标的元素值,这个操作在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等等其实现思路是相似的。