fix: computed obj lost reactive

This commit is contained in:
haiqin 2024-01-26 10:05:50 +08:00
parent ffd2bf2fc5
commit b9e0fc25a1
10 changed files with 106 additions and 37 deletions

View File

@ -22,7 +22,7 @@ module.exports = {
],
root: true,
plugins: ['jest', 'no-for-of-loops', 'no-function-declare-after-return', 'react', '@typescript-eslint'],
plugins: ['jest', 'no-function-declare-after-return', 'react', '@typescript-eslint'],
parser: '@typescript-eslint/parser',
parserOptions: {
@ -56,7 +56,6 @@ module.exports = {
'comma-dangle': ['error', 'only-multiline'],
'no-constant-condition': 'off',
'no-for-of-loops/no-for-of-loops': 'error',
'no-function-declare-after-return/no-function-declare-after-return': 'error',
},
globals: {

View File

@ -14,6 +14,8 @@
*/
import { printNode } from './utils/printNode';
import { Signal } from './Types';
import { isFunction } from './Utils';
let runningRNode: RNode<any> | undefined = undefined; // 当前正执行的RNode
let calledGets: RNode<any>[] | null = null;
@ -47,17 +49,16 @@ function defaultEquality(a: any, b: any) {
return a === b;
}
export class RNode<T = any> {
export class RNode<T = any> implements Signal<T> {
_value: T;
fn?: () => T;
private observers: RNode[] | null = null; // 被谁用
private sources: RNode[] | null = null; // 使用谁
private state: State;
protected state: State;
isEffect = false;
cleanups: ((oldValue: T) => void)[] = [];
equals = defaultEquality;
@ -120,15 +121,9 @@ export class RNode<T = any> {
}
set(fnOrValue: T | ((prev: T) => T)): void {
if (this.fn) {
this.removeParentObservers(0);
this.sources = null;
this.fn = undefined;
}
const prevValue = this.getValue();
const value = typeof fnOrValue === 'function' ? fnOrValue(prevValue) : fnOrValue;
const value = isFunction(fnOrValue) ? fnOrValue(prevValue) : fnOrValue;
this.compare(prevValue, value);
@ -291,7 +286,6 @@ export class RNode<T = any> {
setValue(value: any) {
this._value = value;
}
}
export function onCleanup<T = any>(fn: (oldValue: T) => void): void {
@ -324,4 +318,3 @@ export function untrack(fn) {
runningRNode = preRContext;
}
}

View File

@ -43,12 +43,12 @@ export function setRNodeVal(rNode: RProxyNode<any>, value: unknown): void {
if (isRoot) {
prevValue = rNode.root!.$;
newValue = isFunction<(...prev: any) => any>(value) ? value(prevValue) : value;
newValue = isFunction(value) ? value(prevValue) : value;
rNode.root!.$ = newValue;
} else {
const parentVal = getRNodeVal(parent!);
prevValue = parentVal[key];
newValue = isFunction<(...prev: any) => any>(value) ? value(prevValue) : value;
newValue = isFunction(value) ? value(prevValue) : value;
parentVal[key] = newValue;
}
}

View File

@ -15,25 +15,28 @@
import { isPrimitive } from './Utils';
import { RNode } from './RNode';
import { ProxyRNode } from './Types';
import { RProxyNode } from './RProxyNode';
import { getRNodeVal, getRootRNode } from './RNodeAccessor';
import { Fn, NonFunctionType, Signal } from './Types';
import { DeepReactive, RProxyNode } from './RProxyNode';
import { getRNodeVal } from './RNodeAccessor';
export type Reactive<T = any> = RNode<T> | Atom<T>;
export function createReactive<T extends any>(raw?: T): ReactiveProxy<T> {
export function createReactive<T extends string>(raw?: T): Signal<string>;
export function createReactive<T extends number>(raw?: T): Signal<number>;
export function createReactive<T extends symbol>(raw?: T): Signal<symbol>;
export function createReactive<T extends number | string | symbol>(raw?: T): Signal<T>;
export function createReactive<T extends Record<any, any> | Array<any> | symbol>(raw?: T): DeepReactive<T>;
export function createReactive<T extends NonFunctionType>(raw?: T): DeepReactive<T> | Signal<T> {
if (isPrimitive(raw) || raw === null || raw === undefined) {
return new RNode(raw, { isSignal: true });
} else {
const node = new RProxyNode(null, {
const node = new RProxyNode<T>(null, {
root: { $: raw },
});
return node.proxy as ReactiveProxy<T>;
return node.proxy;
}
}
export function createComputed<T>(fn: T) {
const rNode = new RProxyNode(fn, { isComputed: true });
export function createComputed<T extends Fn>(fn: T) {
const rNode = new RProxyNode<T>(fn, { isComputed: true });
return rNode.proxy;
}
@ -45,13 +48,19 @@ export function createWatch<T>(fn: T) {
rNode.get();
}
export function getOrCreateChildProxy(value: unknown, parent: RProxyNode<any>, key: string | symbol): ProxyRNode<any> {
export function getOrCreateChildProxy(value: unknown, parent: RProxyNode<any>, key: string | symbol) {
const child = getOrCreateChildRNode(parent, key);
return child.proxy;
}
export function getOrCreateChildRNode(node: RProxyNode<any>, key: string | symbol): RProxyNode<any> {
if (node.isComputed && !node.parent) {
const root = node.read();
node.root = {
$: root
};
}
let child = node.children?.get(key);
if (!child) {

View File

@ -14,10 +14,27 @@
*/
import { createProxy } from './proxy/RProxyHandler';
import { getRNodeVal, getRootRNode, setRNodeVal } from './RNodeAccessor';
import { setRNodeVal } from './RNodeAccessor';
import { preciseCompare } from './comparison/InDepthComparison';
import { isObject } from './Utils';
import { RNode, Root, runEffects } from './RNode';
import { Dirty, RNode, Root, runEffects } from './RNode';
import { Computation, Signal } from './Types';
export type DeepReactive<T> = T extends Record<string, unknown>
? SignalProxy<T>
: T extends () => infer Return
? ComputationProxy<Return>
: Signal<T>;
export type SignalProxy<T> = {
[Val in keyof T]: SignalProxy<T[Val]>;
} & Signal<T>;
export type ComputationProxy<T> = T extends Record<string, unknown>
? {
readonly [Val in keyof T]: ComputationProxy<T[Val]>;
} & Computation<T>
: Computation<T>;
export interface RNodeOptions {
root?: Root<any> | null;
@ -39,7 +56,7 @@ export class RProxyNode<T = any> extends RNode<T> {
key: KEY | null;
children: Map<KEY, RProxyNode> | null = null;
proxy: any = null;
proxy: DeepReactive<T> = null;
extend: any; // 用于扩展,放一些自定义属性
@ -47,10 +64,9 @@ export class RProxyNode<T = any> extends RNode<T> {
constructor(fnOrValue: (() => T) | T, options?: RNodeOptions) {
super(fnOrValue, options);
this.isComputed = options?.isComputed || false;
this.proxy = createProxy(this);
// Proxy type should be optimized
this.proxy = createProxy(this) as unknown as DeepReactive<T>;
this.parent = options?.parent || null;
this.key = options?.key as KEY;
this.root = options?.root || {};
@ -101,6 +117,9 @@ export class RProxyNode<T = any> extends RNode<T> {
}
setValue(value: T) {
if (this.parent) {
this.state = Dirty;
}
setRNodeVal(this, value);
}
}

View File

@ -93,3 +93,25 @@ export type RNode<T = any> = RootRNode<T> | ChildrenRNode<T>;
export type Reactive<T = any> = RNode<T> | Atom<T>;
export type Signal<T> = {
/**
*
*/
get(): T;
/**
*
*/
read(): T;
set(value: T): void;
};
export type Computation<T> = {
get(): T;
read(): T;
};
export type NonFunctionType<T = any> = Exclude<T, Fn>;
// Use Fn instead of native Function for tslint
export type Fn = (...args: any[]) => any;
export type NoArgFn = () => any;

View File

@ -15,6 +15,7 @@
import { RNode } from './RNode';
import { RProxyNode } from './RProxyNode';
import { Fn } from './Types';
export function isReactiveObj(obj: any) {
return obj instanceof RNode || obj instanceof RProxyNode;
@ -30,7 +31,7 @@ export function isPrimitive(obj: unknown): boolean {
return obj != null && type !== 'object' && type !== 'function';
}
export function isFunction<T extends (...prev: any) => any>(obj: unknown): obj is Function {
export function isFunction(obj: unknown): obj is Fn {
return typeof obj === 'function';
}

View File

@ -17,7 +17,7 @@ import { getOrCreateChildProxy } from '../RNodeCreator';
import { getRNodeVal } from '../RNodeAccessor';
import { isArray } from '../Utils';
import { RNode } from '../RNode';
import {RProxyNode} from "../RProxyNode";
import { DeepReactive, RProxyNode } from '../RProxyNode';
const GET = 'get';
const SET = 'set';
@ -44,7 +44,7 @@ const MODIFY_ARR_FNS = new Set<string | symbol>([
// 数组的遍历方法
const LOOP_ARR_FNS = new Set<string | symbol>(['forEach', 'map', 'every', 'some', 'filter', 'join']);
export function createProxy<T extends any>(proxyNode: RNode) {
export function createProxy<T extends RProxyNode>(proxyNode: T): DeepReactive<T> {
return new Proxy(proxyNode, {
get,
set,

View File

@ -21,7 +21,7 @@ export function printNode(signal: RNode) {
if (signal.isEffect) {
name = `Effect${signal.fn.name ?? ''}`;
} else {
name = `Computation(${(signal as unknown as any).key.toString()})`;
name = `Computation(${(signal as unknown as any).key?.toString() ?? ''})`;
}
} else {
name = 'Signal';

View File

@ -90,6 +90,32 @@ describe('test reactive', () => {
expect(yWatch).toBeCalledTimes(3);
});
it('overwrite array reactive should keep reactive', () => {
const pos = reactive([0, 0]);
const xWatch = jest.fn();
watch(() => {
xWatch(pos[0].get());
});
const yWatch = jest.fn();
watch(() => {
yWatch(pos[1].get());
});
expect(xWatch).toBeCalledTimes(1);
expect(yWatch).toBeCalledTimes(1);
pos.set([1, 1]);
expect(xWatch).toBeCalledTimes(2);
expect(yWatch).toBeCalledTimes(2);
pos.set([2, 1]);
expect(xWatch).toBeCalledTimes(3);
expect(yWatch).toBeCalledTimes(2);
pos.set([2, 2]);
expect(xWatch).toBeCalledTimes(3);
expect(yWatch).toBeCalledTimes(3);
});
it('reactive is a obj', () => {