Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions packages/reactivity/__tests__/reactive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,26 @@ describe('reactivity/reactive', () => {
expect(() => markRaw(obj)).not.toThrowError()
})

test('markRaw preserves opt-out on non-extensible targets', () => {
const sealed = Object.seal({ a: 1 })
const marked = markRaw(sealed)
expect(marked).toBe(sealed)
// SKIP flag cannot be added onto a sealed target, so reactive() learns
// about the opt-out via the side rawSet instead.
expect(reactive(marked)).toBe(sealed)
expect(isReactive(reactive({ inner: markRaw(sealed) }).inner)).toBe(false)
})

test('deep traverse should skip markRaw non-extensible targets', async () => {
const { traverse } = await import('../src/watch')
const sealed = Object.seal({ inner: { a: 1 } })
const marked = markRaw(sealed)
const seen = new Map()
traverse(marked, Infinity, seen)
// SKIP-flag markRaw stops here; the rawSet fallback must do the same.
expect(seen.has(marked.inner)).toBe(false)
})

test('markRaw should not redefine on an marked object', () => {
const obj = markRaw({ foo: 1 })
const raw = markRaw(obj)
Expand All @@ -321,16 +341,34 @@ describe('reactivity/reactive', () => {
expect(b.a === a).toBe(false)
})

test('should not observe non-extensible objects', () => {
test('should not observe frozen objects', () => {
const obj = reactive({
foo: Object.preventExtensions({ a: 1 }),
// sealed or frozen objects are considered non-extensible as well
bar: Object.freeze({ a: 1 }),
baz: Object.seal({ a: 1 }),
foo: Object.freeze({ a: 1 }),
})
expect(isReactive(obj.foo)).toBe(false)
expect(isReactive(obj.bar)).toBe(false)
expect(isReactive(obj.baz)).toBe(false)
})

test('should observe sealed and non-extensible objects', () => {
// sealed and Object.preventExtensions targets still allow mutating their
// existing keys, so reactive() should proxy them. See issue #14893.
const sealed = reactive(Object.seal({ a: 1 }))
const nonExtensible = reactive(Object.preventExtensions({ a: 1 }))
expect(isReactive(sealed)).toBe(true)
expect(isReactive(nonExtensible)).toBe(true)

let dummy
effect(() => {
dummy = sealed.a + nonExtensible.a
})
expect(dummy).toBe(2)
sealed.a = 10
nonExtensible.a = 20
expect(dummy).toBe(30)

// siblings share the same createReactiveObject guard.
expect(isReactive(shallowReactive(Object.seal({ a: 1 })))).toBe(true)
expect(isReadonly(readonly(Object.seal({ a: 1 })))).toBe(true)
expect(isReadonly(shallowReadonly(Object.seal({ a: 1 })))).toBe(true)
})

test('should not observe objects with __v_skip', () => {
Expand Down
27 changes: 23 additions & 4 deletions packages/reactivity/src/reactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ export interface Target {
[ReactiveFlags.RAW]?: any
}

// non-extensible targets opted out via markRaw (SKIP flag can't be defined on them)
const rawSet: WeakSet<object> = new WeakSet<object>()
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Internal: true if the value has opted out of reactivity via markRaw,
* either through the SKIP flag or through the rawSet fallback.
*/
export function isRawMarked(value: unknown): boolean {
return (
!!value &&
(!!(value as Target)[ReactiveFlags.SKIP] || rawSet.has(value as object))
)
}

export const reactiveMap: WeakMap<Target, any> = new WeakMap<Target, any>()
export const shallowReactiveMap: WeakMap<Target, any> = new WeakMap<
Target,
Expand Down Expand Up @@ -284,8 +298,8 @@ function createReactiveObject(
) {
return target
}
// only specific value types can be observed.
if (target[ReactiveFlags.SKIP] || !Object.isExtensible(target)) {
// skip frozen targets and anything opted out via markRaw.
if (isRawMarked(target) || Object.isFrozen(target)) {
return target
}
// target already has corresponding Proxy
Expand Down Expand Up @@ -418,8 +432,13 @@ export type Raw<T> = T & { [RawSymbol]?: true }
* @see {@link https://vuejs.org/api/reactivity-advanced.html#markraw}
*/
export function markRaw<T extends object>(value: T): Raw<T> {
if (!hasOwn(value, ReactiveFlags.SKIP) && Object.isExtensible(value)) {
def(value, ReactiveFlags.SKIP, true)
if (!hasOwn(value, ReactiveFlags.SKIP)) {
if (Object.isExtensible(value)) {
def(value, ReactiveFlags.SKIP, true)
} else {
// SKIP can't be defined on a non-extensible target; track it via rawSet.
rawSet.add(value)
}
}
return value
}
Expand Down
4 changes: 2 additions & 2 deletions packages/reactivity/src/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@vue/shared'
import { warn } from './warning'
import type { ComputedRef } from './computed'
import { ReactiveFlags } from './constants'
import { isRawMarked } from './reactive'
import {
type DebuggerOptions,
EffectFlags,
Expand Down Expand Up @@ -333,7 +333,7 @@ export function traverse(
depth: number = Infinity,
seen?: Map<unknown, number>,
): unknown {
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
if (depth <= 0 || !isObject(value) || isRawMarked(value)) {
return value
}

Expand Down