DEV: Introduce @dedupeTracked (#27084)

Same as `@tracked`, but skips notifying consumers if the value is unchanged. This introduces some performance overhead, so should only be used where excessive downstream re-evaluations are a problem.

This is loosely based on `@dedupeTracked` in the `tracked-toolbox` package, but without the added complexity of a customizable 'comparator'. Implementing ourselves also avoids the need for pulling in the entire package, which contains some tools which we don't want, or which are now implemented in Ember/Glimmer (e.g. `@cached`).
This commit is contained in:
David Taylor 2024-05-20 15:59:30 +01:00 committed by GitHub
parent 32aaf2e8d3
commit 23b02a3824
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 94 additions and 0 deletions

View File

@ -74,3 +74,49 @@ export function resettableTracked(prototype, key, descriptor) {
},
};
}
/**
* @decorator
*
* Same as `@tracked`, but skips notifying about updates if the value is unchanged. This introduces some
* performance overhead, so should only be used where excessive downstream re-evaluations are a problem.
*
* @example
*
* ```js
* class UserRenameForm {
* @dedupeTracked fullName;
* }
*
* const form = new UserRenameForm();
* form.fullName = "Alice"; // Downstream consumers will be notified
* form.fullName = "Alice"; // Downstream consumers will not be re-notified
* form.fullName = "Bob"; // Downstream consumers will be notified
* ```
*
*/
export function dedupeTracked(target, key, desc) {
let { initializer } = desc;
let { get, set } = tracked(target, key, desc);
let values = new WeakMap();
return {
get() {
if (!values.has(this)) {
let value = initializer?.call(this);
values.set(this, value);
set.call(this, value);
}
return get.call(this);
},
set(value) {
if (!values.has(this) || values.get(this) !== value) {
values.set(this, value);
set.call(this, value);
}
},
};
}

View File

@ -0,0 +1,48 @@
import { cached } from "@glimmer/tracking";
import { module, test } from "qunit";
import { dedupeTracked } from "discourse/lib/tracked-tools";
module("Unit | tracked-tools", function () {
test("@dedupeTracked", async function (assert) {
class Pet {
initialsEvaluatedCount = 0;
@dedupeTracked name;
@cached
get initials() {
this.initialsEvaluatedCount++;
return this.name
?.split(" ")
.map((n) => n[0])
.join("");
}
}
const pet = new Pet();
pet.name = "Scooby Doo";
assert.strictEqual(pet.initials, "SD", "Initials are correct");
assert.strictEqual(
pet.initialsEvaluatedCount,
1,
"Initials getter evaluated once"
);
pet.name = "Scooby Doo";
assert.strictEqual(pet.initials, "SD", "Initials are correct");
assert.strictEqual(
pet.initialsEvaluatedCount,
1,
"Initials getter not re-evaluated"
);
pet.name = "Fluffy";
assert.strictEqual(pet.initials, "F", "Initials are correct");
assert.strictEqual(
pet.initialsEvaluatedCount,
2,
"Initials getter re-evaluated"
);
});
});