Backbone.js ↔ Underscore.js: Deep Comparison Compatibility in Model#set
0 reputation · 24 Apr 2024, 23:10 UTC
0 reputation · 24 Apr 2024, 23:10 UTC
Backbone’s Model#set relies on Underscore’s _.isEqual to detect attribute changes. The 1.9 release of Underscore introduced stricter deep‑comparison semantics, causing Backbone to flag a change when only a nested property is modified, even if the top‑level reference remains the same. This can trigger spurious change events or, conversely, hide necessary updates in collections.
Because Backbone 1.4+ performs no explicit version check on Underscore, projects that upgrade Underscore without reviewing release notes may experience silent regressions. The community has not agreed on whether Backbone should embed its own comparison logic or continue to depend on an evolving external library.
What are the trade‑offs of each approach? Should Backbone provide a fallback for legacy _.isEqual behavior, or is overriding the comparator in Model#set the safest path? How can developers test for unintended change events when upgrading Underscore in an existing Backbone codebase?
32275 reputation · 25 Apr 2024, 01:39 UTC
Because Backbone 1.4+ delegates its change detection inside Model#set directly to _.isEqual with no version check, the safest path for an existing codebase is not to wait for Backbone to embed a legacy comparator, but to control the comparison yourself — either by pinning Underscore, or by overriding set (or wrapping the equality check) in a base model your application owns. A fallback inside Backbone would be nice, but you can't ship your fix on the Backbone project's timeline.
Confirmed: Backbone's set compares each incoming attribute against the current one using _.isEqual, and fires change:attr/change only when the comparison reports inequality. Backbone performs no Underscore version check, so any semantic change in _.isEqual flows straight into your event stream.
Likely explanation for your regression: Underscore 1.9 tightened its deep-equality algorithm. Nested mutations that older versions treated as equal (or that were previously masked by reference shortcuts) are now reported as unequal, so Backbone sees a "change" where it previously saw none — producing spurious events, or in collection-sort and reset scenarios, the opposite surprise. Treat the precise per-version semantics as something to verify against the exact Underscore versions you run, not as gospel.
_.isEqual semantics lose them. Unlikely to be resolved quickly, and not something you control.Extend a base model rather than monkey-patching Backbone.Model.prototype, so third-party components keep stock behavior:
var BaseModel = Backbone.Model.extend({
set: function(key, val, options) {
var attrs = typeof key === 'object' ? key : (key = {}, key[key] = val, key);
// Snapshot current values and restore reference-equal nested objects
// only if your app truly depends on reference-only change detection.
return Backbone.Model.prototype.set.call(this, attrs, options);
}
});
If you genuinely need the old semantics, the cleanest variant is to pre-normalize attributes (e.g., deep-freeze or replace mutated nested objects with new references before calling set) so that any reasonable _.isEqual behaves predictably. Avoid {silent: true} as a fix — it only suppresses events for that one call while still marking attributes changed, which corrupts later hasChanged()/changedAttributes() results.
change, mutate a nested property and call set with the same top-level reference, then assert the event count matches your intended semantics.change events in a test harness, run your existing suite against the old Underscore, then against 1.9+, and diff the counts. Spikes localize the affected models.isEqual in a test build to confirm the comparator — not your code — is the behavioral difference.Pin Underscore short-term to stop the bleeding, then move to application-owned normalization of nested attributes (fresh references on real changes) so any _.isEqual version behaves correctly. Reserve a custom set override for models where that's impractical.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.