本文基于Vue 3.2.30版本源码进行分析
为了增加可读性,会对源码进行删减、调整顺序、改变的操作,文中所有源码均可视作为伪代码
由于ts版本代码携带参数过多,不利于展示,大部分伪代码会采取js的形式展示,而不是原来的ts代码

本文内容

  1. Object数据响应式测试代码调试与Vue3对应源码总结,分为常见的读和写操作的相关响应式处理
  2. Array数据响应式测试代码调试与Vue3对应源码总结,分为常见的读和写操作的相关响应式处理
本篇文章不对ref类型shallow类型readonly类型进行总结分析
本篇文章主要集中在总结对于Vue3每一种数据如何实现读和写操作的响应式拦截,比如数组一些方法,为了实现响应式监听所做的特殊拦截处理
本文尽可能对源码中涉及到的读写操作进行列举和总结,难免会有遗漏

前置知识

在上一篇Vue3源码-响应式系统-依赖收集和派发更新流程浅析文章中,我们梳理了整个响应式系统依赖收集和派发更新的流程,还简单地介绍了Proxy以及Reflect,明白了响应式的基本原理就是拦截target一些方法,比如get,比如set,然后进行依赖收集和派发更新
但是我们并没有对Vue3中响应式数据类型的分类,响应式数据常见属性的拦截等源码进行分析,本篇文章将基于Vue3源码各种非原始值的数据响应式进行总结,主要研究的源代码核心部分如下所示:

function reactive(target) {
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    const targetType = getTargetType(target);
    if (targetType === 0 /* INVALID */) {
        return target;
    }
    const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
    return proxy;
}

const mutableHandlers = {
    get,
    set,
    deleteProperty,
    has,
    ownKeys
};
const mutableCollectionHandlers = {
    get: /*#__PURE__*/ createInstrumentationGetter(false, false)
};

new Proxy(target)中可以发现,响应式监听数据分为两种targetType=2以及targetType=1,从上面和下面代码块可以得知,当target=1时,即数据类型为Object/Array时,new Proxy(target, baseHandlers),本文将基于baseHandlers进行分析

function targetTypeMap(rawType) {
    switch (rawType) {
        case 'Object':
        case 'Array':
            return 1 /* COMMON */;
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
            return 2 /* COLLECTION */;
        default:
            return 0 /* INVALID */;
    }
}

Object

读操作

测试代码

具体代码可以查看github调试代码,有debugger断点,直接整个文件夹下载到本地运行object.html即可
const proxy = reactive({ count: 1, count1: 2 });
watchEffect(() => {
    console.error("object.count", proxy.count);
    for (let key in proxy) {
        console.warn("for in object", key);
    }

    console.info("key in object", "count" in proxy);
});

在effect中直接访问属性:proxy.count

触发Proxy.get()响应,进行track(target, "get", "count")的依赖收集

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver);
        if (!isReadonly) {
            track(target, "get" /* GET */, key);
        }
        if (isObject(res)) {
            return isReadonly ? readonly(res) : reactive(res);
        }
        return res;
    }
}

在effect中遍历访问proxy的key:for(let key in proxy)

触发Proxy.ownKeys()响应,进行track(target, "iterate", ITERATE_KEY)的依赖收集

使用ownKeys获取所有的key,不与具体的某一个key进行绑定,因此只能使用构建的唯一的key:ITERATE_KEY进行track跟踪
function ownKeys(target) {
    track(target, "iterate" /* ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
    return Reflect.ownKeys(target);
}

在effect中判断对象上是否存在指定的key:key in proxy

触发Proxy.has()响应,进行track(target, "iterate", "count")的依赖收集

function has(target, key) {
    const result = Reflect.has(target, key);
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
        track(target, "has" /* HAS */, key);
    }
    return result;
}

写操作

readonly类型会阻止写操作
effect中进行写操作,除非写操作本身有get操作,会触发依赖收集,否则跟写在effect外面差不多,下面分析会有同时触发写操作+读操作的API存在
proxy.count = proxy.count + 1是执行了get操作又执行了post操作,不属于同时触发写操作+读操作的API

测试代码

具体代码可以查看github调试代码,有debugger断点,直接整个文件夹下载到本地运行object.html即可
document.getElementById("testBtn").addEventListener("click", () => {
    proxy.count = proxy.count + 1;
});
var id = 0;
document.getElementById("testBtn1").addEventListener("click", () => {
    proxy["newKey" + id++] = 3;
});
document.getElementById("testBtn2").addEventListener("click", () => {
    delete proxy["newKey" + (id - 1)]
});

