mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] reactivity: add support for collections (Set/Map WeakSet/WeakMap)
This commit is contained in:
committed by
Géry Debongnie
parent
076b0d774e
commit
d735213758
+245
-54
@@ -7,9 +7,9 @@ const SKIP = Symbol("Skip");
|
||||
// Special key to subscribe to, to be notified of key creation/deletion
|
||||
const KEYCHANGES = Symbol("Key changes");
|
||||
|
||||
type ObjectKey = string | number | symbol;
|
||||
|
||||
type Target = object;
|
||||
type Collection = Set<any> | Map<any, any> | WeakMap<any, any>;
|
||||
type CollectionRawType = "Set" | "Map" | "WeakMap";
|
||||
|
||||
export type Reactive<T extends Target = Target> = T & {
|
||||
[TARGET]: any;
|
||||
@@ -18,8 +18,25 @@ export type Reactive<T extends Target = Target> = T & {
|
||||
export type NonReactive<T extends Target = Target> = T & {
|
||||
[SKIP]: any;
|
||||
};
|
||||
const objectToString = Object.prototype.toString;
|
||||
|
||||
const objectToString = Object.prototype.toString;
|
||||
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
||||
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
||||
|
||||
/**
|
||||
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
||||
* many native objects such as Promise (whose toString is [object Promise])
|
||||
* or Date ([object Date]), while also supporting collections without using
|
||||
* instanceof in a loop
|
||||
*
|
||||
* @param obj the object to check
|
||||
* @returns the raw type of the object
|
||||
*/
|
||||
function rawType(obj: any) {
|
||||
return objectToString.call(obj).slice(8, -1);
|
||||
}
|
||||
/**
|
||||
* Checks whether a given value can be made into a reactive object.
|
||||
*
|
||||
@@ -30,11 +47,17 @@ function canBeMadeReactive(value: any): boolean {
|
||||
if (typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
// extract "RawType" from strings like "[object RawType]" => this lets us
|
||||
// ignore many native objects such as Promise (whose toString is [object Promise])
|
||||
// or Date ([object Date]).
|
||||
const rawType = objectToString.call(value).slice(8, -1);
|
||||
return rawType === "Object" || rawType === "Array";
|
||||
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
||||
}
|
||||
/**
|
||||
* Creates a reactive from the given object/callback if possible and returns it,
|
||||
* returns the original object otherwise.
|
||||
*
|
||||
* @param value the value make reactive
|
||||
* @returns a reactive for the given object when possible, the original otherwise
|
||||
*/
|
||||
function possiblyReactive(val: any, cb: Callback) {
|
||||
return canBeMadeReactive(val) ? reactive(val, cb) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +81,7 @@ export function toRaw<T extends object>(value: Reactive<T>): T {
|
||||
return value[TARGET] || value;
|
||||
}
|
||||
|
||||
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
|
||||
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>();
|
||||
/**
|
||||
* Observes a given key on a target with an callback. The callback will be
|
||||
* called when the given key changes on the target.
|
||||
@@ -68,7 +91,7 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>
|
||||
* or deletion)
|
||||
* @param callback the function to call when the key changes
|
||||
*/
|
||||
function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): void {
|
||||
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
|
||||
if (!targetToKeysToCallbacks.get(target)) {
|
||||
targetToKeysToCallbacks.set(target, new Map());
|
||||
}
|
||||
@@ -91,7 +114,7 @@ function observeTargetKey(target: Target, key: ObjectKey, callback: Callback): v
|
||||
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
||||
* or deleted)
|
||||
*/
|
||||
function notifyReactives(target: Target, key: ObjectKey): void {
|
||||
function notifyReactives(target: Target, key: PropertyKey): void {
|
||||
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
||||
if (!keyToCallbacks) {
|
||||
return;
|
||||
@@ -177,50 +200,218 @@ export function reactive<T extends Target>(
|
||||
}
|
||||
const reactivesForTarget = reactiveCache.get(target)!;
|
||||
if (!reactivesForTarget.has(callback)) {
|
||||
const proxy = new Proxy(target, {
|
||||
get(target: any, key: ObjectKey, proxy: Reactive<T>) {
|
||||
if (key === TARGET) {
|
||||
return target;
|
||||
}
|
||||
observeTargetKey(target, key, callback);
|
||||
const value = Reflect.get(target, key, proxy);
|
||||
if (!canBeMadeReactive(value)) {
|
||||
return value;
|
||||
}
|
||||
return reactive(value, callback);
|
||||
},
|
||||
set(target, key, value, proxy) {
|
||||
const isNewKey = !Object.hasOwnProperty.call(target, key);
|
||||
const originalValue = Reflect.get(target, key, proxy);
|
||||
const ret = Reflect.set(target, key, value, proxy);
|
||||
if (isNewKey) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
}
|
||||
// While Array length may trigger the set trap, it's not actually set by this
|
||||
// method but is updated behind the scenes, and the trap is not called with the
|
||||
// new value. We disable the "same-value-optimization" for it because of that.
|
||||
if (originalValue !== value || (Array.isArray(target) && key === "length")) {
|
||||
notifyReactives(target, key);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
const ret = Reflect.deleteProperty(target, key);
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
notifyReactives(target, key);
|
||||
return ret;
|
||||
},
|
||||
ownKeys(target) {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return Reflect.ownKeys(target);
|
||||
},
|
||||
has(target, key) {
|
||||
// TODO: this observes all key changes instead of only the presence of the argument key
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return Reflect.has(target, key);
|
||||
},
|
||||
});
|
||||
const targetRawType = rawType(target);
|
||||
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
||||
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
||||
: basicProxyHandler<T>(callback);
|
||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||
reactivesForTarget.set(callback, proxy);
|
||||
}
|
||||
return reactivesForTarget.get(callback) as Reactive<T>;
|
||||
}
|
||||
/**
|
||||
* Creates a basic proxy handler for regular objects and arrays.
|
||||
*
|
||||
* @param callback @see reactive
|
||||
* @returns a proxy handler object
|
||||
*/
|
||||
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> {
|
||||
return {
|
||||
get(target: any, key: PropertyKey, proxy: Reactive<T>) {
|
||||
if (key === TARGET) {
|
||||
return target;
|
||||
}
|
||||
observeTargetKey(target, key, callback);
|
||||
return possiblyReactive(Reflect.get(target, key, proxy), callback);
|
||||
},
|
||||
set(target, key, value, proxy) {
|
||||
const isNewKey = !objectHasOwnProperty.call(target, key);
|
||||
const originalValue = Reflect.get(target, key, proxy);
|
||||
const ret = Reflect.set(target, key, value, proxy);
|
||||
if (isNewKey) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
}
|
||||
// While Array length may trigger the set trap, it's not actually set by this
|
||||
// method but is updated behind the scenes, and the trap is not called with the
|
||||
// new value. We disable the "same-value-optimization" for it because of that.
|
||||
if (originalValue !== value || (Array.isArray(target) && key === "length")) {
|
||||
notifyReactives(target, key);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
const ret = Reflect.deleteProperty(target, key);
|
||||
// TODO: only notify when something was actually deleted
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
notifyReactives(target, key);
|
||||
return ret;
|
||||
},
|
||||
ownKeys(target) {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return Reflect.ownKeys(target);
|
||||
},
|
||||
has(target, key) {
|
||||
// TODO: this observes all key changes instead of only the presence of the argument key
|
||||
// observing the key itself would observe value changes instead of presence changes
|
||||
// so we may need a finer grained system to distinguish observing value vs presence.
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return Reflect.has(target, key);
|
||||
},
|
||||
} as ProxyHandler<T>;
|
||||
}
|
||||
/**
|
||||
* Creates a function that will observe the key that is passed to it when called
|
||||
* and delegates to the underlying method.
|
||||
*
|
||||
* @param methodName name of the method to delegate to
|
||||
* @param target @see reactive
|
||||
* @param callback @see reactive
|
||||
*/
|
||||
function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callback) {
|
||||
return (key: any) => {
|
||||
key = toRaw(key);
|
||||
observeTargetKey(target, key, callback);
|
||||
return possiblyReactive(target[methodName](key), callback);
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates an iterable that will delegate to the underlying iteration method and
|
||||
* observe keys as necessary.
|
||||
*
|
||||
* @param methodName name of the method to delegate to
|
||||
* @param target @see reactive
|
||||
* @param callback @see reactive
|
||||
*/
|
||||
function makeIteratorObserver(
|
||||
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
|
||||
target: any,
|
||||
callback: Callback
|
||||
) {
|
||||
return function* () {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
const keys = target.keys();
|
||||
for (const item of target[methodName]()) {
|
||||
const key = keys.next().value;
|
||||
observeTargetKey(target, key, callback);
|
||||
yield possiblyReactive(item, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates a function that will delegate to an underlying method, and check if
|
||||
* that method has modified the presence or value of a key, and notify the
|
||||
* reactives appropriately.
|
||||
*
|
||||
* @param setterName name of the method to delegate to
|
||||
* @param getterName name of the method which should be used to retrieve the
|
||||
* value before calling the delegate method for comparison purposes
|
||||
* @param target @see reactive
|
||||
*/
|
||||
function delegateAndNotify(
|
||||
setterName: "set" | "add" | "delete",
|
||||
getterName: "has" | "get",
|
||||
target: any
|
||||
) {
|
||||
return (key: any, value: any) => {
|
||||
key = toRaw(key);
|
||||
const hadKey = target.has(key);
|
||||
const originalValue = target[getterName](key);
|
||||
const ret = target[setterName](key, value);
|
||||
const hasKey = target.has(key);
|
||||
if (hadKey !== hasKey) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
}
|
||||
if (originalValue !== value) {
|
||||
notifyReactives(target, key);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates a function that will clear the underlying collection and notify that
|
||||
* the keys of the collection have changed.
|
||||
*
|
||||
* @param target @see reactive
|
||||
*/
|
||||
function makeClearNotifier(target: Map<any, any> | Set<any>) {
|
||||
return () => {
|
||||
const allKeys = [...target.keys()];
|
||||
target.clear();
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
for (const key of allKeys) {
|
||||
notifyReactives(target, key);
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Maps raw type of an object to an object containing functions that can be used
|
||||
* to build an appropritate proxy handler for that raw type. Eg: when making a
|
||||
* reactive set, calling the has method should mark the key that is being
|
||||
* retrieved as observed, and calling the add or delete method should notify the
|
||||
* reactives that the key which is being added or deleted has been modified.
|
||||
*/
|
||||
const rawTypeToFuncHandlers = {
|
||||
Set: (target: any, callback: Callback) => ({
|
||||
has: makeKeyObserver("has", target, callback),
|
||||
add: delegateAndNotify("add", "has", target),
|
||||
delete: delegateAndNotify("delete", "has", target),
|
||||
keys: makeIteratorObserver("keys", target, callback),
|
||||
values: makeIteratorObserver("values", target, callback),
|
||||
entries: makeIteratorObserver("entries", target, callback),
|
||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||||
clear: makeClearNotifier(target),
|
||||
get size() {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return target.size;
|
||||
},
|
||||
}),
|
||||
Map: (target: any, callback: Callback) => ({
|
||||
has: makeKeyObserver("has", target, callback),
|
||||
get: makeKeyObserver("get", target, callback),
|
||||
set: delegateAndNotify("set", "get", target),
|
||||
delete: delegateAndNotify("delete", "has", target),
|
||||
keys: makeIteratorObserver("keys", target, callback),
|
||||
values: makeIteratorObserver("values", target, callback),
|
||||
entries: makeIteratorObserver("entries", target, callback),
|
||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||||
clear: makeClearNotifier(target),
|
||||
get size() {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
return target.size;
|
||||
},
|
||||
}),
|
||||
WeakMap: (target: any, callback: Callback) => ({
|
||||
has: makeKeyObserver("has", target, callback),
|
||||
get: makeKeyObserver("get", target, callback),
|
||||
set: delegateAndNotify("set", "get", target),
|
||||
delete: delegateAndNotify("delete", "has", target),
|
||||
}),
|
||||
};
|
||||
/**
|
||||
* Creates a proxy handler for collections (Set/Map/WeakMap)
|
||||
*
|
||||
* @param callback @see reactive
|
||||
* @param target @see reactive
|
||||
* @returns a proxy handler object
|
||||
*/
|
||||
function collectionsProxyHandler<T extends Collection>(
|
||||
target: T,
|
||||
callback: Callback,
|
||||
targetRawType: CollectionRawType
|
||||
): ProxyHandler<T> {
|
||||
// TODO: if performance is an issue we can create the special handlers lazily when each
|
||||
// property is read.
|
||||
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
|
||||
return Object.assign(basicProxyHandler(callback), {
|
||||
get(target: any, key: PropertyKey) {
|
||||
if (key === TARGET) {
|
||||
return target;
|
||||
}
|
||||
if (objectHasOwnProperty.call(specialHandlers, key)) {
|
||||
return (specialHandlers as any)[key];
|
||||
}
|
||||
observeTargetKey(target, key, callback);
|
||||
return possiblyReactive(target[key], callback);
|
||||
},
|
||||
}) as ProxyHandler<T>;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,20 @@ exports[`reactivity in lifecycle can use a state hook 2 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`reactivity in lifecycle can use a state hook on Map 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['counter'].get('value');
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`reactivity in lifecycle change state while mounting component 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -53,6 +53,18 @@ describe("reactivity in lifecycle", () => {
|
||||
expect(n).toBe(2); // no new rendering occured: b was never read via state!
|
||||
});
|
||||
|
||||
test("can use a state hook on Map", async () => {
|
||||
class Counter extends Component {
|
||||
static template = xml`<div><t t-esc="counter.get('value')"/></div>`;
|
||||
counter = useState(new Map([["value", 42]]));
|
||||
}
|
||||
const counter = await mount(Counter, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>42</div>");
|
||||
counter.counter.set("value", 3);
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>3</div>");
|
||||
});
|
||||
|
||||
test("state changes in willUnmount do not trigger rerender", async () => {
|
||||
const steps: string[] = [];
|
||||
class Child extends Component {
|
||||
|
||||
+476
-10
@@ -171,6 +171,8 @@ describe("Reactivity", () => {
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
// Skipped because the hasOwnProperty trap is tripped by *writing*. We
|
||||
// (probably) do not want to subscribe to changes on writes.
|
||||
test.skip("hasOwnProperty causes the key's presence to be observed", async () => {
|
||||
let n = 0;
|
||||
const state = createReactive({}, () => n++);
|
||||
@@ -1093,18 +1095,482 @@ describe("Reactivity", () => {
|
||||
expect(n).toBe(1);
|
||||
expect(state.k).toEqual({ n: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
test("can add collections set/weakset/map/weakmap in a reactive object", () => {
|
||||
const rawSet = new Set();
|
||||
const rawWeakSet = new WeakSet();
|
||||
const rawMap = new Map();
|
||||
const rawWeakMap = new WeakMap();
|
||||
describe("Collections", () => {
|
||||
describe("Set", () => {
|
||||
test("can make reactive Set", () => {
|
||||
const set = new Set<number>();
|
||||
const obj = reactive(set);
|
||||
expect(obj).not.toBe(set);
|
||||
});
|
||||
|
||||
const obj = reactive({ rawSet, rawWeakSet, rawMap, rawWeakMap });
|
||||
expect(obj.rawSet).toBe(rawSet);
|
||||
expect(obj.rawWeakSet).toBe(rawWeakSet);
|
||||
expect(obj.rawMap).toBe(rawMap);
|
||||
expect(obj.rawWeakMap).toBe(rawWeakMap);
|
||||
test("can read", async () => {
|
||||
const state = reactive(new Set([1]));
|
||||
expect(state.has(1)).toBe(true);
|
||||
expect(state.has(0)).toBe(false);
|
||||
});
|
||||
|
||||
test("can add entries", () => {
|
||||
const state = reactive(new Set());
|
||||
state.add(1);
|
||||
expect(state.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
test("can remove entries", () => {
|
||||
const state = reactive(new Set([1]));
|
||||
state.delete(1);
|
||||
expect(state.has(1)).toBe(false);
|
||||
});
|
||||
|
||||
test("can clear entries", () => {
|
||||
const state = reactive(new Set([1]));
|
||||
expect(state.size).toBe(1);
|
||||
state.clear();
|
||||
expect(state.size).toBe(0);
|
||||
});
|
||||
|
||||
test("act like a Set", () => {
|
||||
const state = reactive(new Set([1]));
|
||||
expect([...state.entries()]).toEqual([[1, 1]]);
|
||||
expect([...state.values()]).toEqual([1]);
|
||||
expect([...state.keys()]).toEqual([1]);
|
||||
expect([...state]).toEqual([1]); // Checks Symbol.iterator
|
||||
expect(state.size).toBe(1);
|
||||
expect(typeof state).toBe("object");
|
||||
expect(state).toBeInstanceOf(Set);
|
||||
});
|
||||
|
||||
test("reactive Set contains its keys", () => {
|
||||
const state = reactive(new Set([{}]));
|
||||
expect(state.has(state.keys().next().value)).toBe(true);
|
||||
});
|
||||
|
||||
test("reactive Set contains its values", () => {
|
||||
const state = reactive(new Set([{}]));
|
||||
expect(state.has(state.values().next().value)).toBe(true);
|
||||
});
|
||||
|
||||
test("reactive Set contains its entries' keys and values", () => {
|
||||
const state = reactive(new Set([{}]));
|
||||
const [key, val] = state.entries().next().value;
|
||||
expect(state.has(key)).toBe(true);
|
||||
expect(state.has(val)).toBe(true);
|
||||
});
|
||||
|
||||
test("checking for a key subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([1]), observer);
|
||||
|
||||
expect(state.has(2)).toBe(false); // subscribe to 2
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.add(2);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.has(2)).toBe(true); // subscribe to 2
|
||||
state.delete(2);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.add(2);
|
||||
expect(state.has(2)).toBe(true); // subscribe to 2
|
||||
state.clear();
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
expect(state.has(2)).toBe(false); // subscribe to 2
|
||||
state.clear(); // clearing again doesn't notify again
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
|
||||
state.add(3); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("iterating on keys returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([obj]), observer);
|
||||
const reactiveObj = state.keys().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on values returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([obj]), observer);
|
||||
const reactiveObj = state.values().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on entries returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([obj]), observer);
|
||||
const [reactiveObj, reactiveObj2] = state.entries().next().value;
|
||||
expect(reactiveObj2).toBe(reactiveObj);
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on reactive Set returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Set([obj]), observer);
|
||||
const reactiveObj = state[Symbol.iterator]().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WeakSet", () => {
|
||||
test("cannot make reactive WeakSet", () => {
|
||||
const set = new WeakSet();
|
||||
expect(() => reactive(set)).toThrowError("Cannot make the given value reactive");
|
||||
});
|
||||
|
||||
test("WeakSet in reactive is original WeakSet", () => {
|
||||
const obj = { set: new WeakSet() };
|
||||
const state = reactive(obj);
|
||||
expect(state.set).toBe(obj.set);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Map", () => {
|
||||
test("can make reactive Map", () => {
|
||||
const map = new Map();
|
||||
const obj = reactive(map);
|
||||
expect(obj).not.toBe(map);
|
||||
});
|
||||
|
||||
test("can read", async () => {
|
||||
const state = reactive(new Map([[1, 0]]));
|
||||
expect(state.has(1)).toBe(true);
|
||||
expect(state.has(0)).toBe(false);
|
||||
expect(state.get(1)).toBe(0);
|
||||
expect(state.get(0)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can add entries", () => {
|
||||
const state = reactive(new Map());
|
||||
state.set(1, 2);
|
||||
expect(state.has(1)).toBe(true);
|
||||
expect(state.get(1)).toBe(2);
|
||||
});
|
||||
|
||||
test("can remove entries", () => {
|
||||
const state = reactive(new Map([[1, 2]]));
|
||||
state.delete(1);
|
||||
expect(state.has(1)).toBe(false);
|
||||
expect(state.get(1)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can clear entries", () => {
|
||||
const state = reactive(new Map([[1, 2]]));
|
||||
expect(state.size).toBe(1);
|
||||
state.clear();
|
||||
expect(state.size).toBe(0);
|
||||
});
|
||||
|
||||
test("act like a Map", () => {
|
||||
const state = reactive(new Map([[1, 2]]));
|
||||
expect([...state.entries()]).toEqual([[1, 2]]);
|
||||
expect([...state.values()]).toEqual([2]);
|
||||
expect([...state.keys()]).toEqual([1]);
|
||||
expect([...state]).toEqual([[1, 2]]); // Checks Symbol.iterator
|
||||
expect(state.size).toBe(1);
|
||||
expect(typeof state).toBe("object");
|
||||
expect(state).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
test("reactive Map contains its keys", () => {
|
||||
const state = reactive(new Map([[{}, 1]]));
|
||||
expect(state.has(state.keys().next().value)).toBe(true);
|
||||
});
|
||||
|
||||
test("reactive Map values are equal to doing a get on the appropriate key", () => {
|
||||
const state = reactive(new Map([[1, {}]]));
|
||||
expect(state.get(1)).toBe(state.values().next().value);
|
||||
});
|
||||
|
||||
test("reactive Map contains its entries' keys, and the associated value is the same as doing get", () => {
|
||||
const state = reactive(new Map([[{}, {}]]));
|
||||
const [key, val] = state.entries().next().value;
|
||||
expect(state.has(key)).toBe(true);
|
||||
expect(val).toBe(state.get(key));
|
||||
});
|
||||
|
||||
test("checking for a key with 'has' subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[1, 2]]), observer);
|
||||
|
||||
expect(state.has(2)).toBe(false); // subscribe to 2
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.set(2, 3);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.has(2)).toBe(true); // subscribe to 2
|
||||
state.delete(2);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.set(2, 3);
|
||||
expect(state.has(2)).toBe(true); // subscribe to 2
|
||||
state.clear();
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
expect(state.has(2)).toBe(false); // subscribe to 2
|
||||
state.clear(); // clearing again doesn't notify again
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
|
||||
state.set(3, 4); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("checking for a key with 'get' subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[1, 2]]), observer);
|
||||
|
||||
expect(state.get(2)).toBeUndefined(); // subscribe to 2
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.set(2, 3);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.get(2)).toBe(3); // subscribe to 2
|
||||
state.delete(2);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.delete(2); // deleting again doesn't notify again
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.set(2, 3);
|
||||
expect(state.get(2)).toBe(3); // subscribe to 2
|
||||
state.clear();
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
expect(state.get(2)).toBeUndefined(); // subscribe to 2
|
||||
state.clear(); // clearing again doesn't notify again
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
|
||||
state.set(3, 4); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("getting values returns a reactive", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[1, obj]]), observer);
|
||||
const reactiveObj = state.get(1)!;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on values returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[1, obj]]), observer);
|
||||
const reactiveObj = state.values().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on keys returns reactives", async () => {
|
||||
const obj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[obj, 1]]), observer);
|
||||
const reactiveObj = state.keys().next().value;
|
||||
expect(reactiveObj).not.toBe(obj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(obj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("iterating on reactive map returns reactives", async () => {
|
||||
const keyObj = { a: 2 };
|
||||
const valObj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[keyObj, valObj]]), observer);
|
||||
const [reactiveKeyObj, reactiveValObj] = state[Symbol.iterator]().next().value;
|
||||
expect(reactiveKeyObj).not.toBe(keyObj);
|
||||
expect(reactiveValObj).not.toBe(valObj);
|
||||
expect(toRaw(reactiveKeyObj as any)).toBe(keyObj);
|
||||
expect(toRaw(reactiveValObj as any)).toBe(valObj);
|
||||
reactiveKeyObj.a = 0;
|
||||
reactiveValObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveKeyObj.a; // observe key "a" in key sub-reactive;
|
||||
reactiveKeyObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveValObj.a; // observe key "a" in val sub-reactive;
|
||||
reactiveValObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
reactiveKeyObj.a = 1; // setting same value again shouldn't notify
|
||||
reactiveValObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("iterating on entries returns reactives", async () => {
|
||||
const keyObj = { a: 2 };
|
||||
const valObj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new Map([[keyObj, valObj]]), observer);
|
||||
const [reactiveKeyObj, reactiveValObj] = state.entries().next().value;
|
||||
expect(reactiveKeyObj).not.toBe(keyObj);
|
||||
expect(reactiveValObj).not.toBe(valObj);
|
||||
expect(toRaw(reactiveKeyObj as any)).toBe(keyObj);
|
||||
expect(toRaw(reactiveValObj as any)).toBe(valObj);
|
||||
reactiveKeyObj.a = 0;
|
||||
reactiveValObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveKeyObj.a; // observe key "a" in key sub-reactive;
|
||||
reactiveKeyObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveValObj.a; // observe key "a" in val sub-reactive;
|
||||
reactiveValObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
reactiveKeyObj.a = 1; // setting same value again shouldn't notify
|
||||
reactiveValObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WeakMap", () => {
|
||||
test("can make reactive WeakMap", () => {
|
||||
const map = new WeakMap();
|
||||
const obj = reactive(map);
|
||||
expect(obj).not.toBe(map);
|
||||
});
|
||||
|
||||
test("can read", async () => {
|
||||
const obj = {};
|
||||
const obj2 = {};
|
||||
const state = reactive(new WeakMap([[obj, 0]]));
|
||||
expect(state.has(obj)).toBe(true);
|
||||
expect(state.has(obj2)).toBe(false);
|
||||
expect(state.get(obj)).toBe(0);
|
||||
expect(state.get(obj2)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("can add entries", () => {
|
||||
const obj = {};
|
||||
const state = reactive(new WeakMap());
|
||||
state.set(obj, 2);
|
||||
expect(state.has(obj)).toBe(true);
|
||||
expect(state.get(obj)).toBe(2);
|
||||
});
|
||||
|
||||
test("can remove entries", () => {
|
||||
const obj = {};
|
||||
const state = reactive(new WeakMap([[obj, 2]]));
|
||||
state.delete(obj);
|
||||
expect(state.has(obj)).toBe(false);
|
||||
expect(state.get(obj)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("act like a WeakMap", () => {
|
||||
const obj = {};
|
||||
const state = reactive(new WeakMap([[obj, 2]]));
|
||||
expect(typeof state).toBe("object");
|
||||
expect(state).toBeInstanceOf(WeakMap);
|
||||
});
|
||||
|
||||
test("checking for a key with 'has' subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const obj = {};
|
||||
const obj2 = {};
|
||||
const obj3 = {};
|
||||
const state = reactive(new WeakMap([[obj2, 2]]), observer);
|
||||
|
||||
expect(state.has(obj)).toBe(false); // subscribe to obj
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.set(obj, 3);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.has(obj)).toBe(true); // subscribe to obj
|
||||
state.delete(obj);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.set(obj, 3);
|
||||
state.delete(obj);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
expect(state.has(obj)).toBe(false); // subscribe to obj
|
||||
|
||||
state.set(obj3, 4); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("checking for a key with 'get' subscribes the callback to changes to that key", () => {
|
||||
const observer = jest.fn();
|
||||
const obj = {};
|
||||
const obj2 = {};
|
||||
const obj3 = {};
|
||||
const state = reactive(new WeakMap([[obj2, 2]]), observer);
|
||||
|
||||
expect(state.get(obj)).toBeUndefined(); // subscribe to obj
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
state.set(obj, 3);
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
expect(state.get(obj)).toBe(3); // subscribe to obj
|
||||
state.delete(obj);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
state.set(obj, 3);
|
||||
state.delete(obj);
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
expect(state.get(obj)).toBeUndefined(); // subscribe to obj
|
||||
|
||||
state.set(obj3, 4); // setting unobserved key doesn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("getting values returns a reactive", async () => {
|
||||
const keyObj = {};
|
||||
const valObj = { a: 2 };
|
||||
const observer = jest.fn();
|
||||
const state = reactive(new WeakMap([[keyObj, valObj]]), observer);
|
||||
const reactiveObj = state.get(keyObj)!;
|
||||
expect(reactiveObj).not.toBe(valObj);
|
||||
expect(toRaw(reactiveObj as any)).toBe(valObj);
|
||||
reactiveObj.a = 0;
|
||||
expect(observer).toHaveBeenCalledTimes(0);
|
||||
reactiveObj.a; // observe key "a" in sub-reactive;
|
||||
reactiveObj.a = 1;
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
reactiveObj.a = 1; // setting same value again shouldn't notify
|
||||
expect(observer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user