feat: watch

main
CCherry07 8 months ago
parent 9cf1354502
commit cff02e6911
  1. 2
      src/contents.ts
  2. 34
      src/core.ts
  3. 16
      src/deepSignal.ts
  4. 25
      src/test/index.test.ts
  5. 80
      src/utils.ts
  6. 152
      src/watch.ts

@ -1,3 +1,5 @@
export enum ReactiveFlags {
IS_SIGNAL = '__v_isSignal',
SKIP = "__v_skip",
IS_SHALLOW = "__v_isShallow",
}

@ -39,6 +39,7 @@ export function signal<T>(oldValue?: T): Signal<T | undefined> {
export class Signal<T = any> implements Dependency {
public readonly [ReactiveFlags.IS_SIGNAL] = true
public readonly [ReactiveFlags.SKIP] = true
// Dependency fields
subs: Link | undefined = undefined;
subsTail: Link | undefined = undefined;
@ -145,13 +146,22 @@ export function effect<T>(fn: () => T): Effect<T> {
return e;
}
export enum EffectFlags {
/**
* ReactiveEffect only
*/
ALLOW_RECURSE = 1 << 7,
PAUSED = 1 << 8,
NOTIFIED = 1 << 9,
STOP = 1 << 10,
}
export class Effect<T = any> implements Subscriber {
readonly [ReactiveFlags.IS_SIGNAL] = true
// Subscriber fields
deps: Link | undefined = undefined;
depsTail: Link | undefined = undefined;
flags: SubscriberFlags = SubscriberFlags.Effect;
constructor(
public fn: () => T
) { }
@ -162,8 +172,28 @@ export class Effect<T = any> implements Subscriber {
flags & SubscriberFlags.Dirty
|| (flags & SubscriberFlags.PendingComputed && updateDirtyFlag(this, flags))
) {
this.run();
this.scheduler();
}
}
scheduler(): void {
if (this.dirty) {
this.run()
}
}
get active(): boolean {
return !(this.flags & EffectFlags.STOP)
}
get dirty(): boolean {
const flags = this.flags
if (
flags & SubscriberFlags.Dirty ||
(flags & SubscriberFlags.PendingComputed && updateDirtyFlag(this, flags))
) {
return true
}
return false
}
run(): T {

@ -1,3 +1,4 @@
import { ReactiveFlags } from "./contents";
import { computed, Signal, signal } from "./core";
const proxyToSignals = new WeakMap();
@ -9,6 +10,14 @@ const rg = /^\$/;
const descriptor = Object.getOwnPropertyDescriptor;
let peeking = false;
export const isDeepSignal = (source: any) => {
return proxyToSignals.has(source);
}
export const isShallow = (source: any) => {
return ignore.has(source)
}
export const deepSignal = <T extends object>(obj: T): DeepSignal<T> => {
if (!shouldProxy(obj)) throw new Error("This object can't be observed.");
if (!objToProxy.has(obj))
@ -31,7 +40,7 @@ export const peek = <
return value as RevertDeepSignal<RevertDeepSignalObject<T>[K]>;
};
const isShallow = Symbol("shallow");
const shallowFlag = Symbol(ReactiveFlags.IS_SHALLOW);
export function shallow<T extends object>(obj: T): Shallow<T> {
ignore.add(obj);
return obj as Shallow<T>;
@ -84,6 +93,7 @@ const get =
signals.set(key, signal(value));
}
}
// deep getter
return returnSignal ? signals.get(key) : signals.get(key).get();
};
@ -153,7 +163,7 @@ const shouldProxy = (val: any): boolean => {
export type DeepSignal<T> = T extends Function
? T
: T extends { [isShallow]: true }
: T extends { [shallowFlag]: true }
? T
: T extends Array<unknown>
? DeepSignalArray<T>
@ -274,7 +284,7 @@ type DeepSignalArray<T> = DeepArray<ArrayType<T>> & {
$length?: Signal<number>;
};
export type Shallow<T extends object> = T & { [isShallow]: true };
export type Shallow<T extends object> = T & { [shallowFlag]: true };
export declare const useDeepSignal: <T extends object>(obj: T) => DeepSignal<T>;
// @ts-ignore

@ -1,6 +1,7 @@
import { deepSignal, peek, RevertDeepSignal, shallow } from "../index"
import { describe, it, expect, beforeEach } from "vitest"
import { describe, it, expect, beforeEach, test } from "vitest"
import { signal, Signal, effect } from "..";
import { watch } from "../watch";
type Store = {
a?: number;
nested: { b?: number };
@ -1111,3 +1112,25 @@ describe("deepsignal/core", () => {
});
});
});
describe('watch', () => {
test('abc', () => {
const store = deepSignal({
userinfo: {
name: "tom"
}
})
const stop = watch(store, (newValue, oldValue) => {
console.log('newValue', newValue);
console.log('oldValue', oldValue);
}, {
immediate: true,
deep: true,
})
store.userinfo.name = "jon"
stop()
store.userinfo.name = 'jon2'
})
})