正常更新属性:proxy.count++

proxy.count+1触发了get请求,然后触发Proxy.set()响应,进行trigger(target, "set", "count")的派发更新

hasChanged(value, oldValue)会监测值是否发生了变化,如果没有发生变化,则不需要触发响应式的派发更新
function createSetter(shallow = false) {
    const hadKey = isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    if (hadKey && hasChanged(value, oldValue)) {
        trigger(target, "set" /* SET */, key, value, oldValue);
    }
}
function trigger(target, type, key, newValue, oldValue, oldTarget) {
    if (key !== void 0) {
        deps.push(depsMap.get(key));
    }
    if (isMap(target)) {
        // Object不是Map,因此不执行这段
        deps.push(depsMap.get(ITERATE_KEY));
    }
}

新增属性:proxy.newKey1=3

触发Proxy.set()响应,进行trigger(target, "add", "newKey1")的派发更新,最终会触发key=ITERATE_KEY的所有effects执行

由上面for...in的读操作可以知道,track的key=ITERATE_KEY,那么当新增属性时,按照逻辑,for....in应该要触发响应式派发更新,因为for....in遍历的是key,新增key自然要通知for....in,因此触发key=ITERATE_KEY的所有effects执行
function createSetter(shallow = false) {
    const hadKey = isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);

    const result = Reflect.set(target, key, value, receiver);
    if (!hadKey) {
        trigger(target, "add" /* ADD */, key, value);
    }
}
function trigger(target, type, key, newValue, oldValue, oldTarget) {
    if (!isArray(target)) {
        deps.push(depsMap.get(ITERATE_KEY));
        if (isMap(target)) { // false
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
        }
    }
}

删除属性:delete proxy.newKey0

触发Proxy.deleteProperty()响应,进行trigger(target, "delete", "newKey0")的派发更新,最终会触发key=newKey0+key=ITERATE_KEY的所有effects执行

由上面for...in的读操作可以知道,track的key=ITERATE_KEY,那么当删除减少属性时,按照逻辑,for....in应该要触发响应式派发更新,因为for....in遍历的是key,删除key自然要通知for....in,因此会触发key=ITERATE_KEY的所有effects执行s

删除key还必须有这个key才会触发响应式更新,也就是如果删除一个不存在的key,是不会触发任何effect重新执行的

function deleteProperty(target, key) {
    const hadKey = hasOwn(target, key);
    const oldValue = target[key];
    const result = Reflect.deleteProperty(target, key);
    if (result && hadKey) {
        trigger(target, "delete" /* DELETE */, key, undefined, oldValue);
    }
    return result;
}
function trigger(target, type, key, newValue, oldValue, oldTarget) {
    if (key !== void 0) {
        deps.push(depsMap.get(key));
    }
    if (!isArray(target)) {
        deps.push(depsMap.get(ITERATE_KEY));
        if (isMap(target)) { // false
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
        }
    }
}

Array

读操作

测试代码概述

具体代码可以查看github调试代码,有debugger断点,直接整个文件夹下载到本地运行array.html即可

下面两个代码块只是总体测试代码的概述,下面一些分析中,会着重摘录出来详细代码进行对应API的分析

const array = ["item1", "item2", "item3", "item4", "item5"];
const proxy = reactive(array);
effect(() => {
    console.error("proxy[0]正常显示", proxy[2]);
    console.error("proxy[20]正常显示", proxy[20]);
});
effect(() => {
    console.info("proxy.length", proxy.length);
});
effect(() => {
    for (let key in proxy) {
        console.warn("for in array", key);
    }
});
effect(() => {
    for (let value of proxy) {
        console.warn("for of array", value);
    }
});

effect(() => {
    const res0 = proxy.includes("item44443");
    const res = proxy.includes(array[1]);
    const res1 = proxy.includes(proxy[3]);
});

const arrayObject = { item: 1 };
const arrayObjectProxy = reactive([arrayObject]);
effect(() => {
    const res2 = arrayObjectProxy.includes(arrayObjectProxy[0]);
    const res3 = arrayObjectProxy.includes(arrayObject);
});

在effect中直接访问数组的item:arr[0]

