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
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
@@ -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))
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

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",
@@ -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",
@@ -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"
},
Original file line number Diff line number Diff line change
@@ -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
@@ -170,6 +170,11 @@ describe('compiler: cacheStatic transform', () => {
{
/* _ slot flag */
},
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
],
})
})
@@ -197,6 +202,11 @@ describe('compiler: cacheStatic transform', () => {
{
/* _ slot flag */
},
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
],
})
})
27 changes: 27 additions & 0 deletions packages/compiler-core/src/transforms/cacheStatic.ts
Original file line number Diff line number Diff line change
@@ -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'
@@ -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 &&
@@ -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[]),
)
@@ -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[]),
)
@@ -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
1 change: 0 additions & 1 deletion packages/compiler-core/src/transforms/vSlot.ts
Original file line number Diff line number Diff line change
@@ -342,7 +342,6 @@ export function buildSlots(
: hasForwardedSlots(node.children)
? SlotFlags.FORWARDED
: SlotFlags.STABLE

let slots = createObjectExpression(
slotsProperties.concat(
createObjectProperty(
36 changes: 20 additions & 16 deletions packages/compiler-sfc/__tests__/compileStyle.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
2 changes: 1 addition & 1 deletion packages/compiler-sfc/package.json
Original file line number Diff line number Diff line change
@@ -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
@@ -1104,6 +1104,7 @@ function walkDeclaration(
m === userImportAliases['shallowRef'] ||
m === userImportAliases['customRef'] ||
m === userImportAliases['toRef'] ||
m === userImportAliases['useTemplateRef'] ||
m === DEFINE_MODEL,
)
) {
61 changes: 61 additions & 0 deletions packages/reactivity/__tests__/computed.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
@@ -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
@@ -27,6 +27,7 @@ import {
pauseTracking,
resetTracking,
startBatch,
untrack,
} from '../src/effect'

describe('reactivity/effect', () => {
@@ -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
22 changes: 22 additions & 0 deletions packages/reactivity/__tests__/ref.spec.ts
Original file line number Diff line number Diff line change
@@ -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