You've already forked obsidian-visualiser
Rework reactivity for array listening
This commit is contained in:
304
shared/reactive.ts
Normal file
304
shared/reactive.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
export type Reactive<T> = T | (() => T);
|
||||
|
||||
export const isString = (val: unknown): val is string => typeof val === 'string';
|
||||
const isIntegerKey = (key: unknown): boolean => isString(key) && key !== 'NaN' && key[0] !== '-' && '' + parseInt(key, 10) === key;
|
||||
|
||||
let defered = false, _deferSet = new Set<() => void>();
|
||||
export const _defer = (fn: () => void) => {
|
||||
if(!defered)
|
||||
{
|
||||
defered = true;
|
||||
queueMicrotask(() => {
|
||||
_deferSet.forEach(e => e());
|
||||
_deferSet.clear();
|
||||
defered = false;
|
||||
});
|
||||
}
|
||||
|
||||
_deferSet.add(fn);
|
||||
}
|
||||
let activeEffect: (() => void) | null = null, _isTracking = true;
|
||||
const SYMBOLS = {
|
||||
PROXY: Symbol('is a proxy'),
|
||||
ITERATE: Symbol('iterating'),
|
||||
RAW: Symbol('raw value'),
|
||||
} as const;
|
||||
|
||||
function reactiveReadArray<T>(array: T[]): T[]
|
||||
{
|
||||
const _raw = raw(array)
|
||||
if (_raw === array) return _raw;
|
||||
track(_raw, SYMBOLS.ITERATE);
|
||||
return _raw.map(wrapReactive);
|
||||
}
|
||||
function shallowReadArray<T>(arr: T[]): T[]
|
||||
{
|
||||
track((arr = raw(arr)), SYMBOLS.ITERATE);
|
||||
return arr;
|
||||
}
|
||||
function iterator(self: unknown[], method: keyof Array<unknown>, wrapValue: (value: any) => unknown)
|
||||
{
|
||||
const arr = shallowReadArray(self);
|
||||
const iter = (arr[method] as any)() as IterableIterator<unknown> & {
|
||||
_next: IterableIterator<unknown>['next']
|
||||
};
|
||||
if (arr !== self && !isShallow(self))
|
||||
{
|
||||
iter._next = iter.next;
|
||||
iter.next = () => {
|
||||
const result = iter._next();
|
||||
if (!result.done) result.value = wrapValue(result.value);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return iter;
|
||||
}
|
||||
function wrapReactive(obj: any): any
|
||||
{
|
||||
return obj && typeof obj === 'object' ? reactive(obj as Proxy<object>) : obj;
|
||||
}
|
||||
const arrayProto = Array.prototype
|
||||
function apply(self: unknown[], method: keyof Array<any>, fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, wrappedRetFn?: (result: any) => unknown, args?: IArguments)
|
||||
{
|
||||
const arr = shallowReadArray(self);
|
||||
const needsWrap = arr !== self;
|
||||
const methodFn = arr[method] as Function;
|
||||
|
||||
if (methodFn !== arrayProto[method as any])
|
||||
{
|
||||
const result = methodFn.apply(self, args);
|
||||
return needsWrap ? toReactive(result) : result;
|
||||
}
|
||||
|
||||
let wrappedFn = fn;
|
||||
if (arr !== self)
|
||||
{
|
||||
if (needsWrap)
|
||||
{
|
||||
wrappedFn = function (this: unknown, item, index) {
|
||||
return fn.call(this, wrapReactive(item), index, self);
|
||||
};
|
||||
}
|
||||
else if (fn.length > 2)
|
||||
{
|
||||
wrappedFn = function (this: unknown, item, index) {
|
||||
return fn.call(this, item, index, self);
|
||||
};
|
||||
}
|
||||
}
|
||||
const result = methodFn.call(arr, wrappedFn, thisArg);
|
||||
return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result;
|
||||
}
|
||||
function reduce(self: unknown[], method: keyof Array<any>, fn: (acc: unknown, item: unknown, index: number, array: unknown[]) => unknown, args: unknown[])
|
||||
{
|
||||
const arr = shallowReadArray(self);
|
||||
let wrappedFn = fn;
|
||||
if (arr !== self && fn.length > 3)
|
||||
{
|
||||
wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, wrapReactive(item), index, self) };
|
||||
}
|
||||
else
|
||||
{
|
||||
wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, wrapReactive(item), index, self) ;}
|
||||
}
|
||||
return (arr[method] as any)(wrappedFn, ...args);
|
||||
}
|
||||
function searchProxy(self: unknown[], method: keyof Array<any>, args: unknown[])
|
||||
{
|
||||
const arr = raw(self) as any;
|
||||
track(arr, SYMBOLS.ITERATE);
|
||||
const res = arr[method](...args);
|
||||
|
||||
if ((res === -1 || res === false) && isProxy(args[0]))
|
||||
{
|
||||
args[0] = raw(args[0]);
|
||||
return arr[method](...args);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
function noTracking(self: unknown[], method: keyof Array<any>, args: unknown[] = [])
|
||||
{
|
||||
_isTracking = false;
|
||||
const res = (raw(self) as any)[method].apply(self, args);
|
||||
_isTracking = true;
|
||||
return res;
|
||||
}
|
||||
|
||||
const arraySubstitute = <any>{ // <-- <any> is required to allow __proto__ without getting an error
|
||||
__proto__: null, // <-- Required to remove the object prototype removing the object default functions from the substitution
|
||||
[Symbol.iterator]() { return iterator(this, Symbol.iterator, item => wrapReactive(item)) },
|
||||
concat(...args: unknown[]) { return reactiveReadArray(this).concat(...args.map(x => (Array.isArray(x) ? reactiveReadArray(x) : x))) },
|
||||
entries() { return iterator(this, 'entries', (value: [number, unknown]) => { value[1] = wrapReactive(value[1]); return value; }) },
|
||||
every(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'every', fn, thisArg, undefined, arguments) },
|
||||
filter(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply( this, 'filter', fn, thisArg, v => v.map((item: unknown) => wrapReactive(item)), arguments, ) },
|
||||
find(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply( this, 'find', fn, thisArg, item => wrapReactive(item), arguments, ) },
|
||||
findIndex(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply(this, 'findIndex', fn, thisArg, undefined, arguments) },
|
||||
findLast(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply( this, 'findLast', fn, thisArg, item => wrapReactive(item), arguments) },
|
||||
findLastIndex(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply(this, 'findLastIndex', fn, thisArg, undefined, arguments) },
|
||||
forEach(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'forEach', fn, thisArg, undefined, arguments) },
|
||||
includes(...args: unknown[]) { return searchProxy(this, 'includes', args) },
|
||||
indexOf(...args: unknown[]) { return searchProxy(this, 'indexOf', args) },
|
||||
join(separator?: string) { return reactiveReadArray(this).join(separator) },
|
||||
lastIndexOf(...args: unknown[]) { return searchProxy(this, 'lastIndexOf', args) },
|
||||
map(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'map', fn, thisArg, undefined, arguments) },
|
||||
pop() { return noTracking(this, 'pop') },
|
||||
push(...args: unknown[]) { return noTracking(this, 'push', args) },
|
||||
reduce(fn: ( acc: unknown, item: unknown, index: number, array: unknown[], ) => unknown, ...args: unknown[]) { return reduce(this, 'reduce', fn, args) },
|
||||
reduceRight(fn: ( acc: unknown, item: unknown, index: number, array: unknown[], ) => unknown, ...args: unknown[]) { return reduce(this, 'reduceRight', fn, args) },
|
||||
shift() { return noTracking(this, 'shift') },
|
||||
some(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'some', fn, thisArg, undefined, arguments) },
|
||||
splice(...args: unknown[]) { return noTracking(this, 'splice', args) },
|
||||
toReversed() { return reactiveReadArray(this).toReversed() },
|
||||
toSorted(comparer?: (a: unknown, b: unknown) => number) { return reactiveReadArray(this).toSorted(comparer) },
|
||||
toSpliced(...args: unknown[]) { return (reactiveReadArray(this).toSpliced as any)(...args) },
|
||||
unshift(...args: unknown[]) { return noTracking(this, 'unshift', args) },
|
||||
values() { return iterator(this, 'values', item => wrapReactive(item)) }, /* */
|
||||
};
|
||||
|
||||
// Store object to proxy correspondance
|
||||
const _reactiveCache = new WeakMap();
|
||||
|
||||
// Store a Weak map of all the tracked object.
|
||||
// For each object, we have a map of its properties, allowing us to effectively listen to absolutely everything on the object
|
||||
// For a given property, we have a set of "effect" (function called on value update)
|
||||
type Dependency = Set<() => void>;
|
||||
const _tracker = new WeakMap<object, Map<string | symbol | null, Dependency>>();
|
||||
|
||||
function trigger(target: object, key?: string | symbol | null, value?: unknown)
|
||||
{
|
||||
const dependencies = _tracker.get(target);
|
||||
if(!dependencies) return;
|
||||
|
||||
const run = (dep?: Dependency) => {
|
||||
dep?.forEach(_defer);
|
||||
};
|
||||
|
||||
const isArray = Array.isArray(target);
|
||||
const arrayIndex = isIntegerKey(key);
|
||||
|
||||
//When the array length is modified, call not only the length and ITERATE dependencies but also the added/removed items dependencies
|
||||
if(isArray && key === 'length')
|
||||
{
|
||||
// Run for 'length' key, SYMBOL.ITERATE and any index key after the new length (for reduction)
|
||||
dependencies.forEach((v, k: any) => (k === 'length' || k === SYMBOLS.ITERATE || (isIntegerKey(k) && k >= (value as number))) && run(v));
|
||||
}
|
||||
else
|
||||
{
|
||||
key !== undefined && run(dependencies.get(key));
|
||||
|
||||
arrayIndex && run(dependencies.get(SYMBOLS.ITERATE));
|
||||
}
|
||||
}
|
||||
function track(target: object, key: string | symbol | null)
|
||||
{
|
||||
if(!activeEffect || !_isTracking) return;
|
||||
|
||||
let dependencies = _tracker.get(target);
|
||||
if(!dependencies)
|
||||
{
|
||||
dependencies = new Map();
|
||||
_tracker.set(target, dependencies);
|
||||
}
|
||||
|
||||
let set = dependencies.get(key);
|
||||
if(!set)
|
||||
{
|
||||
set = new Set();
|
||||
dependencies.set(key, set);
|
||||
}
|
||||
|
||||
set.add(activeEffect);
|
||||
|
||||
//if(set) console.log('Tracking %o with key "%s"', target, key, set.size);
|
||||
}
|
||||
export type Proxy<T> = T & {
|
||||
[SYMBOLS.PROXY]?: boolean;
|
||||
[SYMBOLS.RAW]?: T;
|
||||
};
|
||||
export function isProxy(target: Proxy<any>): boolean
|
||||
{
|
||||
return target[SYMBOLS.PROXY];
|
||||
}
|
||||
export function reactive<T extends object>(obj: T | Proxy<T>): T | Proxy<T>
|
||||
{
|
||||
if((obj as Proxy<T>)[SYMBOLS.PROXY])
|
||||
return obj;
|
||||
|
||||
if(_reactiveCache.has(obj))
|
||||
return _reactiveCache.get(obj)!;
|
||||
|
||||
const prototype = Object.getPrototypeOf(obj);
|
||||
const isArray = Array.isArray(obj);
|
||||
|
||||
const proxy = new Proxy<T>(obj, {
|
||||
get: (target, key, receiver) => {
|
||||
if(key === SYMBOLS.PROXY)
|
||||
return true;
|
||||
else if(key === SYMBOLS.RAW)
|
||||
return obj;
|
||||
|
||||
if(key in arraySubstitute)
|
||||
return arraySubstitute[key]!;
|
||||
|
||||
const value = Reflect.get(target, key, receiver);
|
||||
|
||||
track(target, key);
|
||||
|
||||
//If the value is an object, mark it as reactive dynamically
|
||||
if(value && typeof value === 'object')
|
||||
return reactive(value as Proxy<object>);
|
||||
|
||||
return value;
|
||||
},
|
||||
set: (target, key, value, receiver) => {
|
||||
if(key === SYMBOLS.PROXY || key === SYMBOLS.RAW)
|
||||
return false;
|
||||
|
||||
const result = Reflect.set(target, key, raw(value), receiver);
|
||||
|
||||
trigger(target, key, value);
|
||||
|
||||
return result;
|
||||
},
|
||||
deleteProperty: (target, key) => {
|
||||
const has = key in target;
|
||||
const result = Reflect.deleteProperty(target, key);
|
||||
if(result && has) trigger(target, key);
|
||||
return result;
|
||||
},
|
||||
has: (target, key) => {
|
||||
const result = Reflect.has(target, key);
|
||||
track(target, key);
|
||||
return result;
|
||||
},
|
||||
ownKeys: (target) => {
|
||||
const result = Reflect.ownKeys(target);
|
||||
track(target, SYMBOLS.ITERATE);
|
||||
return result;
|
||||
}
|
||||
}) as Proxy<T>;
|
||||
|
||||
_reactiveCache.set(obj, proxy);
|
||||
return proxy;
|
||||
}
|
||||
export function raw<T>(obj: T): T
|
||||
{
|
||||
return typeof obj === 'object' ? ((obj as Proxy<T>)[SYMBOLS.RAW] as Proxy<T> | undefined) ?? obj : obj;
|
||||
}
|
||||
export function reactivity<T>(reactiveProperty: Reactive<T>, effect: (processed: T) => void)
|
||||
{
|
||||
// Function wrapping to keep the context safe and secured.
|
||||
// Also useful to retrigger the tracking system if the reactive property provides new properties (via conditions for example)
|
||||
const secureEffect = () => effect(typeof reactiveProperty === 'function' ? (reactiveProperty as () => T)() : reactiveProperty);
|
||||
const secureContext = () => {
|
||||
activeEffect = secureContext;
|
||||
try {
|
||||
return secureEffect();
|
||||
} finally {
|
||||
activeEffect = null;
|
||||
}
|
||||
};
|
||||
|
||||
return secureContext();
|
||||
}
|
||||
Reference in New Issue
Block a user