触发Proxy.get()响应,进行track(target, "get", "0")的依赖收集

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver);
        if (!isReadonly) {
            track(target, "get" /* GET */, key);
        }
        if (isObject(res)) {
            return isReadonly ? readonly(res) : reactive(res);
        }
        return res;
    }
}

在effect中访问数组的长度:arr.length

触发Proxy.get()响应,进行track(target, "get", "length")的依赖收集

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver);
        if (!isReadonly) {
            track(target, "get" /* GET */, key);
        }
        if (isObject(res)) {
            return isReadonly ? readonly(res) : reactive(res);
        }
        return res;
    }
}

在effect中遍历访问数组的key:for(let key in proxy)

触发Proxy.ownKeys()响应,进行track(target, "iterate", "length")的依赖收集

影响for...in array只有改变array的key,而改变array的key有两种方式

  1. 为array新增key,比如var temp = new Array(3); temp[20] = 3
  2. 手动更改array的length,比如var temp = new Array(3); temp.length = 1

而上面两种情况都会触发key=length的响应式派发更新,因此for...in只要track(target, "length")的变化,就能收到(为array新增key)+ (手动更改array的length)两种˙操作所产生的派发更新,从而触发重新执行一次for(let key in proxy),实现响应式更新操作

function ownKeys(target) {
    track(target, "iterate" /* ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
    return Reflect.ownKeys(target);
}

在effect中for...of遍历数组:for(let value of arr)

测试代码
// array.html
watchEffect(() => {
    for (let value of proxy) {
        console.warn("for of array", value);
    }
});
// vue.array.js
function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver);

        if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
            console.error("不会track", "isSymbol", key);
            return res;
        }
        if (!isReadonly) {
            console.info("track", "get", key);
            track(target, "get" /* GET */, key);
        }
    }
}
测试代码执行的流程

截屏2022-11-18 00.32.54.png

源码分析

从常识中可以知道,for...of求的是每一个arrayitem,因此当数据发生变化,比如

  • 数据更新:array[0]=322
  • 新增key: array[20]=323
  • 改变lengtharray.length=10000

都应该触发for...of的重新执行

从上面console打印可以知道,for...of的流程为:
先触发key=length进行当前array.length的获取,如果当前index<=array.length,则返回array[当前index]
然后重复上述流程,直到当前index>array.length,因此for...of会触发key=length以及key=当前index的遍历进行依赖关联

由上面的例子直接访问数组的item,数据更新想要获取响应式监听,只需要会触发track(target, "get", "各种index")的依赖收集
由上面的例子for...in可以知道,新增key或者改变length,想要获取响应式监听,只需要触发track(target, "iterate", "length")的依赖收集

因此for...of的流程所触发的key=length以及key=当前index的遍历的依赖收集就足够覆盖数据更新新增key改变length的响应式监听,不需要额外拦截进行逻辑的新增(includes/indexOf/lastIndexOf需要拦截进行逻辑的新增,见下面的分析)

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
            // 由于for...of会访问到Symbol.iterator等Symbol值,在这里进行依赖追踪阻止
            return res;
        }
        // key="length" / key="0" / key="1" / key="......"
        const res = Reflect.get(target, key, receiver);
        if (!isReadonly) {
            track(target, "get" /* GET */, key);
        }
        if (isObject(res)) {
            return isReadonly ? readonly(res) : reactive(res);
        }
        return res;
    }
}

在effect中不改变数组的多种方法:includes/indexOf/lastIndexOf

测试代码
effect(() => {
    console.warn("=====proxy.includes(原始对象array[2])=====");
    const res = proxy.indexOf(array[1]);
    console.log("proxy.includes(array[2])", res);
    console.warn("=====proxy.includes(原始对象array[2])=====");

    console.warn("=====proxy.includes(代理对象proxy[2])=====");
    const res1 = proxy.indexOf(proxy[3]);
    console.log("proxy.includes(proxy[2])", res1);
    console.warn("=====proxy.includes(代理对象proxy[2])=====");
});
源码分析

Vue3源码中会拦截Array.includesArray.indexOfArray.lastIndexOf,然后进行这些key处理方式的重写

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const targetIsArray = isArray(target);
        if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver);
        }
    }
}
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations();
function createArrayInstrumentations() {
    const instrumentations = {};
    ['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
        // ...
    });
}

我们先注释掉if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key))这个判断,不进行拦截,执行打印结果如下面分析所示

不拦截特定key的测试代码执行流程

Vue3-proxy-array-includes.png

分析不拦截特定key的测试代码执行流程

从上面打印的结果可以发现,如果Vue3没有拦截key=includes/indexOf/lastIndexOf,那么这些key正常触发的流程是:

  • 先获取整个数组的length
  • index=0开始遍历直接访问item[index],直到index> Array.length,返回false
  • 如果中途找到currentTarget===item[index],则中断遍历,直接返回true
源码分析

从上面的分析可以知道,key=includes/indexOf/lastIndexOf是可以正常工作的,那为什么Vue3源码要拦截includes/indexOf/lastIndexOf这些key呢?

那是因为存在一种特殊情况,如下面所示,如果不拦截key=includes/indexOf/lastIndexOf,那么最终在arrayObjectProxy.includes(arrayObject)的判断中,最终结果是为false的,为了能够达到 响应式对象.includes(原始对象item)=trueVue3源码进行了拦截处理,增加了一些处理逻辑

const arrayObject = { item: 1 };
const arrayObjectProxy = reactive([arrayObject]);
effect(() => {
    const res2 = arrayObjectProxy.includes(arrayObjectProxy[0]);
    console.log("arrayObjectProxy.includes(arrayObjectProxy[0])", res2); //true
    const res3 = arrayObjectProxy.includes(arrayObject);
    console.log("arrayObjectProxy.includes(arrayObject)", res3); //false
});

从下面Vue3源码可以知道,先收集了Array.includes原始方法所需要触发的key依赖收集:lengthindex(分析不拦截特定key的测试代码执行流程得出)

  • 通过this.length触发了代理对象的length属性,此时触发Proxy.get()响应,进行track(target, "get", "length")的依赖收集
  • 遍历每一个index,触发Proxy.get()响应,进行track(target, "get", index)的依赖收集

然后进行a.includes(b)ab的原始数据转化

  • 转化响应式对象为原始对象:const arr = toRaw(this),然后进行includes/indexOf/lastIndexOf的方法调用:const res = arr[key](...args)
  • 如果返回值res=false,那说明...args可能为Proxy对象,再转化一次,转化参数为为原始对象:...args.map(toRaw)
  • 最终进行原始对象.includes(原始对象)方法值的返回
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
    instrumentations[key] = function (...args) {
        const arr = toRaw(this);
        for (let i = 0, l = this.length; i < l; i++) {
            track(arr, "get" /* GET */, i + '');
        }
        // we run the method using the original args first (which may be reactive)
        const res = arr[key](...args);
        if (res === -1 || res === false) {
            // if that didn't work, run it again using raw values.
            return arr[key](...args.map(toRaw));
        }
        else {
            return res;
        }
    };
});

在effect中使用Array.concatArray.join等生成新数组的辅助方法

测试代码
effect(() => {
    let newConcatArray = proxy.concat([233, 44]);
    console.error("newArray", newConcatArray);

    let newJoinString = proxy.join(",");
    console.error("newJoinString", newJoinString);
});
测试代码执行的流程
track get concat
track get length
track get 0
track get 1
track get 2
track get 3
track get 4
newArray (7) ['item1', 'item2', 'item3', 'item4', 'item5', 233, 44]
分析测试代码执行的流程

触发Proxy.get()响应,进行track(target, "get", "length")+track(target, "get", 各种index)的依赖收集

当有数据改变length/有任意index数据发生更新时,也会触发concat/join所在的effect重新执行,符合理想状态,不用做任何额外代码的处理

写操作

前置说明

将写操作放在**effect**中,除非allowRecurse=true,否则将会阻止trigger操作:
如下面代码块所示,triggerEffects增加了effect !== activeEffect,因此set操作时不会引发effect重新执行的,适用于任何在effect中都会触发trigger的写操作

function triggerEffects(dep, debuggerEventExtraInfo) {
    // spread into array for stabilization
    for (const effect of isArray(dep) ? dep : [...dep]) {
        if (effect !== activeEffect || effect.allowRecurse) {
            // 如果当前的effect是目前的activeEffect,阻止这个effect执行
        }
    }
}

更改数组的length:arr.length=333333

触发Proxy.set()响应,触发trigger(target, "set", "length")的派发更新

