本文基于Vue 3.2.30
版本源码进行分析
为了增加可读性,会对源码进行删减、调整顺序、改变的操作,文中所有源码均可视作为伪代码
由于ts版本代码携带参数过多,不利于展示,大部分伪代码会采取js的形式展示,而不是原来的ts代码
本文内容
- Object数据响应式测试代码调试与Vue3对应源码总结,分为常见的读和写操作的相关响应式处理
- 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有两种方式
- 为array新增key,比如var temp = new Array(3); temp[20] = 3
- 手动更改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);
}
}
}
测试代码执行的流程
源码分析
从常识中可以知道,for...of
求的是每一个array
的item
,因此当数据发生变化,比如
- 数据更新:
array[0]=322
- 新增
key
:array[20]=323
- 改变
length
:array.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.includes
、Array.indexOf
、Array.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的测试代码执行流程
分析不拦截特定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)=true
,Vue3源码
进行了拦截处理,增加了一些处理逻辑
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
依赖收集:length
和index
(分析不拦截特定key的测试代码执行流程得出)
- 通过
this.length
触发了代理对象的length
属性,此时触发Proxy.get()
响应,进行track(target, "get", "length")
的依赖收集 - 遍历每一个
index
,触发Proxy.get()
响应,进行track(target, "get", index)
的依赖收集
然后进行a.includes(b)
中a
和b
的原始数据转化
- 转化响应式对象为原始对象:
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.concat
和Array.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")
:
- 触发所有收集了
length
的effects
- 当新设置的
length
小于原来的length
,那么所有被废弃的index
的effects
也应该被触发,比如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=length
的effects
,即
进行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/splice
在effect
中的任何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=length
和key=各种index
的track
(排序需要先获取所有的item)
然后根据排序结果,对一些item
进行更新,比如trigger set key=0的位置为item2
,trigger 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
的区别
Array.sort()
不仅仅是一个写操作,如果数组中有元素变化,或者数组长度发生增加时,按照常识来说,我们应该触发effect()
中的Array.sort()
重新执行,但是对于Array.push(1)
来说,就算数组中有元素变化,或者数组长度发生增加时,都跟Array.push()
没有关系,它只是一个纯粹的写入操作,不受其它属性的影响,因此Array.sort()
需要收集一些依赖,而Array.push()
应该完全阻止依赖收集Array.sort()
由于有依赖收集+派发更新的逻辑存在,因此需要使用parent===this
避免无限递归调用的情况,而Array.push()
由于完全阻止依赖收集,因此也消灭了无限递归调用情况的发生
Array.sort()
总结
- 如果将
Array.sort
放在effect
中,Array.sort
会收集key=length
和key=各种index
的依赖,阻止任何trigger
操作(适用于任何在effect
中都会触发trigger
的写操作) - 如果在非
effect
中执行Array.sort
,那么排序过程中会触发某一个item
的更新,触发trigger(target, "set", 当前index)
的派发更新