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)
|
||||
- [`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
|
||||
- [`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)
|
||||
- [`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:
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
- [Overview](#overview)
|
||||
- [`useState`](#usestate)
|
||||
- [`reactive`](#reactive)
|
||||
- [`markRaw`](#markraw)
|
||||
- [`toRaw`](#toraw)
|
||||
|
||||
## 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
|
||||
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 { Memo } from "./memo";
|
||||
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 { EventBus, whenReady, loadFile, markup } from "./utils";
|
||||
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)
|
||||
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
|
||||
const KEYCHANGES = Symbol("Key changes");
|
||||
|
||||
type ObjectKey = string | number | symbol;
|
||||
|
||||
type Target = object;
|
||||
|
||||
export type Reactive<T extends Target = Target> = T & {
|
||||
[TARGET]: any;
|
||||
};
|
||||
|
||||
type NonReactive<T extends Target = Target> = T & {
|
||||
[SKIP]: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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>>>();
|
||||
/**
|
||||
* 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
|
||||
* @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)) {
|
||||
throw new Error(`Cannot make the given value reactive`);
|
||||
}
|
||||
if (SKIP in target) {
|
||||
return target as NonReactive<T>;
|
||||
}
|
||||
const originalTarget = (target as Reactive)[TARGET];
|
||||
if (originalTarget) {
|
||||
return reactive(originalTarget, callback);
|
||||
@@ -202,7 +237,7 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
||||
* relevant changes
|
||||
* @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()!;
|
||||
if (!batchedRenderFunctions.has(node)) {
|
||||
batchedRenderFunctions.set(
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
onWillUpdateProps,
|
||||
useState,
|
||||
xml,
|
||||
markRaw,
|
||||
toRaw,
|
||||
} from "../src";
|
||||
import { reactive } from "../src/reactivity";
|
||||
import { reactive, Reactive } from "../src/reactivity";
|
||||
import { batched } from "../src/utils";
|
||||
import {
|
||||
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", () => {
|
||||
let fixture: HTMLElement;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user