@ -0,0 +1,80 @@
import { ReactiveFlags } from "./contents"
import { isSignal } from "./core"
export const objectToString: typeof Object.prototype.toString =
Object.prototype.toString
export const toTypeString = (value: unknown): string =>
objectToString.call(value)
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol,
): key is keyof typeof val => hasOwnProperty.call(val, key)
export const isArray: typeof Array.isArray = Array.isArray
export const isMap = (val: unknown): val is Map<any, any> =>
toTypeString(val) === '[object Map]'
export const isSet = (val: unknown): val is Set<any> =>
toTypeString(val) === '[object Set]'
export const isDate = (val: unknown): val is Date =>
toTypeString(val) === '[object Date]'
export const isRegExp = (val: unknown): val is RegExp =>
toTypeString(val) === '[object RegExp]'
export const isFunction = (val: unknown): val is Function =>
typeof val === 'function'
export const isString = (val: unknown): val is string => typeof val === 'string'
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return (
(isObject(val) || isFunction(val)) &&
isFunction((val as any).then) &&
isFunction((val as any).catch)
)
}
export const isPlainObject = (val: unknown): val is object =>
toTypeString(val) === '[object Object]'
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
export function traverse(
value: unknown,
depth: number = Infinity,
seen?: Set<unknown>,
): unknown {
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
seen = seen || new Set()
if (seen.has(value)) {
return value
}
seen.add(value)
depth--
if (isSignal(value)) {
traverse(value.value, depth, seen)
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], depth, seen)
}
} else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => {
traverse(v, depth, seen)
})
} else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], depth, seen)
}
for (const key of Object.getOwnPropertySymbols(value)) {
if (Object.prototype.propertyIsEnumerable.call(value, key)) {
traverse(value[key as any], depth, seen)
}
}
}
return value
}

@ -0,0 +1,152 @@
import { Computed, Effect, isSignal, Signal, } from './core';
import { hasChanged, isArray, traverse } from './utils';
import { isDeepSignal, isShallow } from "./deepSignal"
export type OnCleanup = (cleanupFn: () => void) => void
export type WatchEffect = (onCleanup: OnCleanup) => void
export type WatchSource<T = any> = Signal<T> | Computed<T> | (() => T)
export interface WatchOptions<Immediate = boolean> {
immediate?: Immediate
deep?: boolean | number
once?: boolean
}
export type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup,
) => any
const INITIAL_WATCHER_VALUE = {}
let activeWatcher!: Effect
export const remove = <T>(arr: T[], el: T): void => {
const i = arr.indexOf(el)
if (i > -1) {
arr.splice(i, 1)
}
}
export function watch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb?: WatchCallback,
options: WatchOptions = {}
) {
const { once, immediate, deep } = options
let effect!: Effect
let getter!: () => any
// let boundCleanup: typeof onWatcherCleanup
let forceTrigger = false
let isMultiSource = false
const signalGetter = (source: object) => {
// traverse will happen in wrapped getter below
if (deep) return source
// for `deep: false | 0` or shallow reactive, only traverse root-level properties
if (isShallow(source) || deep === false || deep === 0)
return traverse(source, 1)
// for `deep: undefined` on a reactive object, deeply traverse all properties
return traverse(source)
}
const watchHandle = () => {
effect.stop()
return effect
}
if (once && cb) {
const _cb = cb
cb = (...args) => {
_cb(...args)
watchHandle()
}
}
if (isSignal(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isDeepSignal(source)) {
getter = () => {
return signalGetter(source)
}
forceTrigger = true
} else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(s => isDeepSignal(s) || isShallow(s))
getter = () =>
source.map(s => {
if (isSignal(s)) {
return s.value
} else if (isDeepSignal(s)) {
return signalGetter(s)
}
})
}
if (cb && deep) {
const baseGetter = getter
const depth = deep === true ? Infinity : deep
getter = () => traverse(baseGetter(), depth)
}
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
const job = (immediateFirstRun?: boolean) => {
if (!effect.active || (!immediateFirstRun && !effect.dirty)) {
return
}
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue))
) {
const currentWatcher = activeWatcher
activeWatcher = effect
try {
const args = [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
effect.stop,
]
// @ts-ignore
cb!(...args)
oldValue = newValue
} finally {
activeWatcher = currentWatcher
}
}
} else {
// watchEffect
effect.run()
}
}
effect = new Effect(getter)
effect.scheduler = job
if (cb) {
if (immediate) {
job(true)
} else {
oldValue = effect.run()
}
} else {
effect.run()
}
return watchHandle
}
Loading…
Cancel
Save