From 0a73154985087f54ecca1cce0c5a7b7348311bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 21 Jan 2022 13:08:55 +0100 Subject: [PATCH] [IMP] reactivity: introduces markRaw and toRaw functions --- doc/readme.md | 7 ++++++- doc/reference/reactivity.md | 39 +++++++++++++++++++++++++++++++++++++ src/index.ts | 2 +- src/reactivity.ts | 39 +++++++++++++++++++++++++++++++++++-- tests/reactivity.test.ts | 29 ++++++++++++++++++++++++++- 5 files changed, 111 insertions(+), 5 deletions(-) diff --git a/doc/readme.md b/doc/readme.md index 0528c2b7..56e86824 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -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: diff --git a/doc/reference/reactivity.md b/doc/reference/reactivity.md index a99a821b..5e799668 100644 --- a/doc/reference/reactivity.md +++ b/doc/reference/reactivity.md @@ -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! diff --git a/src/index.ts b/src/index.ts index f767bbbe..f4bcd9eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { diff --git a/src/reactivity.ts b/src/reactivity.ts index 737c2219..4bb4aea7 100644 --- a/src/reactivity.ts +++ b/src/reactivity.ts @@ -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 & { [TARGET]: any; }; +type NonReactive = 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(value: T): NonReactive { + (value as any)[SKIP] = true; + return value as NonReactive; +} + +/** + * Given a reactive objet, return the raw (non reactive) underlying object + * + * @param value a reactive value + * @returns the underlying value + */ +export function toRaw(value: Reactive): T { + return value[TARGET]; +} + const targetToKeysToCallbacks = new WeakMap>>(); /** * Observes a given key on a target with an callback. The callback will be @@ -130,10 +159,16 @@ const reactiveCache = new WeakMap>(); * reactive has changed * @returns a proxy that tracks changes to it */ -export function reactive(target: T, callback: Callback = () => {}): Reactive { +export function reactive( + target: T, + callback: Callback = () => {} +): Reactive | NonReactive { if (!canBeMadeReactive(target)) { throw new Error(`Cannot make the given value reactive`); } + if (SKIP in target) { + return target as NonReactive; + } const originalTarget = (target as Reactive)[TARGET]; if (originalTarget) { return reactive(originalTarget, callback); @@ -202,7 +237,7 @@ const batchedRenderFunctions = new WeakMap(); * relevant changes * @see reactive */ -export function useState(state: T): Reactive { +export function useState(state: T): Reactive | NonReactive { const node = getCurrent()!; if (!batchedRenderFunctions.has(node)) { batchedRenderFunctions.set( diff --git a/tests/reactivity.test.ts b/tests/reactivity.test.ts index 4be85ed2..8dd91050 100644 --- a/tests/reactivity.test.ts +++ b/tests/reactivity.test.ts @@ -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)).toBe(obj); + }); +}); + describe("Reactivity: useState", () => { let fixture: HTMLElement;