function createSetter(shallow = false) {
    const hadKey = isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);

    const result = Reflect.set(target, key, value, receiver);

    if (hadKey && hasChanged(value, oldValue)) {
        trigger(target, "set" /* SET */, key, value, oldValue);
    }
}

trigger(target, "set", "length")

  • 触发所有收集了lengtheffects
  • 当新设置的length小于原来的length,那么所有被废弃的indexeffects也应该被触发,比如arr.length=2,那么arr[44]=32所在的effects应该被触发重新执行
function trigger(target, type, key, newValue, oldValue, oldTarget) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        // never been tracked
        return;
    }
    let deps = [];
    if (key === 'length' && isArray(target)) {
        depsMap.forEach((dep, key) => {
            if (key === 'length' || key >= newValue) {
                deps.push(dep);
            }
        });
    }
}

更新/新增数组的item:arr[0]=3/arr[1000]=333

赋值改变了数组的length

触发Proxy.set()响应,进行设置index >= target.length的判断,如果符合,则说明是新增key,新增key必定会影响length,因此必须触发收集过key=当前index + key=lengtheffects,即
进行trigger(target, "set", 当前index)+trigger(target, "set", "length")的派发更新

由于当前index是新的key,一般没有收集对应的effect,因此只会触发trigger(target, "set", "length")的派发更新
trigger(target, "set", "length")的派发更新见上面更改数组的length分析
由上面读操作-for...in的分析可以知道,for...in依赖收集的是key=length,当arr[1000]=333发生时,会trigger(target, "set", "length"),会触发for...in的重新执行,符合(key变化时需要通知for...in重新执行)要求,不需要另外书写逻辑

因此下面源码中进行trigger(target, "set", 当前index)+trigger(target, "set", "length")的派发更新符合理想状态,不需要另外书写逻辑

赋值没有改变数组的length

触发Proxy.set()响应,进行设置index >= target.length的判断,如果index < target.length的判断,说明只是item的单纯更新,不会影响length,那么只需要触发收集key=当前index的effects即可,即
进行trigger(target, "set", 当前index)的派发更新

由上面读操作-for...of的流程收集了key=length以及key=所有index的遍历的依赖,当arr[0]=3/arr[1000]=333发生时,会trigger(target, "set", 当前index),可以正常触发for...of重新执行,符合(value变化时需要通知for...of重新执行)要求,不需要另外书写逻辑
function createSetter(shallow = false) {
    const hadKey = isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);

    const result = Reflect.set(target, key, value, receiver);
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
        if (!hadKey) {
            trigger(target, "add" /* ADD */, key, value);
        }
        else if (hasChanged(value, oldValue)) {
            trigger(target, "set" /* SET */, key, value, oldValue);
        }
    }
}
function trigger(target, type, key, newValue, oldValue, oldTarget) {
    if (key === 'length' && isArray(target)) {
        depsMap.forEach((dep, key) => {
            if (key === 'length' || key >= newValue) {
                deps.push(dep);
            }
        });
    } else {
        if (key !== void 0) {
            deps.push(depsMap.get(key));
        }
        switch (type) {
            case "add" /* ADD */:
                if (isIntegerKey(key)) {
                    // new index added to array -> length changes
                    deps.push(depsMap.get('length'));
                }
                break;
            case "set" /* SET */:
                if (isMap(target)) { // false,不会执行
                    deps.push(depsMap.get(ITERATE_KEY));
                }
                break;
        }
    }
}

在effect中进行push/pop/shift/unshift/splice纯粹改变数组,不进行依赖收集的方法

一般写方法都不会在effect中书写,因为很可能会造成写-读-写-读等无限循环的情况。就算书写在effect中,一般也只会触发trigger,而push/pop/shift/unshift/splice方法则比较特殊,如果在effect中使用这些方法,除了正常触发的trigger,还会触发track(target, "length"),但是对于这几个方法,我们不应该进行track(target, "length")
测试代码
const array = ["item1", "item2", "item3", "item4", "item5"];
const proxy = reactive(array);
effect(() => {
    proxy.push(2);
});
测试代码执行的流程
// 触发依赖收集
track get __v_skip
track get length
// 触发正常的新index的trigger
set操作 key:5 value:2
hadKey比较开始 Number(key):5 原来的数组长度:5
oldValue:undefined newValue:2
trigger add 5 2 undefined
// 触发new length的trigger
set操作 key:length value:6
hadKey不比较,转而hasOwn true
oldValue:6 newValue:6

