Skip to content

looseEqual() reports Map and Set instances with different contents as equal #15320

Description

@haoku123

Vue version

3.5.x / main (verified at a2b40db)

Link to minimal reproduction

Reproducible directly against the repo (see below) — looseEqual is not exported publicly, so the SFC Playground can only show the v-model symptom, not the root cause.

Steps to reproduce

looseEqual() in packages/shared/src/looseEqual.ts reports unrelated Map and Set instances as equal:

import { looseEqual } from '@vue/shared'

looseEqual(new Map([['a', 1]]), new Map([['b', 2]])) // true, expected false
looseEqual(new Set([1]), new Set([2]))               // true, expected false
looseEqual(new Map([['a', 1]]), new Map())           // true, expected false

This surfaces to users through v-model on <select>. The following selects the wrong option:

const mapA = new Map([['id', 1]])
const mapB = new Map([['id', 2]])
const selected = ref(mapB)
<select v-model="selected">
  <option :value="mapA">A</option>
  <option :value="mapB">B</option>
</select>

el.selectedIndex is 0 (option A) when it should be 1, because looseEqual(mapA, mapB) returns true and the loop in setSelected stops at the first option.

What is expected?

Map/Set values with different contents compare as not equal, so v-model selects the option that actually matches.

What is actually happening?

Map and Set fall through to the generic object branch. Object.keys() on either is always [], so the key-count check passes and the for...in loop body never executes — entries are never compared. Execution then reaches the final line:

return String(a) === String(b)

Every Map stringifies to '[object Map]' and every Set to '[object Set]', so any two of the same type are reported equal.

Note this differs from Date and Symbol, which are given explicit branches above, and from arrays and plain objects, which compare correctly:

looseEqual([1], [2])       // false, correct
looseEqual({ a: 1 }, { a: 2 }) // false, correct

packages/shared/__tests__/looseEqual.spec.ts has 13 cases but none for Map/Set, which is why this hasn't been caught.

Possible fix

Add explicit branches before the generic object check, mirroring the existing Date/Symbol pattern. isMap and isSet already exist in packages/shared/src/general.ts. Set needs content comparison rather than reference lookup to stay consistent with looseEqual's semantics (e.g. new Set([{ a: 1 }]) vs new Set([{ a: 1 }])).

Worth confirming the intended semantics first: should Map comparison be key-order-insensitive, and should Set compare loosely (looseEqual per element) or strictly? Happy to open a PR with tests once the direction is confirmed.

System Info

Vue: 3.5.x / main @ a2b40db

Any additional comments?

Unrelated to #15298, which touches only vModel.ts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    🔨 p3-minor-bugPriority 3: this fixes a bug, but is an edge case that only affects very specific usage.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions