304 lines
12 KiB
TypeScript
304 lines
12 KiB
TypeScript
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();
|
|
} |