基础分析
监听数据的读取和修改
js中有两个方法可以监听对象:
defineProperty
监听范围比较窄,兼容性较好,VUE2.0Proxy
监听范围广,所有对象的内部方法都可以监听,但仅支持支持ES6的浏览器,VUE3.0
因此,数据都需为对象
// reactive.js
import { track, trigger } from './effect.js';
// 将一个数据变为响应式数据
export function reactive(target) {
return new Proxy(target, {
get(target, key) {
// 依赖收集:找到当前key对应的依赖
track(target, key);
// 返回值
return Reflect.get(target, key);
},
set(target, key, value) {
// 派发更新
trigger(target, key);
// 设置值
return Reflect.set(target, key, value);
}
});
}
reactive函数将数据变为响应式数据
// effect.js
// Description: 依赖收集
export function track(target, key) {
// 依赖收集
console.log(`%c依赖收集:${key}`, 'color: red');
}
// Description: 派发更新
export function trigger(target, key) {
// 派发更新
console.log(`%c派发更新:${key}`, 'color: blue');
}
测试
// index.js
import { reactive } from "./reactive.js";
const state = reactive({
harvester: 2,
builder: 5,
upgrader: 5
});
function fn() {
state.harvester;
state.builder;
}
fn();
state.upgrader = 3;
state.builder--; // (*)
结果如下:
fn 中引用了 harvester 和 builder 触发了依赖收集。
更改upgrader的值触发了派发更新。
(*) 行的 -- 先读取再赋值。因此都触发了。
监听对象数据读取和修改
优化
// reactive.js
import { isObject } from './utils.js';
import { handlers } from './handlers.js';
const target_map = new WeakMap();
// 将一个数据变为响应式数据
export function reactive(target) {
// 不是对象,直接返回
if (!isObject(target)) {
return target;
}
// 同一个对象, 直接返回
if (target_map.has(target)) {
return target_map.get(target);
}
// 创建代理对象
const proxy = new Proxy(target, handlers);
// 缓存
target_map.set(target, proxy);
return proxy;
}
// handlers.js
import { track, trigger } from './effect.js';
export const handlers = {
get(target, key) {
// 依赖收集:找到当前key对应的依赖
track(target, key);
// 返回值
return Reflect.get(target, key);
},
set(target, key, value) {
// 派发更新
trigger(target, key);
// 设置值
return Reflect.set(target, key, value);
}
}
读取
在上面的代码中若对象使用了 getter 或 setter 会发生以下情况:
// index.js
import { reactive } from "./reactive.js";
const state = reactive({
harvester: 2,
builder: 5,
upgrader: 5,
get total() {
return this.harvester + this.builder + this.upgrader;
}
});
function fn() {
state.total;
}
fn();
// 输出:依赖收集:total
我们希望的是应该也监听到 total 中使用的属性
在内部访问方法中,还有一个 receiver 可以指定 this 指向:
// handler.js
import { track, trigger } from './effect.js';
export const handlers = {
get(target, key, receiver) {
// 依赖收集:找到当前key对应的依赖
track(target, key);
// 返回值
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 派发更新
trigger(target, key);
// 设置值
return Reflect.set(target, key, value, receiver);
}
}
于是:
可以监听到所有属性了
但是!
考虑以下情况:
import { reactive } from "./reactive.js";
const state = reactive({
harvester: 2,
builder: 5,
upgrader: 5,
spawn_point: {
x: 0,
y: 0
}
});
function fn() {
state.spawn_point.x;
state.spawn_point.y;
}
fn();
// 输出:
// 依赖收集:spawn_point
// 依赖收集:spawn_point
为什么没有 x 和 y 呢?
在 handler 中,若 Reflect.get 获取到的是一个对象,被直接返回,并没有被代理,也就无法监听。
// handler.js
import { track, trigger } from './effect.js';
import { isObject } from './utils.js';
import { reactive } from './reactive.js';
export const handlers = {
get(target, key, receiver) {
// 依赖收集:找到当前key对应的依赖
track(target, key);
// 返回值
const result = Reflect.get(target, key, receiver);
// 如果是对象,递归代理
return isObject(result) ? reactive(result) : result;
},
set(target, key, value, receiver) {
// 派发更新
trigger(target, key);
// 设置值
return Reflect.set(target, key, value, receiver);
}
}
判断返回值是否是对象,若是则代理后返回。
但是(梅开二度)
考虑以下情况:
import { reactive } from "./reactive.js";
const state = reactive({
harvester: 2,
builder: 5,
upgrader: 5
});
function fn() {
"carrier" in state;
}
fn();
此时不会触发收集,因此加入 has 方法
// handler.js
import { track, trigger } from './effect.js';
import { isObject } from './utils.js';
import { reactive } from './reactive.js';
export const handlers = {
get(target, key, receiver) {
// 依赖收集:找到当前key对应的依赖
track(target, key);
// 返回值
const result = Reflect.get(target, key, receiver);
// 如果是对象,递归代理
return isObject(result) ? reactive(result) : result;
},
has(target, key) {
// 依赖收集:找到当前key对应的依赖
track(target, key);
// 返回值
return Reflect.has(target, key);
},
set(target, key, value, receiver) {
// 派发更新
trigger(target, key);
// 设置值
return Reflect.set(target, key, value, receiver);
}
}
还有吗,有,遍历,加入即可。
加入 type
在前面的代码中,若使用 in 检查了键
function fn() {
"carrier" in state;
}
当对应键已经存在时,后续的改变,我们不希望此时认为需要触发更新,于是加入操作 type,方便后续操作。
// effect.js
// Description: 依赖收集
export function track(target, type, key) {
// 依赖收集
console.log(`%c依赖收集:[${type}] ${key}`, 'color: red');
}
// Description: 派发更新
export function trigger(target, type, key) {
// 派发更新
console.log(`%c派发更新:[${type}] ${key}`, 'color: blue');
}
// handler.js
import { track, trigger } from './effect.js';
import { isObject } from './utils.js';
import { reactive } from './reactive.js';
import { TrackOpTypes, TriggerOpTypes } from './operations.js';
export const handlers = {
get(target, key, receiver) {
// 依赖收集:找到当前key对应的依赖
track(target, TrackOpTypes.GET, key);
// 返回值
const result = Reflect.get(target, key, receiver);
// 如果是对象,递归代理
return isObject(result) ? reactive(result) : result;
},
has(target, key) {
// 依赖收集:找到当前key对应的依赖
track(target, TrackOpTypes.HAS, key);
// 返回值
return Reflect.has(target, key);
},
set(target, key, value, receiver) {
// 派发更新
// TODO: 判断操作类型
trigger(target, TriggerOpTypes.SET, key);
// 设置值
return Reflect.set(target, key, value, receiver);
}
}
可以看到获取到了更详细的信息,方便后续操作。
优化...
// hanlder.js
import { track, trigger } from './effect.js';
import { isObject } from './utils.js';
import { reactive } from './reactive.js';
import { TrackOpTypes, TriggerOpTypes } from './operations.js';
function get(target, key, receiver) {
// 依赖收集:找到当前key对应的依赖
track(target, TrackOpTypes.GET, key);
// 返回值
const result = Reflect.get(target, key, receiver);
// 如果是对象,递归代理
return isObject(result) ? reactive(result) : result;
}
function has(target, key) {
// 依赖收集:找到当前key对应的依赖
track(target, TrackOpTypes.HAS, key);
// 返回值
return Reflect.has(target, key);
}
function set(target, key, value, receiver) {
// 派发更新
// TODO: 判断操作类型
trigger(target, TriggerOpTypes.SET, key);
// 设置值
return Reflect.set(target, key, value, receiver);
}
function ownKeys(target) {
// 派发更新
track(target, TrackOpTypes.ITERATE);
// 返回值
return Reflect.ownKeys(target);
}
export const handlers = {
get,
has,
set,
ownKeys,
}
修改
区分 add 与 set
function set(target, key, value, receiver) {
const type = target.hasOwnProperty(key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD;
// 派发更新
trigger(target, type, key);
// 设置值
return Reflect.set(target, key, value, receiver);
}
加入 delete,加入一些判断
// handler.js
import { track, trigger } from './effect.js';
import { isObject, isObject } from './utils.js';
import { reactive } from './reactive.js';
import { TrackOpTypes, TriggerOpTypes } from './operations.js';
function get(target, key, receiver) {
// 依赖收集:找到当前key对应的依赖
track(target, TrackOpTypes.GET, key);
// 返回值
const result = Reflect.get(target, key, receiver);
// 如果是对象,递归代理
return isObject(result) ? reactive(result) : result;
}
function has(target, key) {
// 依赖收集:找到当前key对应的依赖
track(target, TrackOpTypes.HAS, key);
// 返回值
return Reflect.has(target, key);
}
function set(target, key, value, receiver) {
const type = target.hasOwnProperty(key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD;
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (result && (hasChanged(value, oldValue) || type === TriggerOpTypes.ADD)) {
// 派发更新
trigger(target, type, key);
}
// 设置值
return result;
}
function deleteProperty(target, key) {
const hadKey = target.hasOwnProperty(key);
// 删除值
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
// 派发更新
trigger(target, TriggerOpTypes.DELETE, key);
}
return result;
}
function ownKeys(target) {
// 依赖收集
track(target, TrackOpTypes.ITERATE);
// 返回值
return Reflect.ownKeys(target);
}
export const handlers = {
get,
has,
set,
ownKeys,
deleteProperty
}
测试代码:
// index.js
import { reactive } from "./reactive.js";
const state = reactive({
harvester: 2,
builder: 5,
upgrader: 5
});
function fn() {
Object.keys(state);
}
fn();
state.builder = +0; // (*)
state.carrier = 3; // (**)
state.builder = -0; // (***)
state.builder = -0; // (****)
delete state.carrier;
delete state.carrier;
结果:
让我们来分析一下:
- fn 函数中使用Object.keys返回对象的所有键,触发了ownKeys,进行依赖收集。
- (*) 行更改 builder 值,5 => +0 值变化,触发set。
- (**) 行添加 carrier 属性,触发add。
- (***) 行更改 builder 值,+0 => -0 值变化,触发set。
- (****) 行值未变化,未触发set。
- 最后两行,第一行删除 carrier 属性,触发delete,第二行已经没有 carrier 属性,不再触发delete。
严格等于还是 Object.is?
+0 === -0 结果为 true,NaN === NaN 结果为 false。
Object.is(-0, +0) 结果为 false,Object.is(NaN, NaN) 结果为 true。
我们需要的是判断新值是否会改变函数的运行结果,因此我们使用Object.is。
监听数组数据读取和修改
读取
尝逝
现在让我们考虑一下响应式数据是数组的情况。
import { reactive } from "./reactive.js";
const state = reactive([1, 2, 3, 4, 5]);
function fn() {
console.log('0---------');
state[0];
state.length;
console.log('1---------');
for (let i = 0; i < state.length; i++) {
state[i];
}
console.log('2---------');
state.includes(3);
console.log('3---------');
state.indexOf(2);
console.log('4---------');
for (let key of state) {
state[key];
}
}
fn();
让我们依次来看一下结果:
第零部分,正常获取,正确收集
第一部分,使用for循环遍历,每次获取length以及下标,有重复,但暂时不用处理。
第二部分,先获取到includes属性,然后运行函数,从0开始取出数据与3比较,找到下标为2时相等,返回结果。收集正确。
第三部分,访问indexOf,然后length,发现使用了has,indexOf会先检查是否有这个键,然后再取出访问。
第四部分,啊嘞,没输出?
查看报错信息,发现将Symbol键转换为string失败,更改effect.js:
// effect.js
import { TrackOpTypes, TriggerOpTypes } from "./operations.js";
// Description: 依赖收集
export function track(target, type, key) {
// 依赖收集
if (type === TrackOpTypes.ITERATE) {
console.log(`%c依赖收集:[${type}]`, 'color: red');
}
else {
console.log(`%c依赖收集:[${type}] `, 'color: red', key);
}
}
// Description: 派发更新
export function trigger(target, type, key) {
// 派发更新
console.log(`%c派发更新:[${type}] `, 'color: blue', key);
}
结果:
为什么要判断数组有没有下标
存在稀疏数组:[0, , , 2]
,根据我们上面发现的逻辑,若我们试图在数组中找undefined
,会返回下标1吗?
不会,因为会先判断键是否存在,不存在将直接跳过,不会有后续比较。
看起来不错,是吗?
数组中的对象
看看这个:
import { reactive } from "./reactive.js";
const obj = {};
const state = reactive([1, obj, 3]);
function fn() {
console.log(state.indexOf(obj));
}
fn();
能找到吗?
没找到,为什么?
还记得我们上一章中的,对象中的对象如何处理的吗?
我们检测返回的是否是对象,若是将其代理,那么这里数组里的对象在访问时返回的是被代理后的对象,与原对象不同,那就找不到了。
两个解决方案:
- 传入的原始对象转换为代理对象。
- 代理对象中找不到,在原始数组中去重新找。(VUE采用的方案)
方法一看起来更好,因此我们选择第二种。(欸嘿)
我们需要改动get方法,拦截获取的indexOf方法,返回我们更改后的方法。
// handlers.js
...
const arrayInstrumentations = {};
const RAW = Symbol('raw');
["includes", "indexOf", "lastIndexOf"].forEach(key => {
arrayInstrumentations[key] = function(...args) {
// 正常找从代理对象中找
const res = Array.prototype[key].apply(this, args);
// 若找不到,再在原始数组中找
if (res === -1 || res === false) {
return Array.prototype[key].apply(this[RAW], args);
}
return res;
};
});
function get(target, key, receiver) {
// 返回原始对象 (***)
if (key === RAW) {
return target;
}
// 依赖收集:找到当前key对应的依赖
track(target, TrackOpTypes.GET, key);
// 返回值
if (arrayInstrumentations.hasOwnProperty(key) && Array.isArray(target)) {
return arrayInstrumentations[key];
}
const result = Reflect.get(target, key, receiver);
// 如果是对象,递归代理
return isObject(result) ? reactive(result) : result;
}
...
当get这["includes", "indexOf", "lastIndexOf"]三个方法时,我们返回我们自定义的方法,先正常找,若找不到则获取原始对象再找。
(***)行,为了避免与传入的对象键冲突,使用Symbol避免冲突。
方法二带来的问题
在我们开头的示例中,我们查找obj,按照正常查找的逻辑,我们在下标1就应该找到了,我们不应该收集到下标2,但是如下图,我们却收集到了下标2。
修改
Length触发问题
看看这个
const obj = {};
const state = reactive([1, obj, 3, , 5]);
// 等同于 state[state.length] = 6;
// 即修改一个超过数组长度索引
state.push(6);
length呢?push后length也会变化,但这里并未出现length的变化
length的属性设置是通过defineProperty方法,我们使用下面的方法试试。
Object.defineProperty(state, 'length', 7);
并未输出length的修改,因此让我们添加监听。
首先分析一下什么情况下需要手动触发length属性变化
- 设置的对象是一个数组。
- 设置前后length发生了变化。
- 设置的不是length属性。
上面三个条件同时满足就需要手动触发。
function set(target, key, value, receiver) {
const type = target.hasOwnProperty(key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD;
const oldValue = target[key];
const oldLength = Array.isArray(target) ? target.length : undefined;
// 设置值
const result = Reflect.set(target, key, value, receiver);
const newLength = Array.isArray(target) ? target.length : undefined;
if (result && (hasChanged(value, oldValue) || type === TriggerOpTypes.ADD)) {
// 派发更新
trigger(target, type, key);
// 数组长度变化
if (Array.isArray(target) && key !== 'length' && newLength !== oldLength) {
trigger(target, TriggerOpTypes.SET, 'length');
}
}
return result;
}
结果:
还没完,当我们将length直接改小时:
state.length = state.length - 2;
length改小会删除属性,但并未监听到,因此继续判断:
function set(target, key, value, receiver) {
const type = target.hasOwnProperty(key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD;
const oldValue = target[key];
const oldLength = Array.isArray(target) ? target.length : undefined;
// 设置值
const result = Reflect.set(target, key, value, receiver);
const newLength = Array.isArray(target) ? target.length : undefined;
if (result && (hasChanged(value, oldValue) || type === TriggerOpTypes.ADD)) {
// 派发更新
trigger(target, type, key);
// 数组长度变化
if (Array.isArray(target) && newLength !== oldLength) {
if (key !== 'length') {
trigger(target, TriggerOpTypes.SET, 'length');
}
else {
// 找到被删除的元素,触发更新
for (let i = newLength; i < oldLength; i++) {
trigger(target, TriggerOpTypes.DELETE, i + '');
}
}
}
}
return result;
}
需要收集Length吗
还记得刚刚的例子吗
const obj = {};
const state = reactive([1, obj, 3, , 5]);
// 等同于 state[state.length] = 6;
// 即修改一个超过数组长度索引
state.push(6);
让我们看看收集部分,收集到push,没问题,如果push被改了结果会变,收集到length,似乎没问题。
但是考虑一下使用push的目的,是想添加一个元素进去,并不想收集length,
我们如果需要收集length,为什么不直接获取length呢?
因此,此处我们不想要收集length。
同样,有两个方法:
- 将会对数组产生改动的方法全部重写。
- 调用方法期间,暂停依赖收集。(VUE)
第一种emmm,纯重写,问题很大,选第二种
// handlers.js
...
["push", "pop", "shift", "unshift", "splice"].forEach(key => {
arrayInstrumentations[key] = function(...args) {
pauseTracking(); // 暂停依赖收集
const res = Array.prototype[key].apply(this, args);
resumeTracking(); // 恢复依赖收集
return res;
};
});
...
// effect.js
...
let shouldTrack = false;
// Description: 暂停依赖收集
export function pauseTracking() {
shouldTrack = false;
}
// Description: 恢复依赖收集
export function resumeTracking() {
shouldTrack = true;
}
// Description: 依赖收集
export function track(target, type, key) {
if (!shouldTrack) {
// 不需要依赖收集
return;
}
...
}
...
当然还有许多细节
除了数组还有Map,Set等...
关联数据和函数
亿些处理
// effect.js
import { TrackOpTypes, TriggerOpTypes } from "./operations.js";
const targetMap = new WeakMap();
const ITERATE_KEY = Symbol('iterate');
let activeEffect;
export function effect(fn) {
const effectfn = () => {
try {
activeEffect = effectfn;
return fn();
}
finally {
activeEffect = null;
}
}
effectfn();
}
let shouldTrack = false;
// Description: 暂停依赖收集
export function pauseTracking() {
shouldTrack = false;
}
// Description: 恢复依赖收集
export function resumeTracking() {
shouldTrack = true;
}
// Description: 依赖收集
export function track(target, type, key) {
if (!shouldTrack || !activeEffect) {
// 不需要依赖收集
return;
}
// 依赖收集
if (type === TrackOpTypes.ITERATE) {
console.log(`%c依赖收集:[${type}]`, 'color: red');
}
else {
console.log(`%c依赖收集:[${type}] `, 'color: red', key);
}
}
// Description: 派发更新
export function trigger(target, type, key) {
// 派发更新
console.log(`%c派发更新:[${type}] `, 'color: blue', key);
}
targetMap 结构:
targetMap
- 对象1:propMap
- 对象2:propMap
- 属性1:typeMap
- 属性2:typeMap
- get:dep
- has:dep
- fn1
- fn2
依赖收集
export function track(target, type, key) {
if (!shouldTrack || !activeEffect) {
// 不需要依赖收集
return;
}
let propMap = targetMap.get(target);
if (!propMap) {
propMap = new Map();
targetMap.set(target, propMap);
}
if (type === TrackOpTypes.ITERATE) {
key = ITERATE_KEY;
}
let typeMap = propMap.get(key);
if (!typeMap) {
typeMap = new Map();
propMap.set(key, typeMap);
}
let depSet = typeMap.get(type);
if (!depSet) {
depSet = new Set();
typeMap.set(type, depSet);
}
if (!depSet.has(activeEffect)) {
depSet.add(activeEffect);
}
}
派发更新
export function trigger(target, type, key) {
// 派发更新
const effectfns = getEffectFns(target, type, key);
if (effectfns) {
for (const effectfn of effectfns) {
effectfn();
}
}
// console.log(`%c派发更新:[${type}] `, 'color: blue', key);
}
function getEffectFns(target, type, key) {
const propMap = targetMap.get(target);
if (!propMap) {
return;
}
const keys = [key];
if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
keys.push(ITERATE_KEY);
}
const effectFns = new Set();
const triggerTypeMap = {
[TriggerOpTypes.SET]: [TrackOpTypes.GET],
[TriggerOpTypes.ADD]: [TrackOpTypes.ITERATE, TrackOpTypes.HAS, TrackOpTypes.GET],
[TriggerOpTypes.DELETE]: [TrackOpTypes.ITERATE, TrackOpTypes.HAS, TrackOpTypes.GET],
};
for (let key of keys) {
const typeMap = propMap.get(key);
if (!typeMap) {
continue;
}
const trackTypes = triggerTypeMap[type];
for (const trackType of trackTypes) {
const depSet = typeMap.get(trackType);
if (depSet) {
depSet.forEach(effect => {
effectFns.add(effect);
});
}
}
}
return effectFns;
}
亿些bug
一:
let state = reactive({
a: 1,
b: 2,
c: 3
});
function fn1() {
console.log('fn1 run');
if (state.a === 3) {
state.b;
}
else {
state.c;
}
}
effect(fn1);
console.log('---------');
state.c = 3;
console.log('---------');
state.a = 3;
console.log('---------');
state.b = 4;
console.log('---------');
state.c = 5;
console.log('---------');
当我们将a的值改了后,运行fn1,应该重新收集a,b,删除c,但是最后修改c发现函数又运行了。我们并未处理删除c。
二:
let state2 = reactive([1, 2, 3]);
function fn3() {
console.log('fn3 run');
effect(() => {
console.log('inner run');
state2[1];
});
state2[2];
}
effect(fn3);
console.log('---------');
state2[1] = 3;
console.log('---------');
state2[2] = 4;
console.log('---------');
修改下标2时应该触发fn3运行,但发现fn3并未运行。
我们只使用了一个activeEffect来储存,在内部函数运行后,activeEffect被设置为null,导致下标2未被收集。
执行栈问题。添加栈处理。
三:
let state2 = reactive([1, 2, 3]);
function fn4() {
console.log('fn4 run');
state2[1]++;
}
effect(fn4);
爆栈,判断一下即可。
四:
let state2 = reactive([1, 2, 3]);
function fn2() {
console.log('fn2 run');
if (state2.length > 3) {
state2[2];
}
else {
state2[1];
}
}
effect(fn2);
console.log('---------');
state2[1] = 3;
console.log('---------');
state2.push(4);
console.log('---------');
state2[2] = 5;
console.log('---------');
按理说,修改下标2时,由于前面push,length大于3后,收集的依赖应该是下标2和length,但是最后修改下标2却没有触发更新。原因是push过程中暂停了依赖收集,导致并没有收集到下标2的依赖。
......
补充实现
ref
// ref.js
import { TrackOpTypes, TriggerOpTypes } from "./operations.js";
import { track, trigger } from "./effect.js";
export function ref(value) {
return {
get value() {
track(this, TrackOpTypes.GET, "value");
return value;
},
set value(newValue) {
value = newValue;
trigger(this, TriggerOpTypes.SET, "value", newValue);
}
};
}
测试:
import { reactive } from "./reactive.js";
import { effect } from "./effect.js";
import { ref } from "./ref.js";
const state = ref(1);
effect(() => {
console.log("effect", state.value);
})
state.value++;
computed
// computed.js
import { effect } from "./effect.js";
import { track, trigger } from "./effect.js";
import { TrackOpTypes, TriggerOpTypes } from "./operations.js";
function normalizeParameters(getterOrOptions) {
let getter, setter;
if (typeof getterOrOptions === 'function') {
getter = getterOrOptions;
setter = () => {
console.warn('computed() received a function but no setter.')
}
}
else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return { getter, setter };
}
export function computed(getterOrOptions) {
const { getter, setter } = normalizeParameters(getterOrOptions);
let value, dirty = true;
const effectfn = effect(getter, {
lazy: true,
scheduler: () => {
dirty = true;
trigger(computed, TriggerOpTypes.SET, 'value');
}
});
return {
get value() {
track(computed, TrackOpTypes.GET, 'value');
if (dirty) {
value = effectfn();
dirty = false;
}
return value;
},
set value(newValue) {
setter(newValue);
}
}
}
测试:
import { reactive } from "./reactive.js";
import { effect } from "./effect.js";
import { ref } from "./ref.js";
import { computed } from "./computed.js";
const state = reactive({
a: 1,
b: 2,
c: 3
});
const sum = computed(() => {
console.log('computed run');
return state.a + state.b + state.c;
});
sum.value;
sum.value;
sum.value;
sum.value;