目标
将函数与函数运行过程中用到的响应式数据建立对应关系。
var data1=true, data2,data3;
function fn(){
if(data1){
data2;
} else {
data3;
}
}
data2=1;
// fn: datal, data2
任务
-
监听响应式数据的读取和修改。
-
defineProperty(vue2)
-
Proxy 和 Reflect (vue3)
-
-
关联响应式数据和调用数据的函数。
-
依赖收集:影响函数运行结果的就应该被收集
-
派发更新
-
监听响应式数据的读取和修改
监听
// reactive.js
-
target 是不是对象(不是对象直接返回)。
-
target 是同一个对象(WeakMap):
const obj={ a: 1, b:2, }; const state1=reactive(obj); const state2= reactive(obj); console.log(state1 === state2);
-
提取 handlers
// handlers.js
读取:依赖收集
-
收集 getter 内部的属性(使用Reflect)
const obj={ a: 1, b:2, get c(){ return this.a + this.b; } } const state1=reactive(obj); function fn(){ state1.c; } fn();
-
收集嵌套对象
const obj={ a: 1, b:2, c: { d: 3 } } const state1 = reactive(obj); function fn(){ state1.c.d; } fn();
-
收集 in 方法
const obj={ a: 1, b:2, } const state1 = reactive(obj); function fn(){ 'e' in state1; } state1.e = 123; fn();
-
添加操作类型信息
const obj={ a: 1, b:2, } const state1 = reactive(obj); function fn(){ 'a' in state1; } fn(); state1.a = 123;
-
handlers 的每个操作提取为单独函数
-
收集
for...in
const obj={ a: 1, b:2, } const state1 = reactive(obj); function fn(){ for(const key in state1) { } } fn(); state1.abc = 123;
修改(写入): 派发更新
-
set 中分辨 add和set 类型。
-
delete 操作派发更新。
-
考虑 set 和 delete 的边界情况
const obj={ a: 1, b:2, } const state1 = reactive(obj); // 删除不存在的属性 delete state1.abc // 值没有变化 state1.a = 1;
数组
依赖收集
-
下标,length , for 循环
const arr = [1,2,3]; const state=reactive(arr); function fn(){ // state[0]; // state.length for(let i=0;i< state.length; i++){ state[i]; } } fn();
-
for...of 循环
const arr = [1,2,3]; const state=reactive(arr); function fn(){ for(const item of state){ } } fn();
-
includes ,lastIndexOf, indexOf方法
const arr = [1,2,3,,,]; const state=reactive(arr); function fn(){ state.includes(1); // state.lastIndexOf(1); // state.lastIndexOf(undefined); // state.indexOf(1); } fn();
-
数组元素为对象时includes,lastIndexOf, indexOf需要处理:
const obj = {}; const arr = [1,obj,3]; const state=reactive(arr); function fn(){ console.log(state.indexOf(obj)); } fn();
解决方案,二选一:
-
传入的原始对象转换为代理对象
-
当无法在代理对象中找到时,去原始数组中重新找一次(vue)
派发更新
-
设置下标没有问题,但是超出长度没有派发 length 的改变。 ECMAScript® 2024 Language Specification
const obj={}; const arr=[1,obj,3]; const state=reactive(arr); state[0] = 3 //state[5] = 5 // Object.defineProperty(state,'abc',{value: 6}); console.log(arr)
-
修改 length没有问题,但 length 设置为小于数组长度时没有触发 delete 元素。
const arr=[1,2,3,4,5,6]; const state=reactive(arr); state.length = 3; console.log(arr);
-
push 会多触发一次 length 收集。pop, shift,unshift,splice 同理。
const arr=[1,2,3,4,5,6]; const state=reactive(arr); state.push(7); console.log(arr);
解决方案,二选一:
-
把那些会对数组产生改动的方法全部重写
-
暂停依赖收集(vue)
关联响应式数据和调用数据的函数
// effect.js
-
数据和函数的对应关系
-
收集函数: 用
effect
标记函数const obj={ a: 1, b:2, }; const state= reactive(obj); function fn1(){ function fn2(){ state.a; } fn2(); } // 运行函数fn1,运行期间用到的所有响应式数据,都会收集为对应关系 effect(fn1);
-
每次运行函数需要重新进行依赖收集,并且是收集运行函数的环境
const obj={ a: 1, b:2, }; const state=reactive(obj); function fn1(){ if(state.a === 1){ state.b; } else { state.c; } } effect(fn1); state.a = 2;
-
建立关系
-
运行前,所有 Set 中删除 fn
const obj={ a: 1, b:2, }; const state=reactive(obj); function fn(){ if(state.a === 1){ state.b; } else { state.c; } } effect(fn); state.a = 2; state.b = 3; // 不应该触发 fn
课堂练习:实现 ref
import { ref } from './ref.js';
const state = ref(1);
effect(() => {
console.log('effect', state.value);
});
state.value++;
完整代码
完整代码https://github.com/yjx3097890/reactive