从上面分析的流程可以看出,如果将array.push放入effect中,array.push与其它写方法不同的点在于,它不仅仅会触发set-trigger方法,还会触发get-length方法,这个会产生两个问题

1. 可能会造成无限递归调用的发生,如下面所示
const array = ["item1", "item2", "item3", "item4", "item5"];
const proxy = reactive(array);
effect(() => {
    proxy.push(2); 
});
effect(() => {
    proxy.push(2);
});
  • 第1个effect

    • 收集key=length,触发track(target, "length")操作
    • 相当于proxy[5]=2,触发key="5"以及key="length"trigger操作
  • 第2个effect

    • 收集key=length,触发track(target, "length")操作
    • 相当于proxy[6]=2,触发key="6"以及key="length"trigger操作
    • 由于第1个effect收集了key=length,因此会触发第1个effect重新执行,再次收集key=length和触发key="7"以及key="length"trigger操作
  • 第1个effect:由于第2个effect收集了key=length,因此会触发第2个effect重新执行,再次收集key=length和触发key="8"以及key="length"trigger操作
  • 第2个effect:由于第1个effect收集了key=length,因此会触发第1个effect重新执行,再次收集key=length和触发key="9"以及key="length"trigger操作
2. push/pop/shift/unshift/splice这些方法不应该进行依赖收集

如下面代码所示,当我们点击button#testBtn3时,我们触发了proxy.push(2),从而触发set-trigger方法,还会触发get-length方法
而在effect()中我们调用proxy.push(2),如果不做额外处理,那么在effect()调用proxy.push(2)会触发get-length方法的依赖收集,因此点击button#testBtn3时会触发effect()重新执行

但是从常识上说,上面所描述这种外部调用proxy.push(2)从而触发effect()重新执行是不合理的,因为push()就是一个纯写操作,不应该再触发另外一个写操作push()的执行,不能因为push()改变了长度,从而又再触发一次push()操作

const array = ["item1", "item2", "item3", "item4", "item5"];
const proxy = reactive(array);
effect(() => {
    proxy.push(2); 
});
document.getElementById("testBtn3").addEventListener("click", ()=> {
   proxy.push(2); 
});
源码分析
为了解决上面分析的问题,需要重写push/pop/shift/unshift/splice
function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const targetIsArray = isArray(target);
        if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver);
        }
    }
}
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations();
function createArrayInstrumentations() {
    const instrumentations = {};
    ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(key => {
    });
    return instrumentations;
}
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(key => {
    instrumentations[key] = function (...args) {
        pauseTracking();
        const res = toRaw(this)[key].apply(this, args);
        resetTracking();
        return res;
    };
});
function pauseTracking() {
    trackStack.push(shouldTrack);
    shouldTrack = false;
}
function resetTracking() {
    const last = trackStack.pop();
    shouldTrack = last === undefined ? true : last;
}

从上面的源码可以知道,使用了shouldTrack阻止了push/pop/shift/unshift/spliceeffect中的任何track依赖收集操作
只有当push/pop/shift/unshift/splice执行完毕后,即const res = toRaw(this)[key].apply(this, args);执行完毕后
才能恢复原有的shouldTrack状态

上面说的shouldTrack是全局的shouldTrack,有一些方法,比如trackEffects()也有一个shouldTrack,但是trackEffects()里面的shouldTrack是局部的一个变量,只影响trackEffects()
function track(target, type, key) {
    // 全局的shouldTrack,受pauseTracking()和resetTracking()影响
    if (shouldTrack && activeEffect) {
      //...
    }
}

在effect中Array.sort(a,b)进行数组排序

测试代码
effect(() => {
    proxy.sort((a, b) => a.localeCompare(b))
    console.error("proxy.sort", toRaw(proxy));
});
测试代码执行的流程
track get sort
track get length
track get 0
track get 1
track get 2
track get 3
track get 4

set操作 key:0 value:item1
trigger set 0 item1 item2

set操作 key:1 value:item2
trigger set 1 item2 item1

set操作 key:2 value:item3
set操作 key:3 value:item4
set操作 key:4 value:item5
源码分析

