Skip to content

feat(reactivity): untrack and peek for ref #13286

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: minor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1faca59
chore(deps): update all non-major dependencies (#13166)
renovate[bot] Apr 8, 2025
347c784
chore: add bsky link (#13175)
bornkiss Apr 8, 2025
4f6ef92
chore(deps): update dependency vite to v5.4.17 [security] (#13173)
renovate[bot] Apr 9, 2025
32bc647
chore(deps): update build (#13165)
renovate[bot] Apr 10, 2025
8ae1122
fix(compiler-sfc): treat the return value of `useTemplateRef` as a de…
KazariEX Apr 14, 2025
9d84d64
chore(deps): update dependency @types/node to ^22.14.1 (#13196)
renovate[bot] Apr 14, 2025
c15ed52
chore(deps): update dependency vite to v5.4.18 [security] (#13198)
renovate[bot] Apr 15, 2025
4f79253
chore(deps): update build (#13195)
renovate[bot] Apr 15, 2025
4085ed9
chore(deps): update pnpm to v10.9.0 (#13224)
renovate[bot] Apr 22, 2025
b782cd6
chore(deps): update dependency vite to ^6.3.2 (#13225)
renovate[bot] Apr 22, 2025
b92ae84
chore: update CHANGELOG.md (#13230)
G1xiang Apr 22, 2025
c3e3396
chore(deps): update dependency vite to v5.4.18 [security] (#13229)
renovate[bot] Apr 23, 2025
a23fb59
chore(deps): update dependency vite to v5.4.18 [security] (#13235)
renovate[bot] Apr 24, 2025
d9923c3
chore(deps): update dependency vite to v5.4.18 [security] (#13237)
renovate[bot] Apr 29, 2025
bfc458f
chore(deps): update build (#13249)
renovate[bot] Apr 29, 2025
e4d9e7e
chore(deps): update lint (#13250)
renovate[bot] Apr 29, 2025
b3ecee3
fix(runtime-core): update __vnode of static nodes when patching alon…
makedopamine May 1, 2025
5d166f3
fix(compiler-core): remove slot cache from parent renderCache during …
edison1105 May 1, 2025
016c472
fix(runtime-core): stop tracking deps in setRef during unmount (#13210)
makedopamine May 1, 2025
8b848cb
fix(TransitionGroup): reset prevChildren to prevent memory leak (#13183)
edison1105 May 1, 2025
0b23fd2
fix(reactivity): should not recompute if computed does not track reac…
edison1105 May 1, 2025
5e37dd0
fix(hmr/teleport): adjust static children traversal for HMR in dev mo…
edison1105 May 2, 2025
2206cd2
fix(ssr): properly init slots during ssr rendering (#12441)
edison1105 May 2, 2025
9196222
fix(slots): properly warn if slot invoked in setup (#12195)
yangxiuxiu1115 May 2, 2025
3f27c58
fix(runtime-core): respect immutability for readonly reactive arrays …
jh-leong May 2, 2025
56be3dd
chore(deps): update compiler to ^7.27.1 (#13277)
renovate[bot] May 5, 2025
cc326a7
feat: ref peek
teleskop150750 May 6, 2025
d089f23
feat: untrack
teleskop150750 May 6, 2025
acf02a1
feat: add untrack export
teleskop150750 May 6, 2025
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* **custom-element:** avoid triggering mutationObserver when relecting props ([352bc88](https://github.com/vuejs/core/commit/352bc88c1bd2fda09c61ab17ea1a5967ffcd7bc0)), closes [#12214](https://github.com/vuejs/core/issues/12214) [#12215](https://github.com/vuejs/core/issues/12215)
* **deps:** update dependency postcss to ^8.4.48 ([#12356](https://github.com/vuejs/core/issues/12356)) ([b5ff930](https://github.com/vuejs/core/commit/b5ff930089985a58c3553977ef999cec2a6708a4))
* **hydration:** the component vnode's el should be updated when a mismatch occurs. ([#12255](https://github.com/vuejs/core/issues/12255)) ([a20a4cb](https://github.com/vuejs/core/commit/a20a4cb36a3e717d1f8f259d0d59f133f508ff0a)), closes [#12253](https://github.com/vuejs/core/issues/12253)
* **reactiivty:** avoid unnecessary watcher effect removal from inactive scope ([2193284](https://github.com/vuejs/core/commit/21932840eae72ffcd357a62ec596aaecc7ec224a)), closes [#5783](https://github.com/vuejs/core/issues/5783) [#5806](https://github.com/vuejs/core/issues/5806)
* **reactivty:** avoid unnecessary watcher effect removal from inactive scope ([2193284](https://github.com/vuejs/core/commit/21932840eae72ffcd357a62ec596aaecc7ec224a)), closes [#5783](https://github.com/vuejs/core/issues/5783) [#5806](https://github.com/vuejs/core/issues/5806)
* **reactivity:** release nested effects/scopes on effect scope stop ([#12373](https://github.com/vuejs/core/issues/12373)) ([bee2f5e](https://github.com/vuejs/core/commit/bee2f5ee62dc0cd04123b737779550726374dd0a)), closes [#12370](https://github.com/vuejs/core/issues/12370)
* **runtime-dom:** set css vars before user onMounted hooks ([2d5c5e2](https://github.com/vuejs/core/commit/2d5c5e25e9b7a56e883674fb434135ac514429b5)), closes [#11533](https://github.com/vuejs/core/issues/11533)
* **runtime-dom:** set css vars on update to handle child forcing reflow in onMount ([#11561](https://github.com/vuejs/core/issues/11561)) ([c4312f9](https://github.com/vuejs/core/commit/c4312f9c715c131a09e552ba46e9beb4b36d55e6))
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Please make sure to respect issue requirements and use [the new issue helper](ht
## Stay In Touch

- [X](https://x.com/vuejs)
- [Bluesky](https://bsky.app/profile/vuejs.org)
- [Blog](https://blog.vuejs.org/)
- [Job Board](https://vuejobs.com/?ref=vuejs)

Expand Down
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"version": "3.5.13",
"packageManager": "pnpm@10.7.0",
"packageManager": "pnpm@10.9.0",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
Expand Down Expand Up @@ -69,23 +69,23 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.11.13",
"@swc/core": "^1.11.21",
"@types/hash-sum": "^1.0.2",
"@types/node": "^22.13.14",
"@types/node": "^22.14.1",
"@types/semver": "^7.7.0",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^3.0.9",
"@vitest/eslint-plugin": "^1.1.38",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
"esbuild": "^0.25.2",
"esbuild": "^0.25.3",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.23.0",
"eslint-plugin-import-x": "^4.9.4",
"eslint": "^9.25.1",
"eslint-plugin-import-x": "^4.11.0",
"estree-walker": "catalog:",
"jsdom": "^26.0.0",
"lint-staged": "^15.5.0",
"lint-staged": "^15.5.1",
"lodash": "^4.17.21",
"magic-string": "^0.30.17",
"markdown-table": "^3.0.4",
Expand All @@ -97,18 +97,18 @@
"pug": "^3.0.3",
"puppeteer": "~24.4.0",
"rimraf": "^6.0.1",
"rollup": "^4.38.0",
"rollup": "^4.40.1",
"rollup-plugin-dts": "^6.2.1",
"rollup-plugin-esbuild": "^6.2.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.7.1",
"serve": "^14.2.4",
"serve-handler": "^6.1.6",
"simple-git-hooks": "^2.12.1",
"simple-git-hooks": "^2.13.0",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.8.1",
"typescript": "~5.6.2",
"typescript-eslint": "^8.28.0",
"typescript-eslint": "^8.31.1",
"vite": "catalog:",
"vitest": "^3.0.9"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"vite": "^6.2.3"
"vite": "^6.3.3"
}
}
10 changes: 10 additions & 0 deletions packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ describe('compiler: cacheStatic transform', () => {
{
/* _ slot flag */
},
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
],
})
})
Expand Down Expand Up @@ -197,6 +202,11 @@ describe('compiler: cacheStatic transform', () => {
{
/* _ slot flag */
},
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
],
})
})
Expand Down
27 changes: 27 additions & 0 deletions packages/compiler-core/src/transforms/cacheStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import {
type RootNode,
type SimpleExpressionNode,
type SlotFunctionExpression,
type SlotsObjectProperty,
type TemplateChildNode,
type TemplateNode,
type TextCallNode,
type VNodeCall,
createArrayExpression,
createObjectProperty,
createSimpleExpression,
getVNodeBlockHelper,
getVNodeHelper,
} from '../ast'
Expand Down Expand Up @@ -140,6 +143,7 @@ function walk(
}

let cachedAsArray = false
const slotCacheKeys = []
if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
if (
node.tagType === ElementTypes.ELEMENT &&
Expand All @@ -163,6 +167,7 @@ function walk(
// default slot
const slot = getSlotNode(node.codegenNode, 'default')
if (slot) {
slotCacheKeys.push(context.cached.length)
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
Expand All @@ -186,6 +191,7 @@ function walk(
slotName.arg &&
getSlotNode(parent.codegenNode, slotName.arg)
if (slot) {
slotCacheKeys.push(context.cached.length)
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
Expand All @@ -196,10 +202,31 @@ function walk(

if (!cachedAsArray) {
for (const child of toCache) {
slotCacheKeys.push(context.cached.length)
child.codegenNode = context.cache(child.codegenNode!)
}
}

// put the slot cached keys on the slot object, so that the cache
// can be removed when component unmounting to prevent memory leaks
if (
slotCacheKeys.length &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
node.codegenNode.children &&
!isArray(node.codegenNode.children) &&
node.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
node.codegenNode.children.properties.push(
createObjectProperty(
`__`,
createSimpleExpression(JSON.stringify(slotCacheKeys), false),
) as SlotsObjectProperty,
)
}

function getCacheExpression(value: JSChildNode): CacheExpression {
const exp = context.cache(value)
// #6978, #7138, #7114
Expand Down
1 change: 0 additions & 1 deletion packages/compiler-core/src/transforms/vSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,6 @@ export function buildSlots(
: hasForwardedSlots(node.children)
? SlotFlags.FORWARDED
: SlotFlags.STABLE

let slots = createObjectExpression(
slotsProperties.concat(
createObjectProperty(
Expand Down
36 changes: 20 additions & 16 deletions packages/compiler-sfc/__tests__/compileStyle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,38 +211,42 @@ color: red
expect(
compileScoped(`.div { color: red; } .div:where(:hover) { color: blue; }`),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:where(:hover) { color: blue;
}"`)
".div[data-v-test] { color: red;
}
.div[data-v-test]:where(:hover) { color: blue;
}"
`)

expect(
compileScoped(`.div { color: red; } .div:is(:hover) { color: blue; }`),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:is(:hover) { color: blue;
}"`)
".div[data-v-test] { color: red;
}
.div[data-v-test]:is(:hover) { color: blue;
}"
`)

expect(
compileScoped(
`.div { color: red; } .div:where(.foo:hover) { color: blue; }`,
),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:where(.foo:hover) { color: blue;
}"`)
".div[data-v-test] { color: red;
}
.div[data-v-test]:where(.foo:hover) { color: blue;
}"
`)

expect(
compileScoped(
`.div { color: red; } .div:is(.foo:hover) { color: blue; }`,
),
).toMatchInlineSnapshot(`
".div[data-v-test] { color: red;
}
.div[data-v-test]:is(.foo:hover) { color: blue;
}"`)
".div[data-v-test] { color: red;
}
.div[data-v-test]:is(.foo:hover) { color: blue;
}"
`)
})

test('media query', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-sfc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,6 @@
"postcss-modules": "^6.0.1",
"postcss-selector-parser": "^7.1.0",
"pug": "^3.0.3",
"sass": "^1.86.0"
"sass": "^1.86.3"
}
}
1 change: 1 addition & 0 deletions packages/compiler-sfc/src/compileScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,7 @@ function walkDeclaration(
m === userImportAliases['shallowRef'] ||
m === userImportAliases['customRef'] ||
m === userImportAliases['toRef'] ||
m === userImportAliases['useTemplateRef'] ||
m === DEFINE_MODEL,
)
) {
Expand Down
61 changes: 61 additions & 0 deletions packages/reactivity/__tests__/computed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,17 @@ describe('reactivity/computed', () => {
expect(cValue.value).toBe(1)
})

test('should not recompute if computed does not track reactive data', async () => {
const spy = vi.fn()
const c1 = computed(() => spy())

c1.value
ref(0).value++ // update globalVersion
c1.value

expect(spy).toBeCalledTimes(1)
})

test('computed should remain live after losing all subscribers', () => {
const state = reactive({ a: 1 })
const p = computed(() => state.a + 1)
Expand Down Expand Up @@ -1139,4 +1150,54 @@ describe('reactivity/computed', () => {
const t2 = performance.now()
expect(t2 - t1).toBeLessThan(process.env.CI ? 100 : 30)
})

describe('peek', () => {
it('should return updated value', () => {
const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.peek()).toBe(undefined)
value.foo = 1
expect(cValue.peek()).toBe(1)
})

it('should compute lazily', () => {
const value = reactive<{ foo?: number }>({})
const getter = vi.fn(() => value.foo)
const cValue = computed(getter)

// lazy
expect(getter).not.toHaveBeenCalled()

expect(cValue.peek()).toBe(undefined)
expect(getter).toHaveBeenCalledTimes(1)

// should not compute again
cValue.peek()
expect(getter).toHaveBeenCalledTimes(1)

// should not compute until needed
value.foo = 1
expect(getter).toHaveBeenCalledTimes(1)

// now it should compute
expect(cValue.peek()).toBe(1)
expect(getter).toHaveBeenCalledTimes(2)

// should not compute again
cValue.peek()
expect(getter).toHaveBeenCalledTimes(2)
})

it('should not trigger effect', () => {
const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
let dummy
effect(() => {
dummy = cValue.peek()
})
expect(dummy).toBe(undefined)
value.foo = 1
expect(dummy).toBe(undefined)
})
})
})
12 changes: 12 additions & 0 deletions packages/reactivity/__tests__/effect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
pauseTracking,
resetTracking,
startBatch,
untrack,
} from '../src/effect'

describe('reactivity/effect', () => {
Expand Down Expand Up @@ -1182,6 +1183,17 @@ describe('reactivity/effect', () => {
expect(spy2).toHaveBeenCalledTimes(2)
})

it('should not track dependencies when using untrack', () => {
const value = ref(1)
let dummy
effect(() => {
dummy = untrack(() => value.value)
})
expect(dummy).toBe(1)
value.value = 2
expect(dummy).toBe(1)
})

describe('dep unsubscribe', () => {
function getSubCount(dep: Dep | undefined) {
let count = 0
Expand Down
22 changes: 22 additions & 0 deletions packages/reactivity/__tests__/ref.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,4 +534,26 @@ describe('reactivity/ref', () => {
// @ts-expect-error internal field
expect(objectRefValue._value).toBe(1)
})

describe('peek', () => {
it('should hold a value', () => {
const a = ref(1)
expect(a.peek()).toBe(1)
a.value = 2
expect(a.peek()).toBe(2)
})

it('should not be reactive', () => {
const a = ref(1)
let dummy
const fn = vi.fn(() => {
dummy = a.peek()
})
effect(fn)
expect(fn).toHaveBeenCalledTimes(1)
expect(dummy).toBe(1)
a.value = 2
expect(fn).toHaveBeenCalledTimes(1)
})
})
})
Loading