diff --git a/.eslintrc.js b/.eslintrc.js index 61851c83..5e4838ae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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: { diff --git a/packages/inula-reactive/src/RNode.ts b/packages/inula-reactive/src/RNode.ts index a25677d3..fe80e811 100644 --- a/packages/inula-reactive/src/RNode.ts +++ b/packages/inula-reactive/src/RNode.ts @@ -14,6 +14,8 @@ */ import { printNode } from './utils/printNode'; +import { Signal } from './Types'; +import { isFunction } from './Utils'; let runningRNode: RNode | undefined = undefined; // 当前正执行的RNode let calledGets: RNode[] | null = null; @@ -47,17 +49,16 @@ function defaultEquality(a: any, b: any) { return a === b; } -export class RNode { +export class RNode implements Signal { _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 { } 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 { setValue(value: any) { this._value = value; } - } export function onCleanup(fn: (oldValue: T) => void): void { @@ -324,4 +318,3 @@ export function untrack(fn) { runningRNode = preRContext; } } - diff --git a/packages/inula-reactive/src/RNodeAccessor.ts b/packages/inula-reactive/src/RNodeAccessor.ts index 4d062642..bd2662f5 100644 --- a/packages/inula-reactive/src/RNodeAccessor.ts +++ b/packages/inula-reactive/src/RNodeAccessor.ts @@ -43,12 +43,12 @@ export function setRNodeVal(rNode: RProxyNode, 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; } } diff --git a/packages/inula-reactive/src/RNodeCreator.ts b/packages/inula-reactive/src/RNodeCreator.ts index 5beb4ded..9dd75cb9 100644 --- a/packages/inula-reactive/src/RNodeCreator.ts +++ b/packages/inula-reactive/src/RNodeCreator.ts @@ -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 = RNode | Atom; - -export function createReactive(raw?: T): ReactiveProxy { +export function createReactive(raw?: T): Signal; +export function createReactive(raw?: T): Signal; +export function createReactive(raw?: T): Signal; +export function createReactive(raw?: T): Signal; +export function createReactive | Array | symbol>(raw?: T): DeepReactive; +export function createReactive(raw?: T): DeepReactive | Signal { if (isPrimitive(raw) || raw === null || raw === undefined) { return new RNode(raw, { isSignal: true }); } else { - const node = new RProxyNode(null, { + const node = new RProxyNode(null, { root: { $: raw }, }); - return node.proxy as ReactiveProxy; + return node.proxy; } } -export function createComputed(fn: T) { - const rNode = new RProxyNode(fn, { isComputed: true }); +export function createComputed(fn: T) { + const rNode = new RProxyNode(fn, { isComputed: true }); return rNode.proxy; } @@ -45,13 +48,19 @@ export function createWatch(fn: T) { rNode.get(); } -export function getOrCreateChildProxy(value: unknown, parent: RProxyNode, key: string | symbol): ProxyRNode { +export function getOrCreateChildProxy(value: unknown, parent: RProxyNode, key: string | symbol) { const child = getOrCreateChildRNode(parent, key); return child.proxy; } export function getOrCreateChildRNode(node: RProxyNode, key: string | symbol): RProxyNode { + if (node.isComputed && !node.parent) { + const root = node.read(); + node.root = { + $: root + }; + } let child = node.children?.get(key); if (!child) { diff --git a/packages/inula-reactive/src/RProxyNode.ts b/packages/inula-reactive/src/RProxyNode.ts index 0cca9941..213db8e4 100644 --- a/packages/inula-reactive/src/RProxyNode.ts +++ b/packages/inula-reactive/src/RProxyNode.ts @@ -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 extends Record + ? SignalProxy + : T extends () => infer Return + ? ComputationProxy + : Signal; + +export type SignalProxy = { + [Val in keyof T]: SignalProxy; +} & Signal; + +export type ComputationProxy = T extends Record + ? { + readonly [Val in keyof T]: ComputationProxy; + } & Computation + : Computation; export interface RNodeOptions { root?: Root | null; @@ -39,7 +56,7 @@ export class RProxyNode extends RNode { key: KEY | null; children: Map | null = null; - proxy: any = null; + proxy: DeepReactive = null; extend: any; // 用于扩展,放一些自定义属性 @@ -47,10 +64,9 @@ export class RProxyNode extends RNode { 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; this.parent = options?.parent || null; this.key = options?.key as KEY; this.root = options?.root || {}; @@ -101,6 +117,9 @@ export class RProxyNode extends RNode { } setValue(value: T) { + if (this.parent) { + this.state = Dirty; + } setRNodeVal(this, value); } } diff --git a/packages/inula-reactive/src/Types.ts b/packages/inula-reactive/src/Types.ts index c6cc9a11..dd448a35 100644 --- a/packages/inula-reactive/src/Types.ts +++ b/packages/inula-reactive/src/Types.ts @@ -93,3 +93,25 @@ export type RNode = RootRNode | ChildrenRNode; export type Reactive = RNode | Atom; +export type Signal = { + /** + * 返回响应式对象的值,自动追踪依赖 + */ + get(): T; + /** + * 返回响应式对象的值,不追踪依赖 + */ + read(): T; + set(value: T): void; +}; + +export type Computation = { + get(): T; + read(): T; +}; + +export type NonFunctionType = Exclude; + +// Use Fn instead of native Function for tslint +export type Fn = (...args: any[]) => any; +export type NoArgFn = () => any; diff --git a/packages/inula-reactive/src/Utils.ts b/packages/inula-reactive/src/Utils.ts index b2607c81..ff09dec5 100644 --- a/packages/inula-reactive/src/Utils.ts +++ b/packages/inula-reactive/src/Utils.ts @@ -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 any>(obj: unknown): obj is Function { +export function isFunction(obj: unknown): obj is Fn { return typeof obj === 'function'; } diff --git a/packages/inula-reactive/src/proxy/RProxyHandler.ts b/packages/inula-reactive/src/proxy/RProxyHandler.ts index 9771fcad..1d2f6cf7 100644 --- a/packages/inula-reactive/src/proxy/RProxyHandler.ts +++ b/packages/inula-reactive/src/proxy/RProxyHandler.ts @@ -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([ // 数组的遍历方法 const LOOP_ARR_FNS = new Set(['forEach', 'map', 'every', 'some', 'filter', 'join']); -export function createProxy(proxyNode: RNode) { +export function createProxy(proxyNode: T): DeepReactive { return new Proxy(proxyNode, { get, set, diff --git a/packages/inula-reactive/src/utils/printNode.ts b/packages/inula-reactive/src/utils/printNode.ts index 4bd1896b..7eff51f3 100644 --- a/packages/inula-reactive/src/utils/printNode.ts +++ b/packages/inula-reactive/src/utils/printNode.ts @@ -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'; diff --git a/packages/inula-reactive/tests/reactive.test.ts b/packages/inula-reactive/tests/reactive.test.ts index 2ef0a4fb..f3f119ad 100644 --- a/packages/inula-reactive/tests/reactive.test.ts +++ b/packages/inula-reactive/tests/reactive.test.ts @@ -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', () => {