mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] reactivity: introduces markRaw and toRaw functions
This commit is contained in:
committed by
Aaron Bohy
parent
d88eb34d4f
commit
0a73154985
+6
-1
@@ -7,9 +7,14 @@ Main entities:
|
|||||||
- [`App`](reference/app.md): represent an Owl application (mainly a root component,a set of templates, and a config)
|
- [`App`](reference/app.md): represent an Owl application (mainly a root component,a set of templates, and a config)
|
||||||
- [`Component`](reference/component.md): the main class to define a concrete Owl component
|
- [`Component`](reference/component.md): the main class to define a concrete Owl component
|
||||||
- [`mount`](reference/app.md#mount-helper): main entry point for most application: mount a component to a target
|
- [`mount`](reference/app.md#mount-helper): main entry point for most application: mount a component to a target
|
||||||
|
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
|
||||||
|
|
||||||
|
Reactivity
|
||||||
|
|
||||||
- [`useState`](reference/reactivity.md#usestate): create a reactive object (hook, linked to a specific component)
|
- [`useState`](reference/reactivity.md#usestate): create a reactive object (hook, linked to a specific component)
|
||||||
- [`reactive`](reference/reactivity.md#reactive): create a reactive object (not linked to any component)
|
- [`reactive`](reference/reactivity.md#reactive): create a reactive object (not linked to any component)
|
||||||
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
|
- [`markRaw`](reference/reactivity.md#markraw): mark an object or array so that it is ignored by the reactivity system
|
||||||
|
- [`toRaw`](reference/reactivity.md#toraw): given a reactive objet, return the raw (non reactive) underlying object
|
||||||
|
|
||||||
Lifecycle hooks:
|
Lifecycle hooks:
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [`useState`](#usestate)
|
- [`useState`](#usestate)
|
||||||
- [`reactive`](#reactive)
|
- [`reactive`](#reactive)
|
||||||
|
- [`markRaw`](#markraw)
|
||||||
|
- [`toRaw`](#toraw)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -78,3 +80,40 @@ obj2.b = 3; // log 'observer1' and 'observer2'
|
|||||||
|
|
||||||
Obviously, one can use `reactive` on the result of a `useState` if wanted, this
|
Obviously, one can use `reactive` on the result of a `useState` if wanted, this
|
||||||
is the proper way to watch for some state changes.
|
is the proper way to watch for some state changes.
|
||||||
|
|
||||||
|
## `markRaw`
|
||||||
|
|
||||||
|
Marks an object so that it is ignored by the reactivity system. This function returns its argument.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const someObject = markRaw(...);
|
||||||
|
const state = useState({
|
||||||
|
a: 1,
|
||||||
|
obj: someObject
|
||||||
|
});
|
||||||
|
// here, state.obj === someObject
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful in some rare cases. For example, some complex and large object such
|
||||||
|
that going through the reactivity system may cause a non trivial performance slowdown.
|
||||||
|
|
||||||
|
However, use this function with caution: this is an escape hatch from the reactivity
|
||||||
|
system, and as such, using it may cause subtle and unintended issues!
|
||||||
|
|
||||||
|
## `toRaw`
|
||||||
|
|
||||||
|
Given a reactive object, this function returns the underlying, non-reactive,
|
||||||
|
corresponding object.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// in setup
|
||||||
|
const state = useState({ value: 1 });
|
||||||
|
|
||||||
|
// later:
|
||||||
|
const rawState = toRaw(this.state);
|
||||||
|
rawState.value = 3; // will NOT be picked up by the reactivity system!!!
|
||||||
|
```
|
||||||
|
|
||||||
|
Here again, this is useful in some situations where we want to explicitely bypass
|
||||||
|
Owl, but using this function means that the responsability of coordinating
|
||||||
|
state update is given to the user code, instead of Owl. Subtle bugs may arise!
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ export { useComponent } from "./component/component_node";
|
|||||||
export { status } from "./component/status";
|
export { status } from "./component/status";
|
||||||
export { Memo } from "./memo";
|
export { Memo } from "./memo";
|
||||||
export { xml } from "./app/template_set";
|
export { xml } from "./app/template_set";
|
||||||
export { useState, reactive } from "./reactivity";
|
export { useState, reactive, markRaw, toRaw } from "./reactivity";
|
||||||
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
|
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
|
||||||
export { EventBus, whenReady, loadFile, markup } from "./utils";
|
export { EventBus, whenReady, loadFile, markup } from "./utils";
|
||||||
export {
|
export {
|
||||||
|
|||||||
+37
-2
@@ -4,15 +4,23 @@ import { batched, Callback } from "./utils";
|
|||||||
|
|
||||||
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
|
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
|
||||||
const TARGET = Symbol("Target");
|
const TARGET = Symbol("Target");
|
||||||
|
// Escape hatch to prevent reactivity system to turn something into a reactive
|
||||||
|
const SKIP = Symbol("Skip");
|
||||||
// Special key to subscribe to, to be notified of key creation/deletion
|
// Special key to subscribe to, to be notified of key creation/deletion
|
||||||
const KEYCHANGES = Symbol("Key changes");
|
const KEYCHANGES = Symbol("Key changes");
|
||||||
|
|
||||||
type ObjectKey = string | number | symbol;
|
type ObjectKey = string | number | symbol;
|
||||||
|
|
||||||
type Target = object;
|
type Target = object;
|
||||||
|
|
||||||
export type Reactive<T extends Target = Target> = T & {
|
export type Reactive<T extends Target = Target> = T & {
|
||||||
[TARGET]: any;
|
[TARGET]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type NonReactive<T extends Target = Target> = T & {
|
||||||
|
[SKIP]: any;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a given value can be made into a reactive object.
|
* Checks whether a given value can be made into a reactive object.
|
||||||
*
|
*
|
||||||
@@ -30,6 +38,27 @@ function canBeMadeReactive(value: any): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark an object or array so that it is ignored by the reactivity system
|
||||||
|
*
|
||||||
|
* @param value the value to mark
|
||||||
|
* @returns the object itself
|
||||||
|
*/
|
||||||
|
export function markRaw<T extends Target>(value: T): NonReactive<T> {
|
||||||
|
(value as any)[SKIP] = true;
|
||||||
|
return value as NonReactive<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a reactive objet, return the raw (non reactive) underlying object
|
||||||
|
*
|
||||||
|
* @param value a reactive value
|
||||||
|
* @returns the underlying value
|
||||||
|
*/
|
||||||
|
export function toRaw<T extends object>(value: Reactive<T>): T {
|
||||||
|
return value[TARGET];
|
||||||
|
}
|
||||||
|
|
||||||
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
|
const targetToKeysToCallbacks = new WeakMap<Target, Map<ObjectKey, Set<Callback>>>();
|
||||||
/**
|
/**
|
||||||
* Observes a given key on a target with an callback. The callback will be
|
* Observes a given key on a target with an callback. The callback will be
|
||||||
@@ -130,10 +159,16 @@ const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive>>();
|
|||||||
* reactive has changed
|
* reactive has changed
|
||||||
* @returns a proxy that tracks changes to it
|
* @returns a proxy that tracks changes to it
|
||||||
*/
|
*/
|
||||||
export function reactive<T extends Target>(target: T, callback: Callback = () => {}): Reactive<T> {
|
export function reactive<T extends Target>(
|
||||||
|
target: T,
|
||||||
|
callback: Callback = () => {}
|
||||||
|
): Reactive<T> | NonReactive<T> {
|
||||||
if (!canBeMadeReactive(target)) {
|
if (!canBeMadeReactive(target)) {
|
||||||
throw new Error(`Cannot make the given value reactive`);
|
throw new Error(`Cannot make the given value reactive`);
|
||||||
}
|
}
|
||||||
|
if (SKIP in target) {
|
||||||
|
return target as NonReactive<T>;
|
||||||
|
}
|
||||||
const originalTarget = (target as Reactive)[TARGET];
|
const originalTarget = (target as Reactive)[TARGET];
|
||||||
if (originalTarget) {
|
if (originalTarget) {
|
||||||
return reactive(originalTarget, callback);
|
return reactive(originalTarget, callback);
|
||||||
@@ -202,7 +237,7 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
|||||||
* relevant changes
|
* relevant changes
|
||||||
* @see reactive
|
* @see reactive
|
||||||
*/
|
*/
|
||||||
export function useState<T extends object>(state: T): Reactive<T> {
|
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent()!;
|
||||||
if (!batchedRenderFunctions.has(node)) {
|
if (!batchedRenderFunctions.has(node)) {
|
||||||
batchedRenderFunctions.set(
|
batchedRenderFunctions.set(
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import {
|
|||||||
onWillUpdateProps,
|
onWillUpdateProps,
|
||||||
useState,
|
useState,
|
||||||
xml,
|
xml,
|
||||||
|
markRaw,
|
||||||
|
toRaw,
|
||||||
} from "../src";
|
} from "../src";
|
||||||
import { reactive } from "../src/reactivity";
|
import { reactive, Reactive } from "../src/reactivity";
|
||||||
import { batched } from "../src/utils";
|
import { batched } from "../src/utils";
|
||||||
import {
|
import {
|
||||||
makeDeferred,
|
makeDeferred,
|
||||||
@@ -1093,6 +1095,31 @@ describe("Reactivity", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("markRaw", () => {
|
||||||
|
test("markRaw works as expected: value is not observed", () => {
|
||||||
|
const obj1: any = markRaw({ value: 1 });
|
||||||
|
const obj2 = { value: 1 };
|
||||||
|
let n = 0;
|
||||||
|
const r = reactive({ obj1, obj2 }, () => n++);
|
||||||
|
expect(n).toBe(0);
|
||||||
|
r.obj1.value = r.obj1.value + 1;
|
||||||
|
expect(n).toBe(0);
|
||||||
|
r.obj2.value = r.obj2.value + 1;
|
||||||
|
expect(n).toBe(1);
|
||||||
|
expect(r.obj1).toBe(obj1);
|
||||||
|
expect(r.obj2).not.toBe(obj2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toRaw", () => {
|
||||||
|
test("toRaw works as expected", () => {
|
||||||
|
const obj = { value: 1 };
|
||||||
|
const reactiveObj = reactive(obj);
|
||||||
|
expect(reactiveObj).not.toBe(obj);
|
||||||
|
expect(toRaw(reactiveObj as Reactive<typeof obj>)).toBe(obj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Reactivity: useState", () => {
|
describe("Reactivity: useState", () => {
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user