从上面流程可以知道,Array.sort会先进行key=lengthkey=各种indextrack(排序需要先获取所有的item)
然后根据排序结果,对一些item进行更新,比如trigger set key=0的位置为item2trigger set key=1的位置为item1
那为什么一边track一边trigger不会导致无限循环执行呢?因为如下面代码所示,triggerEffects增加了effect !== activeEffect,因此set操作时不会引发effect重新执行的

function triggerEffects(dep, debuggerEventExtraInfo) {
    // spread into array for stabilization
    for (const effect of isArray(dep) ? dep : [...dep]) {
        if (effect !== activeEffect || effect.allowRecurse) {
            // 如果当前的effect是目前的activeEffect,阻止这个effect执行
        }
    }
}

Array.sort会不会跟上面分析的Array.push一样,出现如下代码所示两个effect互相调用无限递归的问题呢?

effect(() => {
    proxy.push(2); 
});
effect(() => {
    proxy.push(2);
});

(排序算法是一致的)情况下是不会的,有两个原因:

  • 因为Array.sort是排序,排序一次就结束了,如果后面再触发排序,但是排列顺序没有改变的话,由于值不会改变,也就是每一个item的值都不改变,那么最终是不会触发任何trigger-set key newValue方法的(如下面代码所示)
  • Array.push是触发了length track依赖收集+新的length trigger派发更新,一边依赖于length,一边又在改变length,而Array.sort只要排序算法一致,就不会改变length
effect(() => {
    proxy.sort((a, b) => a.localeCompare(b))
});
effect(() => {
    proxy.sort((a, b) => a.localeCompare(b))
});

那使用下面的代码,会造成互相调用,从而无限循环吗?

effect(() => {
    proxy.sort((a, b) => a.localeCompare(b))
});
effect(() => {
    proxy.sort((a, b) => b.localeCompare(a))
});

正常情况下是会的,为了避免这种情况发生,Vue3源码又做了一些处理,如下面代码块所示

  • 当上面代码中的第二个effect触发item改变时,会触发第一个effect重新执行
  • 第一个effect重新执行,触发item改变时,会触发第二个effect重新执行
  • 但是下面ReactiveEffecive.run()做了一个parent===this的判断,第2个effect->第1个effect->第2个effect,此时触发了parent===this的判断,阻止了run()的执行,因此中断了无限循环的执行
run() {
    if (!this.active) {
        return this.fn();
    }
    let parent = activeEffect;
    let lastShouldTrack = shouldTrack;
    while (parent) {
        if (parent === this) {
            console.warn("ReactiveEffect run parent==this阻止run")
            return;
        }
        parent = parent.parent;
    }
}
与上面分析push/pop/shift/unshift/splice的区别
  1. Array.sort()不仅仅是一个写操作,如果数组中有元素变化,或者数组长度发生增加时,按照常识来说,我们应该触发effect()中的Array.sort()重新执行,但是对于Array.push(1)来说,就算数组中有元素变化,或者数组长度发生增加时,都跟Array.push()没有关系,它只是一个纯粹的写入操作,不受其它属性的影响,因此Array.sort()需要收集一些依赖,而Array.push()应该完全阻止依赖收集
  2. Array.sort()由于有依赖收集+派发更新的逻辑存在,因此需要使用parent===this避免无限递归调用的情况,而Array.push()由于完全阻止依赖收集,因此也消灭了无限递归调用情况的发生
Array.sort()总结
  • 如果将Array.sort放在effect中,Array.sort会收集key=lengthkey=各种index的依赖,阻止任何trigger操作(适用于任何在effect中都会触发trigger的写操作)
  • 如果在非effect中执行Array.sort,那么排序过程中会触发某一个item的更新,触发trigger(target, "set", 当前index)的派发更新

Vue系列其它文章

  1. Vue2源码-响应式原理浅析
  2. Vue2源码-整体流程浅析
  3. Vue2源码-双端比较diff算法 patchVNode流程浅析
  4. Vue3源码-响应式系统-依赖收集和派发更新流程浅析
  5. Vue3源码-响应式系统-Object、Array数据响应式总结
  6. Vue3源码-响应式系统-Set、Map数据响应式总结
  7. Vue3源码-响应式系统-ref、shallow、readonly相关浅析
  8. Vue3源码-整体流程浅析
  9. Vue3源码-diff算法-patchKeyChildren流程浅析

白边
209 声望37 粉丝

源码爱好者,已经完成vue2和vue3的源码解析+webpack5整体流程源码+vite4开发环境核心流程源码+koa2源码