Merge branch 'main' into fix/8466

This commit is contained in:
edison 2025-01-16 08:24:53 +08:00 committed by GitHub
commit 043712047d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
115 changed files with 3424 additions and 1566 deletions

View File

@ -1,18 +1,17 @@
{
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
extends: ['config:base', 'schedule:weekly', 'group:allNonMajor'],
extends: ['config:recommended', 'schedule:weekly', 'group:allNonMajor'],
labels: ['dependencies'],
ignorePaths: ['**/__tests__/**'],
rangeStrategy: 'bump',
packageRules: [
{
depTypeList: ['peerDependencies'],
matchDepTypes: ['peerDependencies'],
enabled: false,
},
{
groupName: 'test',
matchPackageNames: ['vitest', 'jsdom', 'puppeteer'],
matchPackagePrefixes: ['@vitest'],
matchPackageNames: ['vitest', 'jsdom', 'puppeteer', '@vitest{/,}**'],
},
{
groupName: 'playground',
@ -23,18 +22,28 @@
},
{
groupName: 'compiler',
matchPackageNames: ['magic-string'],
matchPackagePrefixes: ['@babel', 'postcss'],
matchPackageNames: ['magic-string', '@babel{/,}**', 'postcss{/,}**'],
},
{
groupName: 'build',
matchPackageNames: ['vite', '@swc/core'],
matchPackagePrefixes: ['rollup', 'esbuild', '@rollup', '@vitejs'],
matchPackageNames: [
'vite',
'@swc/core',
'rollup{/,}**',
'esbuild{/,}**',
'@rollup{/,}**',
'@vitejs{/,}**',
],
},
{
groupName: 'lint',
matchPackageNames: ['simple-git-hooks', 'lint-staged'],
matchPackagePrefixes: ['typescript-eslint', 'eslint', 'prettier'],
matchPackageNames: [
'simple-git-hooks',
'lint-staged',
'typescript-eslint{/,}**',
'eslint{/,}**',
'prettier{/,}**',
],
},
],
ignoreDeps: [

View File

@ -31,4 +31,4 @@ jobs:
- name: Run prettier
run: pnpm run format
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

@ -37,7 +37,7 @@ jobs:
run: pnpm install
- name: Download Size Data
uses: dawidd6/action-download-artifact@v6
uses: dawidd6/action-download-artifact@v7
with:
name: size-data
run_id: ${{ github.event.workflow_run.id }}
@ -56,7 +56,7 @@ jobs:
path: temp/size/base.txt
- name: Download Previous Size Data
uses: dawidd6/action-download-artifact@v6
uses: dawidd6/action-download-artifact@v7
with:
branch: ${{ steps.pr-base.outputs.content }}
workflow: size-data.yml

View File

@ -13,5 +13,6 @@
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
},
"editor.formatOnSave": true
}

View File

@ -0,0 +1 @@
https://vuejs.org/funding.json

View File

@ -1,3 +1,67 @@
## [3.5.13](https://github.com/vuejs/core/compare/v3.5.12...v3.5.13) (2024-11-15)
### Bug Fixes
* **compiler-core:** handle v-memo + v-for with functional key ([#12014](https://github.com/vuejs/core/issues/12014)) ([99009ee](https://github.com/vuejs/core/commit/99009eee0efc238392daba93792d478525b21afa)), closes [#12013](https://github.com/vuejs/core/issues/12013)
* **compiler-dom:** properly stringify template string style ([#12392](https://github.com/vuejs/core/issues/12392)) ([2d78539](https://github.com/vuejs/core/commit/2d78539da35322aea5f821b3cf9b02d006abac72)), closes [#12391](https://github.com/vuejs/core/issues/12391)
* **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)
* **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))
* **ssr:** avoid updating subtree of async component if it is resolved ([#12363](https://github.com/vuejs/core/issues/12363)) ([da7ad5e](https://github.com/vuejs/core/commit/da7ad5e3d24f3e108401188d909d27a4910da095)), closes [#12362](https://github.com/vuejs/core/issues/12362)
* **ssr:** ensure v-text updates correctly with custom directives in SSR output ([#12311](https://github.com/vuejs/core/issues/12311)) ([1f75d4e](https://github.com/vuejs/core/commit/1f75d4e6dfe18121ebe443cd3e8105d54f727893)), closes [#12309](https://github.com/vuejs/core/issues/12309)
* **ssr:** handle initial selected state for select with v-model + v-for option ([#12399](https://github.com/vuejs/core/issues/12399)) ([4f8d807](https://github.com/vuejs/core/commit/4f8d8078221ee52deed266677a227ad2a6d8dd22)), closes [#12395](https://github.com/vuejs/core/issues/12395)
* **teleport:** handle deferred teleport update before mounted ([#12168](https://github.com/vuejs/core/issues/12168)) ([8bff142](https://github.com/vuejs/core/commit/8bff142f99b646e9dd15897ec75368fbf34f1534)), closes [#12161](https://github.com/vuejs/core/issues/12161)
* **templateRef:** set ref on cached async component which wrapped in KeepAlive ([#12290](https://github.com/vuejs/core/issues/12290)) ([983eb50](https://github.com/vuejs/core/commit/983eb50a17eac76f1bba4394ad0316c62b72191d)), closes [#4999](https://github.com/vuejs/core/issues/4999) [#5004](https://github.com/vuejs/core/issues/5004)
* **test:** update snapshot ([#12169](https://github.com/vuejs/core/issues/12169)) ([828d4a4](https://github.com/vuejs/core/commit/828d4a443919fa2aa4e2e92fbd03a5f04b258eea))
* **Transition:** fix transition memory leak edge case ([#12182](https://github.com/vuejs/core/issues/12182)) ([660132d](https://github.com/vuejs/core/commit/660132df6c6a8c14bf75e593dc47d2fdada30322)), closes [#12181](https://github.com/vuejs/core/issues/12181)
* **transition:** reflow before leave-active class after leave-from ([#12288](https://github.com/vuejs/core/issues/12288)) ([4b479db](https://github.com/vuejs/core/commit/4b479db61d233b054561402ae94ef08550073ea1)), closes [#2593](https://github.com/vuejs/core/issues/2593)
* **types:** defineEmits w/ interface declaration ([#12343](https://github.com/vuejs/core/issues/12343)) ([1022eab](https://github.com/vuejs/core/commit/1022eabaa1aaf8436876f5ec5573cb1e4b3959a6)), closes [#8457](https://github.com/vuejs/core/issues/8457)
* **v-once:** setting hasOnce to current block only when in v-once ([#12374](https://github.com/vuejs/core/issues/12374)) ([37300fc](https://github.com/vuejs/core/commit/37300fc26190a7299efddbf98800ffd96d5cad96)), closes [#12371](https://github.com/vuejs/core/issues/12371)
### Performance Improvements
* **reactivity:** do not track inner key `__v_skip`` ([#11690](https://github.com/vuejs/core/issues/11690)) ([d637bd6](https://github.com/vuejs/core/commit/d637bd6c0164c2883e6eabd3c2f1f8c258dedfb1))
* **runtime-core:** use feature flag for call to resolveMergedOptions ([#12163](https://github.com/vuejs/core/issues/12163)) ([1755ac0](https://github.com/vuejs/core/commit/1755ac0a108ba3486bd8397e56d3bdcd69196594))
## [3.5.12](https://github.com/vuejs/core/compare/v3.5.11...v3.5.12) (2024-10-11)
### Bug Fixes
* **compiler-dom:** avoid stringify option with null value ([#12096](https://github.com/vuejs/core/issues/12096)) ([f6d9926](https://github.com/vuejs/core/commit/f6d99262364b7444ebab8742158599e8cdd79eaa)), closes [#12093](https://github.com/vuejs/core/issues/12093)
* **compiler-sfc:** do not skip TSInstantiationExpression when transforming props destructure ([#12064](https://github.com/vuejs/core/issues/12064)) ([d3ecde8](https://github.com/vuejs/core/commit/d3ecde8a696ff62c8d0ab067fd1d7ee0565b63c5))
* **compiler-sfc:** use sass modern api if available and avoid deprecation warning ([#11992](https://github.com/vuejs/core/issues/11992)) ([4474c11](https://github.com/vuejs/core/commit/4474c113d1fb1c26298dd6794275d5b5c7cc4d93))
* **compiler:** clone loc to `ifNode` ([#12131](https://github.com/vuejs/core/issues/12131)) ([cde2c06](https://github.com/vuejs/core/commit/cde2c0671b00d4f6111fcbd7aa76e45872f20b0c)), closes [vuejs/language-tools#4911](https://github.com/vuejs/language-tools/issues/4911)
* **custom-element:** properly remove hyphenated attribute ([#12143](https://github.com/vuejs/core/issues/12143)) ([e16e9a7](https://github.com/vuejs/core/commit/e16e9a7341e7cfb3c443da4e5e5b06e8158712c3)), closes [#12139](https://github.com/vuejs/core/issues/12139)
* **defineModel:** handle kebab-case model correctly ([#12063](https://github.com/vuejs/core/issues/12063)) ([c0418a3](https://github.com/vuejs/core/commit/c0418a3b8fa96a0b108ab71b7aab5d3388f90557)), closes [#12060](https://github.com/vuejs/core/issues/12060)
* **deps:** update dependency monaco-editor to ^0.52.0 ([#12119](https://github.com/vuejs/core/issues/12119)) ([f7cbea2](https://github.com/vuejs/core/commit/f7cbea2111c7770a180b640f36f6a5d4d6abc698))
* **hydration:** provide compat fallback for idle callback hydration strategy ([#11935](https://github.com/vuejs/core/issues/11935)) ([1ae545a](https://github.com/vuejs/core/commit/1ae545a3786abef983be1c969726489685569c92))
* **reactivity:** trigger reactivity for Map key `undefined` ([#12055](https://github.com/vuejs/core/issues/12055)) ([7ad289e](https://github.com/vuejs/core/commit/7ad289e1e7fea654524008ff91e43a8b8a55ef22)), closes [#12054](https://github.com/vuejs/core/issues/12054)
* **runtime-core:** allow symbol values for slot prop key ([#12069](https://github.com/vuejs/core/issues/12069)) ([d9d4d4e](https://github.com/vuejs/core/commit/d9d4d4e158cd51a9ddda249f29de8467f60b2792)), closes [#12068](https://github.com/vuejs/core/issues/12068)
* **runtime-core:** fix required prop check false positive for kebab-case edge cases ([#12034](https://github.com/vuejs/core/issues/12034)) ([9da1ac1](https://github.com/vuejs/core/commit/9da1ac156552ac449754e1373aac7e349841becb)), closes [#12011](https://github.com/vuejs/core/issues/12011)
* **runtime-dom:** prevent unnecessary updates in v-model checkbox when value is unchanged ([#12146](https://github.com/vuejs/core/issues/12146)) ([ea943af](https://github.com/vuejs/core/commit/ea943afe404c4ca4b729906c5e8daf7aa2ccde9b)), closes [#12144](https://github.com/vuejs/core/issues/12144)
* **teleport:** handle disabled teleport with updateCssVars ([#12113](https://github.com/vuejs/core/issues/12113)) ([76a8223](https://github.com/vuejs/core/commit/76a8223199c148b79a5c0ea19e235164809760cd)), closes [#12112](https://github.com/vuejs/core/issues/12112)
* **transition/ssr:** make transition appear work with Suspense in SSR ([#12047](https://github.com/vuejs/core/issues/12047)) ([f1a4f67](https://github.com/vuejs/core/commit/f1a4f67aedfe83e440c54222213f070774faa421)), closes [#12046](https://github.com/vuejs/core/issues/12046)
* **types:** ensure `this.$props` type does not include `string` ([#12123](https://github.com/vuejs/core/issues/12123)) ([704173e](https://github.com/vuejs/core/commit/704173e24276706de672cca6c9507e4dd9651197)), closes [#12122](https://github.com/vuejs/core/issues/12122)
* **types:** retain union type narrowing with defaults applied ([#12108](https://github.com/vuejs/core/issues/12108)) ([05685a9](https://github.com/vuejs/core/commit/05685a9d7c42d4cd37169b867833776b91154fed)), closes [#12106](https://github.com/vuejs/core/issues/12106)
* **useId:** ensure useId consistency when using serverPrefetch ([#12128](https://github.com/vuejs/core/issues/12128)) ([b4d3534](https://github.com/vuejs/core/commit/b4d35349d8bc39aa15bd3f1094d230e5928b177c)), closes [#12102](https://github.com/vuejs/core/issues/12102)
* **watch:** watchEffect clean-up with SSR ([#12097](https://github.com/vuejs/core/issues/12097)) ([b094c72](https://github.com/vuejs/core/commit/b094c72b3d40c52c7124f145a9db028509a11202)), closes [#11956](https://github.com/vuejs/core/issues/11956)
### Performance Improvements
* **reactivity:** avoid unnecessary recursion in removeSub ([#12135](https://github.com/vuejs/core/issues/12135)) ([ec917cf](https://github.com/vuejs/core/commit/ec917cfdb9d0169cd0835d3a0e28244242657dc9))
## [3.5.11](https://github.com/vuejs/core/compare/v3.5.10...v3.5.11) (2024-10-03)

View File

@ -741,17 +741,6 @@ Note that this is a type-only breaking change in a minor release, which adheres
## [3.3.13](https://github.com/vuejs/core/compare/v3.3.12...v3.3.13) (2023-12-19)
### Bug Fixes
* **compiler-core:** fix v-on with modifiers on inline expression of undefined ([#9866](https://github.com/vuejs/core/issues/9866)) ([bae79dd](https://github.com/vuejs/core/commit/bae79ddf8564a2da4a5365cfeb8d811990f42335)), closes [#9865](https://github.com/vuejs/core/issues/9865)
* **runtime-dom:** cache event handlers by key/modifiers ([#9851](https://github.com/vuejs/core/issues/9851)) ([04d2c05](https://github.com/vuejs/core/commit/04d2c05054c26b02fbc1d84839b0ed5cd36455b6)), closes [#9849](https://github.com/vuejs/core/issues/9849)
* **types:** extract properties from extended collections ([#9854](https://github.com/vuejs/core/issues/9854)) ([24b1c1d](https://github.com/vuejs/core/commit/24b1c1dd57fd55d998aa231a147500e010b10219)), closes [#9852](https://github.com/vuejs/core/issues/9852)
# [3.4.0-beta.3](https://github.com/vuejs/core/compare/v3.3.12...v3.4.0-beta.3) (2023-12-16)
@ -764,19 +753,6 @@ Note that this is a type-only breaking change in a minor release, which adheres
## [3.3.12](https://github.com/vuejs/core/compare/v3.3.11...v3.3.12) (2023-12-16)
### Bug Fixes
* **hydration:** handle appear transition before patch props ([#9837](https://github.com/vuejs/core/issues/9837)) ([e70f4c4](https://github.com/vuejs/core/commit/e70f4c47c553b6e16d8fad70743271ca23802fe7)), closes [#9832](https://github.com/vuejs/core/issues/9832)
* **sfc/cssVars:** fix loss of CSS v-bind variables when setting inline style with string value ([#9824](https://github.com/vuejs/core/issues/9824)) ([0a387df](https://github.com/vuejs/core/commit/0a387dfb1d04afb6eae4296b6da76dfdaca77af4)), closes [#9821](https://github.com/vuejs/core/issues/9821)
* **ssr:** fix suspense hydration of fallback content ([#7188](https://github.com/vuejs/core/issues/7188)) ([60415b5](https://github.com/vuejs/core/commit/60415b5d67df55f1fd6b176615299c08640fa142))
* **types:** add `xmlns:xlink` to `SVGAttributes` ([#9300](https://github.com/vuejs/core/issues/9300)) ([0d61b42](https://github.com/vuejs/core/commit/0d61b429ecf63591d31e09702058fa4c7132e1a7)), closes [#9299](https://github.com/vuejs/core/issues/9299)
* **types:** fix `shallowRef` type error ([#9839](https://github.com/vuejs/core/issues/9839)) ([9a57158](https://github.com/vuejs/core/commit/9a571582b53220270e498d8712ea59312c0bef3a))
* **types:** support for generic keyof slots ([#8374](https://github.com/vuejs/core/issues/8374)) ([213eba4](https://github.com/vuejs/core/commit/213eba479ce080efc1053fe636f6be4a4c889b44))
# [3.4.0-beta.2](https://github.com/vuejs/core/compare/v3.4.0-beta.1...v3.4.0-beta.2) (2023-12-14)
@ -836,22 +812,6 @@ default.
## [3.3.11](https://github.com/vuejs/core/compare/v3.3.10...v3.3.11) (2023-12-08)
### Bug Fixes
* **custom-element:** correctly handle number type props in prod ([#8989](https://github.com/vuejs/core/issues/8989)) ([d74d364](https://github.com/vuejs/core/commit/d74d364d62db8e48881af6b5a75ce4fb5f36cc35))
* **reactivity:** fix mutation on user proxy of reactive Array ([6ecbd5c](https://github.com/vuejs/core/commit/6ecbd5ce2a7f59314a8326a1d193874b87f4d8c8)), closes [#9742](https://github.com/vuejs/core/issues/9742) [#9751](https://github.com/vuejs/core/issues/9751) [#9750](https://github.com/vuejs/core/issues/9750)
* **runtime-dom:** fix width and height prop check condition ([5b00286](https://github.com/vuejs/core/commit/5b002869c533220706f9788b496b8ca8d8e98609)), closes [#9762](https://github.com/vuejs/core/issues/9762)
* **shared:** handle Map with symbol keys in toDisplayString ([#9731](https://github.com/vuejs/core/issues/9731)) ([364821d](https://github.com/vuejs/core/commit/364821d6bdb1775e2f55a69bcfb9f40f7acf1506)), closes [#9727](https://github.com/vuejs/core/issues/9727)
* **shared:** handle more Symbol cases in toDisplayString ([983d45d](https://github.com/vuejs/core/commit/983d45d4f8eb766b5a16b7ea93b86d3c51618fa6))
* **Suspense:** properly get anchor when mount fallback vnode ([#9770](https://github.com/vuejs/core/issues/9770)) ([b700328](https://github.com/vuejs/core/commit/b700328342e17dc16b19316c2e134a26107139d2)), closes [#9769](https://github.com/vuejs/core/issues/9769)
* **types:** ref() return type should not be any when initial value is any ([#9768](https://github.com/vuejs/core/issues/9768)) ([cdac121](https://github.com/vuejs/core/commit/cdac12161ec27b45ded48854c3d749664b6d4a6d))
* **watch:** should not fire pre watcher on child component unmount ([#7181](https://github.com/vuejs/core/issues/7181)) ([6784f0b](https://github.com/vuejs/core/commit/6784f0b1f8501746ea70d87d18ed63a62cf6b76d)), closes [#7030](https://github.com/vuejs/core/issues/7030)
# [3.4.0-alpha.4](https://github.com/vuejs/core/compare/v3.3.10...v3.4.0-alpha.4) (2023-12-04)
@ -873,37 +833,6 @@ default.
## [3.3.10](https://github.com/vuejs/core/compare/v3.3.9...v3.3.10) (2023-12-04)
### Bug Fixes
* **app:** prevent template from being cached between apps with different options ([#9724](https://github.com/vuejs/core/issues/9724)) ([ec71585](https://github.com/vuejs/core/commit/ec715854ca12520b2afc9e9b3981cbae05ae5206)), closes [#9618](https://github.com/vuejs/core/issues/9618)
* **compiler-sfc:** avoid passing forEach index to genMap ([f12db7f](https://github.com/vuejs/core/commit/f12db7fb564a534cef2e5805cc9f54afe5d72fbf))
* **compiler-sfc:** deindent pug/jade templates ([6345197](https://github.com/vuejs/core/commit/634519720a21fb5a6871454e1cadad7053a568b8)), closes [#3231](https://github.com/vuejs/core/issues/3231) [#3842](https://github.com/vuejs/core/issues/3842) [#7723](https://github.com/vuejs/core/issues/7723)
* **compiler-sfc:** fix :where and :is selector in scoped mode with multiple selectors ([#9735](https://github.com/vuejs/core/issues/9735)) ([c3e2c55](https://github.com/vuejs/core/commit/c3e2c556b532656b50b8ab5cd2d9eabc26622d63)), closes [#9707](https://github.com/vuejs/core/issues/9707)
* **compiler-sfc:** generate more treeshaking friendly code ([#9507](https://github.com/vuejs/core/issues/9507)) ([8d74ca0](https://github.com/vuejs/core/commit/8d74ca0e6fa2738ca6854b7e879ff59419f948c7)), closes [#9500](https://github.com/vuejs/core/issues/9500)
* **compiler-sfc:** support inferring generic types ([#8511](https://github.com/vuejs/core/issues/8511)) ([eb5e307](https://github.com/vuejs/core/commit/eb5e307c0be62002e62c4c800d0dfacb39b0d4ca)), closes [#8482](https://github.com/vuejs/core/issues/8482)
* **compiler-sfc:** support resolving components from props ([#8785](https://github.com/vuejs/core/issues/8785)) ([7cbcee3](https://github.com/vuejs/core/commit/7cbcee3d831241a8bd3588ae92d3f27e3641e25f))
* **compiler-sfc:** throw error when failing to load TS during type resolution ([#8883](https://github.com/vuejs/core/issues/8883)) ([4936d2e](https://github.com/vuejs/core/commit/4936d2e11a8d0ca3704bfe408548cb26bb3fd5e9))
* **cssVars:** cssVar names should be double-escaped when generating code for ssr ([#8824](https://github.com/vuejs/core/issues/8824)) ([5199a12](https://github.com/vuejs/core/commit/5199a12f8855cd06f24bf355708b5a2134f63176)), closes [#7823](https://github.com/vuejs/core/issues/7823)
* **deps:** update compiler to ^7.23.4 ([#9681](https://github.com/vuejs/core/issues/9681)) ([31f6ebc](https://github.com/vuejs/core/commit/31f6ebc4df84490ed29fb75e7bf4259200eb51f0))
* **runtime-core:** Suspense get anchor properly in Transition ([#9309](https://github.com/vuejs/core/issues/9309)) ([65f3fe2](https://github.com/vuejs/core/commit/65f3fe273127a8b68e1222fbb306d28d85f01757)), closes [#8105](https://github.com/vuejs/core/issues/8105)
* **runtime-dom:** set width/height with units as attribute ([#8781](https://github.com/vuejs/core/issues/8781)) ([bfc1838](https://github.com/vuejs/core/commit/bfc1838f31199de3f189198a3c234fa7bae91386))
* **ssr:** avoid computed being accidentally cached before server render ([#9688](https://github.com/vuejs/core/issues/9688)) ([30d5d93](https://github.com/vuejs/core/commit/30d5d93a92b2154406ec04f8aca6b217fa01177c)), closes [#5300](https://github.com/vuejs/core/issues/5300)
* **types:** expose emits as props in functional components ([#9234](https://github.com/vuejs/core/issues/9234)) ([887e54c](https://github.com/vuejs/core/commit/887e54c347ea9eac4c721b5e2288f054873d1d30))
* **types:** fix reactive collection types ([#8960](https://github.com/vuejs/core/issues/8960)) ([ad27473](https://github.com/vuejs/core/commit/ad274737015c36906d76f3189203093fa3a2e4e7)), closes [#8904](https://github.com/vuejs/core/issues/8904)
* **types:** improve return type withKeys and withModifiers ([#9734](https://github.com/vuejs/core/issues/9734)) ([43c3cfd](https://github.com/vuejs/core/commit/43c3cfdec5ae5d70fa2a21e857abc2d73f1a0d07))
### Performance Improvements
* optimize on* prop check ([38aaa8c](https://github.com/vuejs/core/commit/38aaa8c88648c54fe2616ad9c0961288092fcb44))
* **runtime-dom:** cache modifier wrapper functions ([da4a4fb](https://github.com/vuejs/core/commit/da4a4fb5e8eee3c6d31f24ebd79a9d0feca56cb2)), closes [#8882](https://github.com/vuejs/core/issues/8882)
* **v-on:** constant handlers with modifiers should not be treated as dynamic ([4d94ebf](https://github.com/vuejs/core/commit/4d94ebfe75174b340d2b794e699cad1add3600a9))
# [3.4.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.0-alpha.2...v3.4.0-alpha.3) (2023-11-28)
@ -960,55 +889,6 @@ default.
## [3.3.9](https://github.com/vuejs/core/compare/v3.3.8...v3.3.9) (2023-11-25)
### Bug Fixes
* **compiler-core:** avoid rewriting scope variables in inline for loops ([#7245](https://github.com/vuejs/core/issues/7245)) ([a2d810e](https://github.com/vuejs/core/commit/a2d810eb40cef631f61991ca68b426ee9546aba0)), closes [#7238](https://github.com/vuejs/core/issues/7238)
* **compiler-core:** fix `resolveParserPlugins` decorators check ([#9566](https://github.com/vuejs/core/issues/9566)) ([9d0eba9](https://github.com/vuejs/core/commit/9d0eba916f3bf6fb5c03222400edae1a2db7444f)), closes [#9560](https://github.com/vuejs/core/issues/9560)
* **compiler-sfc:** consistently escape type-only prop names ([#8654](https://github.com/vuejs/core/issues/8654)) ([3e08d24](https://github.com/vuejs/core/commit/3e08d246dfd8523c54fb8e7a4a6fd5506ffb1bcc)), closes [#8635](https://github.com/vuejs/core/issues/8635) [#8910](https://github.com/vuejs/core/issues/8910) [vitejs/vite-plugin-vue#184](https://github.com/vitejs/vite-plugin-vue/issues/184)
* **compiler-sfc:** malformed filename on windows using path.posix.join() ([#9478](https://github.com/vuejs/core/issues/9478)) ([f18a174](https://github.com/vuejs/core/commit/f18a174979626b3429db93c5d5b7ae5448917c70)), closes [#8671](https://github.com/vuejs/core/issues/8671) [#9583](https://github.com/vuejs/core/issues/9583) [#9446](https://github.com/vuejs/core/issues/9446) [#9473](https://github.com/vuejs/core/issues/9473)
* **compiler-sfc:** support `:is` and `:where` selector in scoped css rewrite ([#8929](https://github.com/vuejs/core/issues/8929)) ([3227e50](https://github.com/vuejs/core/commit/3227e50b32105f8893f7dff2f29278c5b3a9f621))
* **compiler-sfc:** support resolve extends interface for defineEmits ([#8470](https://github.com/vuejs/core/issues/8470)) ([9e1b74b](https://github.com/vuejs/core/commit/9e1b74bcd5fa4151f5d1bc02c69fbbfa4762f577)), closes [#8465](https://github.com/vuejs/core/issues/8465)
* **hmr/transition:** fix kept-alive component inside transition disappearing after hmr ([#7126](https://github.com/vuejs/core/issues/7126)) ([d11e978](https://github.com/vuejs/core/commit/d11e978fc98dcc83526c167e603b8308f317f786)), closes [#7121](https://github.com/vuejs/core/issues/7121)
* **hydration:** force hydration for v-bind with .prop modifier ([364f319](https://github.com/vuejs/core/commit/364f319d214226770d97c98d8fcada80c9e8dde3)), closes [#7490](https://github.com/vuejs/core/issues/7490)
* **hydration:** properly hydrate indeterminate prop ([34b5a5d](https://github.com/vuejs/core/commit/34b5a5da4ae9c9faccac237acd7acc8e7e017571)), closes [#7476](https://github.com/vuejs/core/issues/7476)
* **reactivity:** clear method on readonly collections should return undefined ([#7316](https://github.com/vuejs/core/issues/7316)) ([657476d](https://github.com/vuejs/core/commit/657476dcdb964be4fbb1277c215c073f3275728e))
* **reactivity:** onCleanup also needs to be cleaned ([#8655](https://github.com/vuejs/core/issues/8655)) ([73fd810](https://github.com/vuejs/core/commit/73fd810eebdd383a2b4629f67736c4db1f428abd)), closes [#5151](https://github.com/vuejs/core/issues/5151) [#7695](https://github.com/vuejs/core/issues/7695)
* **ssr:** hydration `__vnode` missing for devtools ([#9328](https://github.com/vuejs/core/issues/9328)) ([5156ac5](https://github.com/vuejs/core/commit/5156ac5b38cfa80d3db26f2c9bf40cb22a7521cb))
* **types:** allow falsy value types in `StyleValue` ([#7954](https://github.com/vuejs/core/issues/7954)) ([17aa92b](https://github.com/vuejs/core/commit/17aa92b79b31d8bb8b5873ddc599420cb9806db8)), closes [#7955](https://github.com/vuejs/core/issues/7955)
* **types:** defineCustomElement using defineComponent return type with emits ([#7937](https://github.com/vuejs/core/issues/7937)) ([5d932a8](https://github.com/vuejs/core/commit/5d932a8e6d14343c9d7fc7c2ecb58ac618b2f938)), closes [#7782](https://github.com/vuejs/core/issues/7782)
* **types:** fix `unref` and `toValue` when input union type contains ComputedRef ([#8748](https://github.com/vuejs/core/issues/8748)) ([176d476](https://github.com/vuejs/core/commit/176d47671271b1abc21b1508e9a493c7efca6451)), closes [#8747](https://github.com/vuejs/core/issues/8747) [#8857](https://github.com/vuejs/core/issues/8857)
* **types:** fix instance type when props type is incompatible with setup returned type ([#7338](https://github.com/vuejs/core/issues/7338)) ([0e1e8f9](https://github.com/vuejs/core/commit/0e1e8f919e5a74cdaadf9c80ee135088b25e7fa3)), closes [#5885](https://github.com/vuejs/core/issues/5885)
* **types:** fix shallowRef return type with union value type ([#7853](https://github.com/vuejs/core/issues/7853)) ([7c44800](https://github.com/vuejs/core/commit/7c448000b0def910c2cfabfdf7ff20a3d6bc844f)), closes [#7852](https://github.com/vuejs/core/issues/7852)
* **types:** more precise types for class bindings ([#8012](https://github.com/vuejs/core/issues/8012)) ([46e3374](https://github.com/vuejs/core/commit/46e33744c890bd49482c5e5c5cdea44e00ec84d5))
* **types:** remove optional properties from defineProps return type ([#6421](https://github.com/vuejs/core/issues/6421)) ([94c049d](https://github.com/vuejs/core/commit/94c049d930d922069e38ea8700d7ff0970f71e61)), closes [#6420](https://github.com/vuejs/core/issues/6420)
* **types:** return type of withDefaults should be readonly ([#8601](https://github.com/vuejs/core/issues/8601)) ([f15debc](https://github.com/vuejs/core/commit/f15debc01acb22d23f5acee97e6f02db88cef11a))
* **types:** revert class type restrictions ([5d077c8](https://github.com/vuejs/core/commit/5d077c8754cc14f85d2d6d386df70cf8c0d93842)), closes [#8012](https://github.com/vuejs/core/issues/8012)
* **types:** update jsx type definitions ([#8607](https://github.com/vuejs/core/issues/8607)) ([58e2a94](https://github.com/vuejs/core/commit/58e2a94871ae06a909c5f8bad07fb401193e6a38))
* **types:** widen ClassValue type ([2424013](https://github.com/vuejs/core/commit/242401305944422d0c361b16101a4d18908927af))
* **v-model:** avoid overwriting number input with same value ([#7004](https://github.com/vuejs/core/issues/7004)) ([40f4b77](https://github.com/vuejs/core/commit/40f4b77bb570868cb6e47791078767797e465989)), closes [#7003](https://github.com/vuejs/core/issues/7003)
* **v-model:** unnecessary value binding error should apply to dynamic instead of static binding ([2859b65](https://github.com/vuejs/core/commit/2859b653c9a22460e60233cac10fe139e359b046)), closes [#3596](https://github.com/vuejs/core/issues/3596)
## [3.3.8](https://github.com/vuejs/core/compare/v3.3.7...v3.3.8) (2023-11-06)
### Bug Fixes
* **compile-sfc:** support `Error` type in `defineProps` ([#5955](https://github.com/vuejs/core/issues/5955)) ([a989345](https://github.com/vuejs/core/commit/a9893458ec519aae442e1b99e64e6d74685cd22c))
* **compiler-core:** known global should be shadowed by local variables in expression rewrite ([#9492](https://github.com/vuejs/core/issues/9492)) ([a75d1c5](https://github.com/vuejs/core/commit/a75d1c5c6242e91a73cc5ba01e6da620dea0b3d9)), closes [#9482](https://github.com/vuejs/core/issues/9482)
* **compiler-sfc:** fix dynamic directive arguments usage check for slots ([#9495](https://github.com/vuejs/core/issues/9495)) ([b39fa1f](https://github.com/vuejs/core/commit/b39fa1f8157647859331ce439c42ae016a49b415)), closes [#9493](https://github.com/vuejs/core/issues/9493)
* **deps:** update dependency @vue/repl to ^2.6.2 ([#9536](https://github.com/vuejs/core/issues/9536)) ([5cef325](https://github.com/vuejs/core/commit/5cef325f41e3b38657c72fa1a38dedeee1c7a60a))
* **deps:** update dependency @vue/repl to ^2.6.3 ([#9540](https://github.com/vuejs/core/issues/9540)) ([176d590](https://github.com/vuejs/core/commit/176d59058c9aecffe9da4d4311e98496684f06d4))
* **hydration:** fix tagName access error on comment/text node hydration mismatch ([dd8a0cf](https://github.com/vuejs/core/commit/dd8a0cf5dcde13d2cbd899262a0e07f16e14e489)), closes [#9531](https://github.com/vuejs/core/issues/9531)
* **types:** avoid exposing lru-cache types in generated dts ([462aeb3](https://github.com/vuejs/core/commit/462aeb3b600765e219ded2ee9a0ed1e74df61de0)), closes [#9521](https://github.com/vuejs/core/issues/9521)
* **warn:** avoid warning on empty children with Suspense ([#3962](https://github.com/vuejs/core/issues/3962)) ([405f345](https://github.com/vuejs/core/commit/405f34587a63a5f1e3d147b9848219ea98acc22d))
# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28)

View File

@ -1,7 +1,7 @@
{
"private": true,
"version": "3.5.11",
"packageManager": "pnpm@9.12.0",
"version": "3.5.13",
"packageManager": "pnpm@9.15.3",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@ -22,7 +22,10 @@
"test-dts": "run-s build-dts test-dts-only",
"test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json",
"test-coverage": "vitest run --project unit --coverage",
"test-bench": "vitest bench",
"prebench": "node scripts/build.js -pf esm-browser reactivity",
"prebench-compare": "node scripts/build.js -pf esm-browser reactivity",
"bench": "vitest bench --project=unit --outputJson=temp/bench.json",
"bench-compare": "vitest bench --project=unit --compare=temp/bench.json",
"release": "node scripts/release.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"dev-esm": "node scripts/dev.js -if esm-bundler-runtime",
@ -62,52 +65,52 @@
"@babel/parser": "catalog:",
"@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.7.28",
"@swc/core": "^1.10.7",
"@types/hash-sum": "^1.0.2",
"@types/node": "^20.16.10",
"@types/node": "^22.10.5",
"@types/semver": "^7.5.8",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^2.1.1",
"@vitest/coverage-v8": "^2.1.8",
"@vue/consolidate": "1.0.0",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
"esbuild": "^0.24.0",
"esbuild": "^0.24.2",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^9.12.0",
"eslint-plugin-import-x": "^4.3.1",
"@vitest/eslint-plugin": "^1.0.1",
"eslint": "^9.18.0",
"eslint-plugin-import-x": "^4.6.1",
"@vitest/eslint-plugin": "^1.1.25",
"estree-walker": "catalog:",
"jsdom": "^25.0.0",
"lint-staged": "^15.2.10",
"jsdom": "^26.0.0",
"lint-staged": "^15.3.0",
"lodash": "^4.17.21",
"magic-string": "^0.30.11",
"markdown-table": "^3.0.3",
"magic-string": "^0.30.17",
"markdown-table": "^3.0.4",
"marked": "13.0.3",
"npm-run-all2": "^6.2.3",
"picocolors": "^1.1.0",
"prettier": "^3.3.3",
"npm-run-all2": "^7.0.2",
"picocolors": "^1.1.1",
"prettier": "^3.4.2",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~23.3.0",
"puppeteer": "~24.0.0",
"rimraf": "^6.0.1",
"rollup": "^4.24.0",
"rollup": "^4.30.1",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"semver": "^7.6.3",
"serve": "^14.2.3",
"serve-handler": "^6.1.5",
"serve": "^14.2.4",
"serve-handler": "^6.1.6",
"simple-git-hooks": "^2.11.1",
"todomvc-app-css": "^2.4.3",
"tslib": "^2.7.0",
"tslib": "^2.8.1",
"typescript": "~5.6.2",
"typescript-eslint": "^8.8.0",
"typescript-eslint": "^8.19.1",
"vite": "catalog:",
"vitest": "^2.1.1"
"vitest": "^2.1.8"
},
"pnpm": {
"peerDependencyRules": {

View File

@ -2068,3 +2068,13 @@ expectString(instance.actionText)
// public prop on $props should be optional
// @ts-expect-error
expectString(instance.$props.actionText)
// #12122
defineComponent({
props: { foo: String },
render() {
expectType<{ readonly foo?: string }>(this.$props)
// @ts-expect-error
expectType<string>(this.$props)
},
})

View File

@ -240,6 +240,23 @@ describe('withDefaults w/ defineProp type is different from the defaults type',
res1.value
})
describe('withDefaults w/ defineProp discriminate union type', () => {
const props = withDefaults(
defineProps<
{ type: 'button'; buttonType?: 'submit' } | { type: 'link'; href: string }
>(),
{
type: 'button',
},
)
if (props.type === 'button') {
expectType<'submit' | undefined>(props.buttonType)
}
if (props.type === 'link') {
expectType<string>(props.href)
}
})
describe('defineProps w/ runtime declaration', () => {
// runtime declaration
const props = defineProps({
@ -289,6 +306,14 @@ describe('defineEmits w/ type declaration', () => {
emit2('baz')
})
describe('defineEmits w/ interface declaration', () => {
interface Emits {
foo: [value: string]
}
const emit = defineEmits<Emits>()
emit('foo', 'hi')
})
describe('defineEmits w/ alt type declaration', () => {
const emit = defineEmits<{
foo: [id: string]

View File

@ -13,7 +13,7 @@
"vite": "catalog:"
},
"dependencies": {
"@vue/repl": "^4.4.2",
"@vue/repl": "^4.4.3",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"vue": "workspace:*"

View File

@ -123,6 +123,7 @@ onMounted(() => {
:prod="productionMode"
:ssr="useSSRMode"
:autoSave="autoSave"
:theme="theme"
@toggle-theme="toggleTheme"
@toggle-prod="toggleProdMode"
@toggle-ssr="toggleSSR"

View File

@ -15,6 +15,7 @@ const props = defineProps<{
prod: boolean
ssr: boolean
autoSave: boolean
theme: 'dark' | 'light'
}>()
const emit = defineEmits([
'toggle-theme',
@ -45,6 +46,7 @@ function resetVueVersion() {
async function copyLink(e: MouseEvent) {
if (e.metaKey) {
resetVueVersion()
// hidden logic for going to local debug from play.vuejs.org
window.location.href = 'http://localhost:5173/' + window.location.hash
return
@ -117,7 +119,11 @@ function toggleDark() {
>
<span>{{ autoSave ? 'AutoSave ON' : 'AutoSave OFF' }}</span>
</button>
<button title="Toggle dark mode" class="toggle-dark" @click="toggleDark">
<button
:title="`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`"
class="toggle-dark"
@click="toggleDark"
>
<Sun class="light" />
<Moon class="dark" />
</button>

View File

@ -11,7 +11,7 @@
"vue": "^3.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.8"
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.7"
}
}

View File

@ -11,7 +11,7 @@
"enableNonBrowserBranches": true
},
"dependencies": {
"monaco-editor": "^0.52.0",
"monaco-editor": "^0.52.2",
"source-map-js": "^1.2.1"
}
}

View File

@ -1,5 +1,23 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-memo transform > element v-for key expression prefixing + v-memo 1`] = `
"import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, isMemoSame as _isMemoSame, withMemo as _withMemo } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.tableData, (data, __, ___, _cached) => {
const _memo = (_ctx.getLetter(data))
if (_cached && _cached.key === _ctx.getId(data) && _isMemoSame(_cached, _memo)) return _cached
const _item = (_openBlock(), _createElementBlock("span", {
key: _ctx.getId(data)
}))
_item.memo = _memo
return _item
}, _cache, 0), 128 /* KEYED_FRAGMENT */))
]))
}"
`;
exports[`compiler: v-memo transform > on component 1`] = `
"import { resolveComponent as _resolveComponent, createVNode as _createVNode, withMemo as _withMemo, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

View File

@ -8,7 +8,7 @@ return function render(_ctx, _cache) {
const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue
return _cache[0] || (
_setBlockTracking(-1),
_setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
@ -28,7 +28,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
_setBlockTracking(-1, true),
(_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
@ -47,7 +47,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
_setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
@ -66,7 +66,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
_setBlockTracking(-1, true),
(_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]
@ -85,7 +85,7 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (
_setBlockTracking(-1),
_setBlockTracking(-1, true),
(_cache[0] = _createElementVNode("div")).cacheIndex = 0,
_setBlockTracking(1),
_cache[0]

View File

@ -53,4 +53,12 @@ describe('compiler: v-memo transform', () => {
),
).toMatchSnapshot()
})
test('element v-for key expression prefixing + v-memo', () => {
expect(
compile(
`<span v-for="data of tableData" :key="getId(data)" v-memo="getLetter(data)"></span>`,
),
).toMatchSnapshot()
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
"version": "3.5.11",
"version": "3.5.13",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",

View File

@ -418,6 +418,7 @@ export interface CacheExpression extends Node {
index: number
value: JSChildNode
needPauseTracking: boolean
inVOnce: boolean
needArraySpread: boolean
}
@ -774,12 +775,14 @@ export function createCacheExpression(
index: number,
value: JSChildNode,
needPauseTracking: boolean = false,
inVOnce: boolean = false,
): CacheExpression {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
index,
value,
needPauseTracking: needPauseTracking,
inVOnce,
needArraySpread: false,
loc: locStub,
}

View File

@ -1017,7 +1017,9 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
push(`_cache[${node.index}] || (`)
if (needPauseTracking) {
indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
push(`${helper(SET_BLOCK_TRACKING)}(-1`)
if (node.inVOnce) push(`, true`)
push(`),`)
newline()
push(`(`)
}

View File

@ -933,6 +933,10 @@ function getLoc(start: number, end?: number): SourceLocation {
}
}
export function cloneLoc(loc: SourceLocation): SourceLocation {
return getLoc(loc.start.offset, loc.end.offset)
}
function setLocEnd(loc: SourceLocation, end: number) {
loc.end = tokenizer.getPos(end)
loc.source = getSlice(loc.start.offset, end)

View File

@ -116,7 +116,7 @@ export interface TransformContext
addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode | string): void
hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode
cache(exp: JSChildNode, isVNode?: boolean): CacheExpression
cache(exp: JSChildNode, isVNode?: boolean, inVOnce?: boolean): CacheExpression
constantCache: WeakMap<TemplateChildNode, ConstantTypes>
// 2.x Compat only
@ -297,11 +297,12 @@ export function createTransformContext(
identifier.hoisted = exp
return identifier
},
cache(exp, isVNode = false) {
cache(exp, isVNode = false, inVOnce = false) {
const cacheExp = createCacheExpression(
context.cached.length,
exp,
isVNode,
inVOnce,
)
context.cached.push(cacheExp)
return cacheExp

View File

@ -24,7 +24,7 @@ import {
isStaticPropertyKey,
walkIdentifiers,
} from '../babelUtils'
import { advancePositionWithClone, isSimpleIdentifier } from '../utils'
import { advancePositionWithClone, findDir, isSimpleIdentifier } from '../utils'
import {
genPropsAccessExp,
hasOwn,
@ -54,6 +54,7 @@ export const transformExpression: NodeTransform = (node, context) => {
)
} else if (node.type === NodeTypes.ELEMENT) {
// handle directives on element
const memo = findDir(node, 'memo')
for (let i = 0; i < node.props.length; i++) {
const dir = node.props[i]
// do not process for v-on & v-for since they are special handled
@ -65,7 +66,14 @@ export const transformExpression: NodeTransform = (node, context) => {
if (
exp &&
exp.type === NodeTypes.SIMPLE_EXPRESSION &&
!(dir.name === 'on' && arg)
!(dir.name === 'on' && arg) &&
// key has been processed in transformFor(vMemo + vFor)
!(
memo &&
arg &&
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
arg.content === 'key'
)
) {
dir.exp = processExpression(
exp,

View File

@ -12,7 +12,7 @@ import { camelize } from '@vue/shared'
import { CAMELIZE } from '../runtimeHelpers'
import { processExpression } from './transformExpression'
// v-bind without arg is handled directly in ./transformElements.ts due to it affecting
// v-bind without arg is handled directly in ./transformElement.ts due to its affecting
// codegen for the entire props object. This transform here is only for v-bind
// *with* args.
export const transformBind: DirectiveTransform = (dir, _node, context) => {

View File

@ -63,17 +63,27 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
const isTemplate = isTemplateNode(node)
const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`, false, true)
if (keyProp && keyProp.type === NodeTypes.DIRECTIVE && !keyProp.exp) {
const isDirKey = keyProp && keyProp.type === NodeTypes.DIRECTIVE
if (isDirKey && !keyProp.exp) {
// resolve :key shorthand #10882
transformBindShorthand(keyProp, context)
}
const keyExp =
let keyExp =
keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE
? keyProp.value
? createSimpleExpression(keyProp.value.content, true)
: undefined
: keyProp.exp)
if (memo && keyExp && isDirKey) {
if (!__BROWSER__) {
keyProp.exp = keyExp = processExpression(
keyExp as SimpleExpressionNode,
context,
)
}
}
const keyProperty =
keyProp && keyExp ? createObjectProperty(`key`, keyExp) : null

View File

@ -30,6 +30,7 @@ import {
import { ErrorCodes, createCompilerError } from '../errors'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { cloneLoc } from '../parser'
import { CREATE_COMMENT, FRAGMENT } from '../runtimeHelpers'
import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils'
import { PatchFlags } from '@vue/shared'
@ -110,7 +111,7 @@ export function processIf(
const branch = createIfBranch(node, dir)
const ifNode: IfNode = {
type: NodeTypes.IF,
loc: node.loc,
loc: cloneLoc(node.loc),
branches: [branch],
}
context.replaceNode(ifNode)

View File

@ -17,7 +17,7 @@ import { hasScopeRef, isFnExpression, isMemberExpression } from '../utils'
import { TO_HANDLER_KEY } from '../runtimeHelpers'
export interface VOnDirectiveNode extends DirectiveNode {
// v-on without arg is handled directly in ./transformElements.ts due to it affecting
// v-on without arg is handled directly in ./transformElement.ts due to its affecting
// codegen for the entire props object. This transform here is only for v-on
// *with* args.
arg: ExpressionNode

View File

@ -17,7 +17,11 @@ export const transformOnce: NodeTransform = (node, context) => {
context.inVOnce = false
const cur = context.currentNode as ElementNode | IfNode | ForNode
if (cur.codegenNode) {
cur.codegenNode = context.cache(cur.codegenNode, true /* isVNode */)
cur.codegenNode = context.cache(
cur.codegenNode,
true /* isVNode */,
true /* inVOnce */,
)
}
}
}

View File

@ -32,6 +32,33 @@ return function render(_ctx, _cache) {
}"
`;
exports[`stringify static html > serializing template string style 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1)
])))
}"
`;
exports[`stringify static html > should bail for <option> elements with null values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("select", null, [
_createElementVNode("option", { value: null }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" }),
_createElementVNode("option", { value: "1" })
], -1 /* HOISTED */)
])))
}"
`;
exports[`stringify static html > should bail for <option> elements with number values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

View File

@ -162,6 +162,27 @@ describe('stringify static html', () => {
expect(code).toMatchSnapshot()
})
// #12391
test('serializing template string style', () => {
const { ast, code } = compileWithStringify(
`<div><div :style="\`color:red;\`">${repeat(
`<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`,
)
// should be optimized now
expect(ast.cached).toMatchObject([
cachedArrayStaticNodeMatcher(
`<div style="color:red;">${repeat(
`<span class="foo bar">1 + false</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`,
1,
),
])
expect(code).toMatchSnapshot()
})
test('escape', () => {
const { ast, code } = compileWithStringify(
`<div><div>${repeat(
@ -470,6 +491,17 @@ describe('stringify static html', () => {
expect(code).toMatchSnapshot()
})
test('should bail for <option> elements with null values', () => {
const { ast, code } = compileWithStringify(
`<div><select><option :value="null" />${repeat(
`<option value="1" />`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select></div>`,
)
expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
expect(code).toMatchSnapshot()
})
test('eligible content (elements > 20) + non-eligible content', () => {
const { code } = compileWithStringify(
`<div>${repeat(

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
"version": "3.5.11",
"version": "3.5.13",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",

View File

@ -261,8 +261,7 @@ function analyzeNode(node: StringifiableNode): [number, number] | false {
isOptionTag &&
isStaticArgOf(p.arg, 'value') &&
p.exp &&
p.exp.ast &&
p.exp.ast.type !== 'StringLiteral'
!p.exp.isStatic
) {
return bail()
}

View File

@ -233,6 +233,33 @@ export default /*@__PURE__*/_defineComponent({
return { }
}
})"
`;
exports[`defineProps > w/ extends intersection type 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Foo = {
x?: number;
};
interface Props extends Foo {
z: number
y: string
}
export default /*@__PURE__*/_defineComponent({
props: {
z: { type: Number, required: true },
y: { type: String, required: true },
x: { type: Number, required: false }
},
setup(__props: any, { expose: __expose }) {
__expose();
return { }
}
@ -268,6 +295,31 @@ export default /*@__PURE__*/_defineComponent({
return { }
}
})"
`;
exports[`defineProps > w/ intersection type 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Foo = {
x?: number;
};
type Bar = {
y: string;
};
export default /*@__PURE__*/_defineComponent({
props: {
x: { type: Number, required: false },
y: { type: String, required: true }
},
setup(__props: any, { expose: __expose }) {
__expose();
return { }
}

View File

@ -320,3 +320,22 @@ return { rest }
}"
`;
exports[`sfc reactive props destructure > with TSInstantiationExpression 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Foo = <T extends string | number>(data: T) => void
export default /*@__PURE__*/_defineComponent({
props: {
value: { type: Function }
},
setup(__props: any) {
const foo = __props.value<123>
return () => {}
}
})"
`;

View File

@ -261,6 +261,51 @@ const props = defineProps({ foo: String })
})
})
test('w/ extends intersection type', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
type Foo = {
x?: number;
};
interface Props extends Foo {
z: number
y: string
}
defineProps<Props>()
</script>
`)
assertCode(content)
expect(content).toMatch(`z: { type: Number, required: true }`)
expect(content).toMatch(`y: { type: String, required: true }`)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS,
y: BindingTypes.PROPS,
z: BindingTypes.PROPS,
})
})
test('w/ intersection type', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
type Foo = {
x?: number;
};
type Bar = {
y: string;
};
defineProps<Foo & Bar>()
</script>
`)
assertCode(content)
expect(content).toMatch(`y: { type: String, required: true }`)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS,
y: BindingTypes.PROPS,
})
})
test('w/ exported interface', () => {
const { content, bindings } = compile(`
<script setup lang="ts">

View File

@ -198,6 +198,21 @@ describe('sfc reactive props destructure', () => {
}`)
})
test('with TSInstantiationExpression', () => {
const { content } = compile(
`
<script setup lang="ts">
type Foo = <T extends string | number>(data: T) => void
const { value } = defineProps<{ value: Foo }>()
const foo = value<123>
</script>
`,
{ isProd: true },
)
assertCode(content)
expect(content).toMatch(`const foo = __props.value<123>`)
})
test('aliasing', () => {
const { content, bindings } = compile(`
<script setup>

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.5.11",
"version": "3.5.13",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@ -49,7 +49,7 @@
"@vue/shared": "workspace:*",
"estree-walker": "catalog:",
"magic-string": "catalog:",
"postcss": "^8.4.47",
"postcss": "^8.4.49",
"source-map-js": "catalog:"
},
"devDependencies": {
@ -58,10 +58,10 @@
"hash-sum": "^2.0.0",
"lru-cache": "10.1.0",
"merge-source-map": "^1.1.0",
"minimatch": "~9.0.5",
"postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.2",
"minimatch": "~10.0.1",
"postcss-modules": "^6.0.1",
"postcss-selector-parser": "^7.0.0",
"pug": "^3.0.3",
"sass": "^1.79.4"
"sass": "^1.83.1"
}
}

View File

@ -10,6 +10,7 @@ import type {
import { walk } from 'estree-walker'
import {
BindingTypes,
TS_NODE_TYPES,
extractIdentifiers,
isFunctionType,
isInDestructureAssignment,
@ -240,10 +241,7 @@ export function transformDestructuredProps(
if (
parent &&
parent.type.startsWith('TS') &&
parent.type !== 'TSAsExpression' &&
parent.type !== 'TSNonNullExpression' &&
parent.type !== 'TSSatisfiesExpression' &&
parent.type !== 'TSTypeAssertion'
!TS_NODE_TYPES.includes(parent.type)
) {
return this.skip()
}

View File

@ -189,8 +189,7 @@ function rewriteSelector(
// global: replace with inner selector and do not inject [id].
// ::v-global(.foo) -> .foo
if (value === ':global' || value === '::v-global') {
selectorRoot.insertAfter(selector, n.nodes[0])
selectorRoot.removeChild(selector)
selector.replaceWith(n.nodes[0])
return false
}
}

View File

@ -23,28 +23,48 @@ export interface StylePreprocessorResults {
// .scss/.sass processor
const scss: StylePreprocessor = (source, map, options, load = require) => {
const nodeSass = load('sass')
const finalOptions = {
...options,
data: getSource(source, options.filename, options.additionalData),
file: options.filename,
outFile: options.filename,
sourceMap: !!map,
}
const nodeSass: typeof import('sass') = load('sass')
const { compileString, renderSync } = nodeSass
const data = getSource(source, options.filename, options.additionalData)
let css: string
let dependencies: string[]
let sourceMap: any
try {
const result = nodeSass.renderSync(finalOptions)
const dependencies = result.stats.includedFiles
if (map) {
return {
code: result.css.toString(),
map: merge(map, JSON.parse(result.map.toString())),
errors: [],
dependencies,
}
if (compileString) {
const { pathToFileURL, fileURLToPath }: typeof import('url') = load('url')
const result = compileString(data, {
...options,
url: pathToFileURL(options.filename),
sourceMap: !!map,
})
css = result.css
dependencies = result.loadedUrls.map(url => fileURLToPath(url))
sourceMap = map ? result.sourceMap! : undefined
} else {
const result = renderSync({
...options,
data,
file: options.filename,
outFile: options.filename,
sourceMap: !!map,
})
css = result.css.toString()
dependencies = result.stats.includedFiles
sourceMap = map ? JSON.parse(result.map!.toString()) : undefined
}
return { code: result.css.toString(), errors: [], dependencies }
if (map) {
return {
code: css,
errors: [],
dependencies,
map: merge(map, sourceMap!),
}
}
return { code: css, errors: [], dependencies }
} catch (e: any) {
return { code: '', errors: [e], dependencies: [] }
}

View File

@ -337,6 +337,39 @@ describe('ssr: element', () => {
`)
})
test('custom dir with v-text', () => {
expect(getCompiledString(`<div v-xxx v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('custom dir with v-text and normal attrs', () => {
expect(getCompiledString(`<div class="test" v-xxx v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({ class: "test" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('mulptiple custom dirs with v-text', () => {
expect(getCompiledString(`<div v-xxx v-yyy v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps(_ssrGetDirectiveProps(_ctx, _directive_xxx), _ssrGetDirectiveProps(_ctx, _directive_yyy)))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('custom dir with object v-bind', () => {
expect(getCompiledString(`<div v-bind="x" v-xxx />`))
.toMatchInlineSnapshot(`

View File

@ -52,6 +52,52 @@ describe('ssr: v-model', () => {
}"
`)
expect(
compileWithWrapper(
`<select v-model="model"><option v-for="i in items" :value="i"></option></select>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}><select><!--[-->\`)
_ssrRenderList(_ctx.items, (i) => {
_push(\`<option\${
_ssrRenderAttr("value", i)
}\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, i)
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
}></option>\`)
})
_push(\`<!--]--></select></div>\`)
}"
`)
expect(
compileWithWrapper(
`<select v-model="model"><option v-if="true" :value="i"></option></select>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttr: _ssrRenderAttr, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}><select>\`)
if (true) {
_push(\`<option\${
_ssrRenderAttr("value", _ctx.i)
}\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.model))
? _ssrLooseContain(_ctx.model, _ctx.i)
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
}></option>\`)
} else {
_push(\`<!---->\`)
}
_push(\`</select></div>\`)
}"
`)
expect(
compileWithWrapper(
`<select multiple v-model="model"><option value="1" selected></option><option value="2"></option></select>`,

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
"version": "3.5.11",
"version": "3.5.13",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",

View File

@ -28,6 +28,7 @@ import {
createSequenceExpression,
createSimpleExpression,
createTemplateLiteral,
findDir,
hasDynamicKeyVBind,
isStaticArgOf,
isStaticExp,
@ -164,24 +165,28 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
]
}
} else if (directives.length && !node.children.length) {
const tempId = `_temp${context.temps++}`
propsExp.arguments = [
createAssignmentExpression(
createSimpleExpression(tempId, false),
mergedProps,
),
]
rawChildrenMap.set(
node,
createConditionalExpression(
createSimpleExpression(`"textContent" in ${tempId}`, false),
createCallExpression(context.helper(SSR_INTERPOLATE), [
createSimpleExpression(`${tempId}.textContent`, false),
]),
createSimpleExpression(`${tempId}.innerHTML ?? ''`, false),
false,
),
)
// v-text directive has higher priority than the merged props
const vText = findDir(node, 'text')
if (!vText) {
const tempId = `_temp${context.temps++}`
propsExp.arguments = [
createAssignmentExpression(
createSimpleExpression(tempId, false),
mergedProps,
),
]
rawChildrenMap.set(
node,
createConditionalExpression(
createSimpleExpression(`"textContent" in ${tempId}`, false),
createCallExpression(context.helper(SSR_INTERPOLATE), [
createSimpleExpression(`${tempId}.textContent`, false),
]),
createSimpleExpression(`${tempId}.innerHTML ?? ''`, false),
false,
),
)
}
}
if (needTagForRuntime) {

View File

@ -5,6 +5,7 @@ import {
type ExpressionNode,
NodeTypes,
type PlainElementNode,
type TemplateChildNode,
createCallExpression,
createConditionalExpression,
createDOMCompilerError,
@ -162,11 +163,18 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)]
} else if (node.tag === 'select') {
node.children.forEach(child => {
if (child.type === NodeTypes.ELEMENT) {
processOption(child as PlainElementNode)
}
})
const processChildren = (children: TemplateChildNode[]) => {
children.forEach(child => {
if (child.type === NodeTypes.ELEMENT) {
processOption(child as PlainElementNode)
} else if (child.type === NodeTypes.FOR) {
processChildren(child.children)
} else if (child.type === NodeTypes.IF) {
child.branches.forEach(b => processChildren(b.children))
}
})
}
processChildren(node.children)
} else {
context.onError(
createDOMCompilerError(

View File

@ -1,5 +1,10 @@
import { bench, describe } from 'vitest'
import { type ComputedRef, type Ref, computed, effect, ref } from '../src'
import type { ComputedRef, Ref } from '../src'
import { computed, effect, ref } from '../dist/reactivity.esm-browser.prod'
declare module '../dist/reactivity.esm-browser.prod' {
function computed(...args: any[]): any
}
describe('computed', () => {
bench('create computed', () => {

View File

@ -1,5 +1,6 @@
import { bench, describe } from 'vitest'
import { type Ref, effect, ref } from '../src'
import type { Ref } from '../src'
import { effect, ref } from '../dist/reactivity.esm-browser.prod'
describe('effect', () => {
{

View File

@ -1,5 +1,9 @@
import { bench } from 'vitest'
import { effect, reactive, shallowReadArray } from '../src'
import {
effect,
reactive,
shallowReadArray,
} from '../dist/reactivity.esm-browser.prod'
for (let amount = 1e1; amount < 1e4; amount *= 10) {
{

View File

@ -1,5 +1,6 @@
import { bench } from 'vitest'
import { type ComputedRef, computed, reactive } from '../src'
import type { ComputedRef } from '../src'
import { computed, reactive } from '../dist/reactivity.esm-browser.prod'
function createMap(obj: Record<string, any>) {
const map = new Map()

View File

@ -1,5 +1,5 @@
import { bench } from 'vitest'
import { reactive } from '../src'
import { reactive } from '../dist/reactivity.esm-browser.prod'
bench('create reactive obj', () => {
reactive({ a: 1 })

View File

@ -1,5 +1,5 @@
import { bench, describe } from 'vitest'
import { ref } from '../src/index'
import { ref } from '../dist/reactivity.esm-browser.prod'
describe('ref', () => {
bench('create ref', () => {

View File

@ -1107,4 +1107,36 @@ describe('reactivity/computed', () => {
end.prop4.value,
]).toMatchObject([-2, -4, 2, 3])
})
test('performance when removing dependencies from deeply nested computeds', () => {
const base = ref(1)
const trigger = ref(true)
const computeds: ComputedRef<number>[] = []
const LAYERS = 30
for (let i = 0; i < LAYERS; i++) {
const earlier = [...computeds]
computeds.push(
computed(() => {
return base.value + earlier.reduce((sum, c) => sum + c.value, 0)
}),
)
}
const tail = computed(() =>
trigger.value ? computeds[computeds.length - 1].value : 0,
)
const t0 = performance.now()
expect(tail.value).toBe(2 ** (LAYERS - 1))
const t1 = performance.now()
expect(t1 - t0).toBeLessThan(process.env.CI ? 100 : 30)
trigger.value = false
expect(tail.value).toBe(0)
const t2 = performance.now()
expect(t2 - t1).toBeLessThan(process.env.CI ? 100 : 30)
})
})

View File

@ -176,7 +176,7 @@ describe('reactivity/effect/scope', () => {
expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
expect(scope.effects.length).toBe(1)
expect(scope.effects.length).toBe(0)
counter.num = 7
expect(dummy).toBe(0)
@ -322,4 +322,44 @@ describe('reactivity/effect/scope', () => {
scope.resume()
expect(fnSpy).toHaveBeenCalledTimes(3)
})
test('removing a watcher while stopping its effectScope', async () => {
const count = ref(0)
const scope = effectScope()
let watcherCalls = 0
let cleanupCalls = 0
scope.run(() => {
const stop1 = watch(count, () => {
watcherCalls++
})
watch(count, (val, old, onCleanup) => {
watcherCalls++
onCleanup(() => {
cleanupCalls++
stop1()
})
})
watch(count, () => {
watcherCalls++
})
})
expect(watcherCalls).toBe(0)
expect(cleanupCalls).toBe(0)
count.value++
await nextTick()
expect(watcherCalls).toBe(3)
expect(cleanupCalls).toBe(0)
scope.stop()
count.value++
await nextTick()
expect(watcherCalls).toBe(3)
expect(cleanupCalls).toBe(1)
expect(scope.effects.length).toBe(0)
expect(scope.cleanups.length).toBe(0)
})
})

View File

@ -409,4 +409,14 @@ describe('reactivity/reactive', () => {
e.effect.stop()
expect(targetMap.get(obj)?.get('x')).toBeFalsy()
})
test('should trigger reactivity when Map key is undefined', () => {
const map = reactive(new Map())
const c = computed(() => map.get(void 0))
expect(c.value).toBe(void 0)
map.set(void 0, 1)
expect(c.value).toBe(1)
})
})

View File

@ -51,6 +51,7 @@ describe('reactivity/reactive/Array', () => {
const raw = {}
const arr = reactive([{}, {}])
arr.push(raw)
expect(arr.indexOf(raw)).toBe(2)
expect(arr.indexOf(raw, 3)).toBe(-1)
expect(arr.includes(raw)).toBe(true)
@ -89,6 +90,84 @@ describe('reactivity/reactive/Array', () => {
expect(index).toBe(1)
})
// only non-existent reactive will try to search by using its raw value
describe('Array identity methods should not be called more than necessary', () => {
const identityMethods = ['includes', 'indexOf', 'lastIndexOf'] as const
function instrumentArr(rawTarget: any[]) {
identityMethods.forEach(key => {
const spy = vi.fn(rawTarget[key] as any)
rawTarget[key] = spy
})
}
function searchValue(target: any[], ...args: unknown[]) {
return identityMethods.map(key => (target[key] as any)(...args))
}
function unInstrumentArr(rawTarget: any[]) {
identityMethods.forEach(key => {
;(rawTarget[key] as any).mockClear()
// relink to prototype method
rawTarget[key] = Array.prototype[key] as any
})
}
function expectHaveBeenCalledTimes(rawTarget: any[], times: number) {
identityMethods.forEach(key => {
expect(rawTarget[key]).toHaveBeenCalledTimes(times)
})
}
test('should be called once with a non-existent raw value', () => {
const reactiveArr = reactive([])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, {})
expectHaveBeenCalledTimes(toRaw(reactiveArr), 1)
expect(searchResult).toStrictEqual([false, -1, -1])
unInstrumentArr(toRaw(reactiveArr))
})
test('should be called once with an existent reactive value', () => {
const existReactiveValue = reactive({})
const reactiveArr = reactive([existReactiveValue, existReactiveValue])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, existReactiveValue)
expectHaveBeenCalledTimes(toRaw(reactiveArr), 1)
expect(searchResult).toStrictEqual([true, 0, 1])
unInstrumentArr(toRaw(reactiveArr))
})
test('should be called twice with a non-existent reactive value', () => {
const reactiveArr = reactive([])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, reactive({}))
expectHaveBeenCalledTimes(toRaw(reactiveArr), 2)
expect(searchResult).toStrictEqual([false, -1, -1])
unInstrumentArr(toRaw(reactiveArr))
})
test('should be called twice with a non-existent reactive value, but the raw value exists', () => {
const existRaw = {}
const reactiveArr = reactive([existRaw, existRaw])
instrumentArr(toRaw(reactiveArr))
const searchResult = searchValue(reactiveArr, reactive(existRaw))
expectHaveBeenCalledTimes(toRaw(reactiveArr), 2)
expect(searchResult).toStrictEqual([true, 0, 1])
unInstrumentArr(toRaw(reactiveArr))
})
})
test('delete on Array should not trigger length dependency', () => {
const arr = reactive([1, 2, 3])
const fn = vi.fn()

View File

@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
"version": "3.5.11",
"version": "3.5.13",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",

View File

@ -53,6 +53,8 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
) {}
get(target: Target, key: string | symbol, receiver: object): any {
if (key === ReactiveFlags.SKIP) return target[ReactiveFlags.SKIP]
const isReadonly = this._isReadonly,
isShallow = this._isShallow
if (key === ReactiveFlags.IS_REACTIVE) {

View File

@ -8,7 +8,14 @@ import {
} from './reactive'
import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared'
import {
capitalize,
extend,
hasChanged,
hasOwn,
isMap,
toRawType,
} from '@vue/shared'
import { warn } from './warning'
type CollectionTypes = IterableCollections | WeakCollections
@ -23,152 +30,6 @@ const toShallow = <T extends unknown>(value: T): T => value
const getProto = <T extends CollectionTypes>(v: T): any =>
Reflect.getPrototypeOf(v)
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false,
) {
// #1772: readonly(reactive(Map)) should return readonly + reactive version
// of the value
target = target[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
}
const { has } = getProto(rawTarget)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
// #3602 readonly(reactive(Map))
// ensure that the nested reactive `Map` can do tracking for itself
target.get(key)
}
}
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!isReadonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
}
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
function size(target: IterableCollections, isReadonly = false) {
target = target[ReactiveFlags.RAW]
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
function add(this: SetTypes, value: unknown, _isShallow = false) {
if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
}
function set(this: MapTypes, key: unknown, value: unknown, _isShallow = false) {
if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
}
function deleteEntry(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = target.clear()
if (hadItems) {
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
function createForEach(isReadonly: boolean, isShallow: boolean) {
return function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown,
) {
const observed = this
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
@ -232,74 +93,158 @@ function createReadonlyMethod(type: TriggerOpTypes): Function {
type Instrumentations = Record<string | symbol, Function | number>
function createInstrumentations() {
const mutableInstrumentations: Instrumentations = {
function createInstrumentations(
readonly: boolean,
shallow: boolean,
): Instrumentations {
const instrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key)
// #1772: readonly(reactive(Map)) should return readonly + reactive version
// of the value
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!readonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
}
const { has } = getProto(rawTarget)
const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
// #3602 readonly(reactive(Map))
// ensure that the nested reactive `Map` can do tracking for itself
target.get(key)
}
},
get size() {
return size(this as unknown as IterableCollections)
const target = (this as unknown as IterableCollections)[ReactiveFlags.RAW]
!readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
},
has(this: CollectionTypes, key: unknown): boolean {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (!readonly) {
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
}
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
},
forEach(this: IterableCollections, callback: Function, thisArg?: unknown) {
const observed = this
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive
!readonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false),
}
const shallowInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key, false, true)
},
get size() {
return size(this as unknown as IterableCollections)
},
has,
add(this: SetTypes, value: unknown) {
return add.call(this, value, true)
},
set(this: MapTypes, key: unknown, value: unknown) {
return set.call(this, key, value, true)
},
delete: deleteEntry,
clear,
forEach: createForEach(false, true),
}
extend(
instrumentations,
readonly
? {
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
}
: {
add(this: SetTypes, value: unknown) {
if (!shallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
},
set(this: MapTypes, key: unknown, value: unknown) {
if (!shallow && !isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const { has, get } = getProto(target)
const readonlyInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key, true)
},
get size() {
return size(this as unknown as IterableCollections, true)
},
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, false),
}
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const shallowReadonlyInstrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
return get(this, key, true, true)
},
get size() {
return size(this as unknown as IterableCollections, true)
},
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, true),
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
},
delete(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
},
clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = target.clear()
if (hadItems) {
trigger(
target,
TriggerOpTypes.CLEAR,
undefined,
undefined,
oldTarget,
)
}
return result
},
},
)
const iteratorMethods = [
'keys',
@ -309,39 +254,14 @@ function createInstrumentations() {
] as const
iteratorMethods.forEach(method => {
mutableInstrumentations[method] = createIterableMethod(method, false, false)
readonlyInstrumentations[method] = createIterableMethod(method, true, false)
shallowInstrumentations[method] = createIterableMethod(method, false, true)
shallowReadonlyInstrumentations[method] = createIterableMethod(
method,
true,
true,
)
instrumentations[method] = createIterableMethod(method, readonly, shallow)
})
return [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations,
]
return instrumentations
}
const [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations,
] = /* @__PURE__*/ createInstrumentations()
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
const instrumentations = createInstrumentations(isReadonly, shallow)
return (
target: CollectionTypes,

View File

@ -340,7 +340,7 @@ export function trigger(
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
if (key !== void 0 || depsMap.has(void 0)) {
run(depsMap.get(key))
}

View File

@ -426,23 +426,24 @@ function removeSub(link: Link, soft = false) {
nextSub.prevSub = prevSub
link.nextSub = undefined
}
if (dep.subs === link) {
// was previous tail, point new tail to prev
dep.subs = prevSub
}
if (__DEV__ && dep.subsHead === link) {
// was previous head, point new head to next
dep.subsHead = nextSub
}
if (!dep.subs && dep.computed) {
// if computed, unsubscribe it from all its deps so this computed and its
// value can be GCed
dep.computed.flags &= ~EffectFlags.TRACKING
for (let l = dep.computed.deps; l; l = l.nextDep) {
// here we are only "soft" unsubscribing because the computed still keeps
// referencing the deps and the dep should not decrease its sub count
removeSub(l, true)
if (dep.subs === link) {
// was previous tail, point new tail to prev
dep.subs = prevSub
if (!prevSub && dep.computed) {
// if computed, unsubscribe it from all its deps so this computed and its
// value can be GCed
dep.computed.flags &= ~EffectFlags.TRACKING
for (let l = dep.computed.deps; l; l = l.nextDep) {
// here we are only "soft" unsubscribing because the computed still keeps
// referencing the deps and the dep should not decrease its sub count
removeSub(l, true)
}
}
}

View File

@ -8,6 +8,10 @@ export class EffectScope {
* @internal
*/
private _active = true
/**
* @internal track `on` calls, allow `on` call multiple times
*/
private _on = 0
/**
* @internal
*/
@ -99,12 +103,16 @@ export class EffectScope {
}
}
prevScope: EffectScope | undefined
/**
* This should only be called on non-detached scopes
* @internal
*/
on(): void {
activeEffectScope = this
if (++this._on === 1) {
this.prevScope = activeEffectScope
activeEffectScope = this
}
}
/**
@ -112,23 +120,33 @@ export class EffectScope {
* @internal
*/
off(): void {
activeEffectScope = this.parent
if (this._on > 0 && --this._on === 0) {
activeEffectScope = this.prevScope
this.prevScope = undefined
}
}
stop(fromParent?: boolean): void {
if (this._active) {
this._active = false
let i, l
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop()
}
this.effects.length = 0
for (i = 0, l = this.cleanups.length; i < l; i++) {
this.cleanups[i]()
}
this.cleanups.length = 0
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].stop(true)
}
this.scopes.length = 0
}
// nested scope, dereference from parent to avoid memory leaks
if (!this.detached && this.parent && !fromParent) {
// optimized O(1) removal
@ -139,7 +157,6 @@ export class EffectScope {
}
}
this.parent = undefined
this._active = false
}
}
}

View File

@ -213,7 +213,7 @@ export function watch(
const scope = getCurrentScope()
const watchHandle: WatchHandle = () => {
effect.stop()
if (scope) {
if (scope && scope.active) {
remove(scope.effects, effect)
}
}

View File

@ -25,13 +25,13 @@ import {
} from '@vue/runtime-test'
import {
type DebuggerEvent,
EffectFlags,
ITERATE_KEY,
type Ref,
type ShallowRef,
TrackOpTypes,
TriggerOpTypes,
effectScope,
onScopeDispose,
shallowReactive,
shallowRef,
toRef,
@ -1341,7 +1341,7 @@ describe('api: watch', () => {
await nextTick()
await nextTick()
expect(instance!.scope.effects[0].flags & EffectFlags.ACTIVE).toBeFalsy()
expect(instance!.scope.effects.length).toBe(0)
})
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
@ -1983,4 +1983,31 @@ describe('api: watch', () => {
expect(spy1).toHaveBeenCalled()
expect(spy2).toHaveBeenCalled()
})
// #12631
test('this.$watch w/ onScopeDispose', () => {
const onCleanup = vi.fn()
const toggle = ref(true)
const Comp = defineComponent({
render() {},
created(this: any) {
this.$watch(
() => 1,
function () {},
)
onScopeDispose(onCleanup)
},
})
const App = defineComponent({
render() {
return toggle.value ? h(Comp) : null
},
})
const root = nodeOps.createElement('div')
createApp(App).mount(root)
expect(onCleanup).toBeCalledTimes(0)
})
})

View File

@ -333,6 +333,30 @@ describe('component props', () => {
})
})
//#12011
test('replace camelize with hyphenate to handle props key', () => {
const Comp = {
props: {
hasB4BProp: { type: Boolean, required: true },
},
setup() {
return () => null
},
}
render(
h('div', {}, [
h(Comp, {
'has-b-4-b-prop': true,
}),
h(Comp, {
'has-b4-b-prop': true,
}),
]),
nodeOps.createElement('div'),
)
expect(`Missing required prop: "hasB4BProp"`).not.toHaveBeenWarned()
})
test('warn props mutation', () => {
let instance: ComponentInternalInstance
let setupProps: any

View File

@ -87,6 +87,49 @@ describe('renderer: teleport', () => {
`</div>`,
)
})
test('update before mounted with defer', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const show = ref(false)
const foo = ref('foo')
const Header = {
props: { foo: String },
setup(props: any) {
return () => h('div', props.foo)
},
}
const Footer = {
setup() {
foo.value = 'bar'
return () => h('div', 'Footer')
},
}
createDOMApp({
render() {
return show.value
? [
h(
Teleport,
{ to: '#targetId', defer: true },
h(Header, { foo: foo.value }),
),
h(Footer),
h('div', { id: 'targetId' }),
]
: [h('div')]
},
}).mount(root)
expect(root.innerHTML).toMatchInlineSnapshot(`"<div></div>"`)
show.value = true
await nextTick()
expect(root.innerHTML).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end--><div>Footer</div><div id="targetId"><div>bar</div></div>"`,
)
})
})
function runSharedTests(deferMode: boolean) {

View File

@ -32,6 +32,12 @@ describe('renderSlot', () => {
expect(vnode.key).toBe('foo')
})
it('should allow symbol values for slot prop key', () => {
const key = Symbol()
const vnode = renderSlot({ default: () => [h('div')] }, 'default', { key })
expect(vnode.key).toBe('_default')
})
it('should render slot fallback', () => {
const vnode = renderSlot({}, 'default', { key: 'foo' }, () => ['fallback'])
expect(vnode.children).toEqual(['fallback'])

View File

@ -8,6 +8,7 @@ import {
defineAsyncComponent,
defineComponent,
h,
onServerPrefetch,
useId,
} from 'vue'
import { renderToString } from '@vue/server-renderer'
@ -145,6 +146,40 @@ describe('useId', () => {
expect(await getOutput(() => factory(16, 0))).toBe(expected)
})
test('components with serverPrefetch', async () => {
const factory = (): ReturnType<TestCaseFactory> => {
const SPOne = defineComponent({
setup() {
onServerPrefetch(() => {})
return () => h(BasicComponentWithUseId)
},
})
const SPTwo = defineComponent({
render() {
return h(BasicComponentWithUseId)
},
})
const app = createApp({
setup() {
const id1 = useId()
const id2 = useId()
return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)]
},
})
return [app, []]
}
const expected =
'v-0 v-1 ' + // root
'v-0-0 v-0-1 ' + // inside first async subtree
'v-2 v-3' // inside second async subtree
// assert different async resolution order does not affect id stable-ness
expect(await getOutput(() => factory())).toBe(expected)
expect(await getOutput(() => factory())).toBe(expected)
})
test('async setup()', async () => {
const factory = (
delay1: number,

View File

@ -153,10 +153,10 @@ describe('useModel', () => {
const compRender = vi.fn()
const Comp = defineComponent({
props: ['fooBar'],
emits: ['update:fooBar'],
props: ['foo-bar'],
emits: ['update:foo-bar'],
setup(props) {
foo = useModel(props, 'fooBar')
foo = useModel(props, 'foo-bar')
return () => {
compRender()
return foo.value
@ -192,10 +192,10 @@ describe('useModel', () => {
const compRender = vi.fn()
const Comp = defineComponent({
props: ['fooBar'],
emits: ['update:fooBar'],
props: ['foo-bar'],
emits: ['update:foo-bar'],
setup(props) {
foo = useModel(props, 'fooBar')
foo = useModel(props, 'foo-bar')
return () => {
compRender()
return foo.value

View File

@ -21,6 +21,7 @@ import {
h,
nextTick,
onMounted,
onServerPrefetch,
openBlock,
reactive,
ref,
@ -518,6 +519,45 @@ describe('SSR hydration', () => {
)
})
test('with data-allow-mismatch component when using onServerPrefetch', async () => {
const Comp = {
template: `
<div>Comp2</div>
`,
}
let foo: any
const App = {
setup() {
const flag = ref(true)
foo = () => {
flag.value = false
}
onServerPrefetch(() => (flag.value = false))
return { flag }
},
components: {
Comp,
},
template: `
<span data-allow-mismatch>
<Comp v-if="flag"></Comp>
</span>
`,
}
// hydrate
const container = document.createElement('div')
container.innerHTML = await renderToString(h(App))
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
'<span data-allow-mismatch=""><div>Comp2</div></span>',
)
foo()
await nextTick()
expect(container.innerHTML).toBe(
'<span data-allow-mismatch=""><!--v-if--></span>',
)
})
test('Teleport unmount (full integration)', async () => {
const Comp1 = {
template: `
@ -1284,6 +1324,84 @@ describe('SSR hydration', () => {
resolve({})
})
//#12362
test('nested async wrapper', async () => {
const Toggle = defineAsyncComponent(
() =>
new Promise(r => {
r(
defineComponent({
setup(_, { slots }) {
const show = ref(false)
onMounted(() => {
nextTick(() => {
show.value = true
})
})
return () =>
withDirectives(
h('div', null, [renderSlot(slots, 'default')]),
[[vShow, show.value]],
)
},
}) as any,
)
}),
)
const Wrapper = defineAsyncComponent(() => {
return new Promise(r => {
r(
defineComponent({
render(this: any) {
return renderSlot(this.$slots, 'default')
},
}) as any,
)
})
})
const count = ref(0)
const fn = vi.fn()
const Child = {
setup() {
onMounted(() => {
fn()
count.value++
})
return () => h('div', count.value)
},
}
const App = {
render() {
return h(Toggle, null, {
default: () =>
h(Wrapper, null, {
default: () =>
h(Wrapper, null, {
default: () => h(Child),
}),
}),
})
},
}
const root = document.createElement('div')
root.innerHTML = await renderToString(h(App))
expect(root.innerHTML).toMatchInlineSnapshot(
`"<div style="display:none;"><!--[--><!--[--><!--[--><div>0</div><!--]--><!--]--><!--]--></div>"`,
)
createSSRApp(App).mount(root)
await nextTick()
await nextTick()
expect(root.innerHTML).toMatchInlineSnapshot(
`"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
)
expect(fn).toBeCalledTimes(1)
})
test('unmount async wrapper before load (fragment)', async () => {
let resolve: any
const AsyncComp = defineAsyncComponent(
@ -1613,6 +1731,36 @@ describe('SSR hydration', () => {
`)
})
test('Suspense + transition appear', async () => {
const { vnode, container } = mountWithHydration(
`<template><div>foo</div></template>`,
() =>
h(Suspense, {}, () =>
h(
Transition,
{ appear: true },
{
default: () => h('div', 'foo'),
},
),
),
)
expect(vnode.el).toBe(container.firstChild)
// wait for hydration to finish
await new Promise(r => setTimeout(r))
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="v-enter-from v-enter-active"
>
foo
</div>
`)
await nextTick()
expect(vnode.el).toBe(container.firstChild)
})
// #10607
test('update component stable slot (prod + optimized mode)', async () => {
__DEV__ = false

View File

@ -6,6 +6,7 @@
import {
Fragment,
type FunctionalComponent,
Teleport,
createBlock,
createCommentVNode,
createElementBlock,
@ -391,6 +392,26 @@ describe('attribute fallthrough', () => {
expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
})
it('should warn when fallthrough fails on teleport root node', () => {
const Parent = {
render() {
return h(Child, { class: 'parent' })
},
}
const root = document.createElement('div')
const Child = defineComponent({
render() {
return h(Teleport, { to: root }, h('div'))
},
})
document.body.appendChild(root)
render(h(Parent), root)
expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
})
it('should dedupe same listeners when $attrs is used during render', () => {
const click = vi.fn()
const count = ref(0)

View File

@ -17,6 +17,7 @@ import {
serializeInner as inner,
nextTick,
nodeOps,
onBeforeMount,
onBeforeUnmount,
onUnmounted,
openBlock,
@ -1199,7 +1200,7 @@ describe('renderer: optimized mode', () => {
createBlock('div', null, [
createVNode('div', null, [
cache[0] ||
(setBlockTracking(-1),
(setBlockTracking(-1, true),
((cache[0] = createVNode('div', null, [
createVNode(Child),
])).cacheIndex = 0),
@ -1233,4 +1234,64 @@ describe('renderer: optimized mode', () => {
expect(inner(root)).toBe('<!--v-if-->')
expect(spyUnmounted).toHaveBeenCalledTimes(2)
})
// #12371
test('unmount children when the user calls a compiled slot', async () => {
const beforeMountSpy = vi.fn()
const beforeUnmountSpy = vi.fn()
const Child = {
setup() {
onBeforeMount(beforeMountSpy)
onBeforeUnmount(beforeUnmountSpy)
return () => 'child'
},
}
const Wrapper = {
setup(_: any, { slots }: SetupContext) {
return () => (
openBlock(),
createElementBlock('section', null, [
(openBlock(),
createElementBlock('div', { key: 1 }, [
createTextVNode(slots.header!() ? 'foo' : 'bar', 1 /* TEXT */),
renderSlot(slots, 'content'),
])),
])
)
},
}
const show = ref(false)
const app = createApp({
render() {
return show.value
? (openBlock(),
createBlock(Wrapper, null, {
header: withCtx(() => [createVNode({})]),
content: withCtx(() => [createVNode(Child)]),
_: 1,
}))
: createCommentVNode('v-if', true)
},
})
app.mount(root)
expect(inner(root)).toMatchInlineSnapshot(`"<!--v-if-->"`)
expect(beforeMountSpy).toHaveBeenCalledTimes(0)
expect(beforeUnmountSpy).toHaveBeenCalledTimes(0)
show.value = true
await nextTick()
expect(inner(root)).toMatchInlineSnapshot(
`"<section><div>foochild</div></section>"`,
)
expect(beforeMountSpy).toHaveBeenCalledTimes(1)
show.value = false
await nextTick()
expect(inner(root)).toBe('<!--v-if-->')
expect(beforeUnmountSpy).toHaveBeenCalledTimes(1)
})
})

View File

@ -1,4 +1,6 @@
import {
KeepAlive,
defineAsyncComponent,
defineComponent,
h,
nextTick,
@ -217,6 +219,7 @@ describe('api: template refs', () => {
}
render(h(Comp), root)
expect(state.refKey).toBe(root.children[0])
expect('Template ref "refKey" used on a non-ref value').toHaveBeenWarned()
})
test('multiple root refs', () => {
@ -537,4 +540,68 @@ describe('api: template refs', () => {
'<div><div>[object Object],[object Object]</div><ul><li>2</li><li>3</li></ul></div>',
)
})
test('with async component which nested in KeepAlive', async () => {
const AsyncComp = defineAsyncComponent(
() =>
new Promise(resolve =>
setTimeout(() =>
resolve(
defineComponent({
setup(_, { expose }) {
expose({
name: 'AsyncComp',
})
return () => h('div')
},
}) as any,
),
),
),
)
const Comp = defineComponent({
setup(_, { expose }) {
expose({
name: 'Comp',
})
return () => h('div')
},
})
const toggle = ref(false)
const instanceRef = ref<any>(null)
const App = {
render: () => {
return h(KeepAlive, () =>
toggle.value
? h(AsyncComp, { ref: instanceRef })
: h(Comp, { ref: instanceRef }),
)
},
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(instanceRef.value.name).toBe('Comp')
// switch to async component
toggle.value = true
await nextTick()
expect(instanceRef.value).toBe(null)
await new Promise(r => setTimeout(r))
expect(instanceRef.value.name).toBe('AsyncComp')
// switch back to normal component
toggle.value = false
await nextTick()
expect(instanceRef.value.name).toBe('Comp')
// switch to async component again
toggle.value = true
await nextTick()
expect(instanceRef.value.name).toBe('AsyncComp')
})
})

View File

@ -629,7 +629,7 @@ describe('vnode', () => {
const vnode =
(openBlock(),
createBlock('div', null, [
setBlockTracking(-1),
setBlockTracking(-1, true),
(vnode1 = (openBlock(), createBlock('div'))),
setBlockTracking(1),
vnode1,

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-core",
"version": "3.5.11",
"version": "3.5.13",
"description": "@vue/runtime-core",
"main": "index.js",
"module": "dist/runtime-core.esm-bundler.js",

View File

@ -265,7 +265,7 @@ export function defineComponent<
Mixin,
Extends,
ResolvedEmits,
RuntimeEmitsKeys,
{},
{},
false,
InjectOptions,

View File

@ -149,9 +149,7 @@ export function defineEmits() {
return null as any
}
export type ComponentTypeEmits =
| ((...args: any[]) => any)
| Record<string, any[]>
export type ComponentTypeEmits = ((...args: any[]) => any) | Record<string, any>
type RecordToUnion<T extends Record<string, any>> = T[keyof T]
@ -331,21 +329,23 @@ type PropsWithDefaults<
T,
Defaults extends InferDefaults<T>,
BKeys extends keyof T,
> = Readonly<MappedOmit<T, keyof Defaults>> & {
readonly [K in keyof Defaults as K extends keyof T
? K
: never]-?: K extends keyof T
? Defaults[K] extends undefined
? IfAny<Defaults[K], NotUndefined<T[K]>, T[K]>
: NotUndefined<T[K]>
: never
} & {
readonly [K in BKeys]-?: K extends keyof Defaults
? Defaults[K] extends undefined
? boolean | undefined
: boolean
: boolean
}
> = T extends unknown
? Readonly<MappedOmit<T, keyof Defaults>> & {
readonly [K in keyof Defaults as K extends keyof T
? K
: never]-?: K extends keyof T
? Defaults[K] extends undefined
? IfAny<Defaults[K], NotUndefined<T[K]>, T[K]>
: NotUndefined<T[K]>
: never
} & {
readonly [K in BKeys]-?: K extends keyof Defaults
? Defaults[K] extends undefined
? boolean | undefined
: boolean
: boolean
}
: never
/**
* Vue `<script setup>` compiler macro for providing props default values when

View File

@ -856,11 +856,10 @@ function setupStatefulComponent(
// 2. call setup()
const { setup } = Component
if (setup) {
pauseTracking()
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
const reset = setCurrentInstance(instance)
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
@ -870,12 +869,16 @@ function setupStatefulComponent(
setupContext,
],
)
const isAsyncSetup = isPromise(setupResult)
resetTracking()
reset()
if (isPromise(setupResult)) {
// async setup, mark as async boundary for useId()
if (!isAsyncWrapper(instance)) markAsyncBoundary(instance)
if ((isAsyncSetup || instance.sp) && !isAsyncWrapper(instance)) {
// async setup / serverPrefetch, mark as async boundary for useId()
markAsyncBoundary(instance)
}
if (isAsyncSetup) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
@ -1003,7 +1006,7 @@ export function finishComponentSetup(
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
Component.template ||
resolveMergedOptions(instance).template
(__FEATURE_OPTIONS_API__ && resolveMergedOptions(instance).template)
if (template) {
if (__DEV__) {
startMeasure(instance, `compile`)

View File

@ -416,20 +416,20 @@ interface LegacyOptions<
extends?: Extends
// lifecycle
beforeCreate?(): void
created?(): void
beforeMount?(): void
mounted?(): void
beforeUpdate?(): void
updated?(): void
activated?(): void
deactivated?(): void
beforeCreate?(): any
created?(): any
beforeMount?(): any
mounted?(): any
beforeUpdate?(): any
updated?(): any
activated?(): any
deactivated?(): any
/** @deprecated use `beforeUnmount` instead */
beforeDestroy?(): void
beforeUnmount?(): void
beforeDestroy?(): any
beforeUnmount?(): any
/** @deprecated use `unmounted` instead */
destroyed?(): void
unmounted?(): void
destroyed?(): any
unmounted?(): any
renderTracked?: DebuggerHook
renderTriggered?: DebuggerHook
errorCaptured?: ErrorCapturedHook

View File

@ -654,6 +654,7 @@ function validateProps(
) {
const resolvedValues = toRaw(props)
const options = instance.propsOptions[0]
const camelizePropsKey = Object.keys(rawProps).map(key => camelize(key))
for (const key in options) {
let opt = options[key]
if (opt == null) continue
@ -662,7 +663,7 @@ function validateProps(
resolvedValues[key],
opt,
__DEV__ ? shallowReadonly(resolvedValues) : resolvedValues,
!hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key)),
!camelizePropsKey.includes(key),
)
}
}

View File

@ -190,7 +190,7 @@ export function renderComponentRoot(
`Extraneous non-props attributes (` +
`${extraAttrs.join(', ')}) ` +
`were passed to component but could not be automatically inherited ` +
`because component renders fragment or text root nodes.`,
`because component renders fragment or text or teleport root nodes.`,
)
}
if (eventAttrs.length) {

View File

@ -198,8 +198,7 @@ const BaseTransitionImpl: ComponentOptions = {
setTransitionHooks(innerChild, enterHooks)
}
const oldChild = instance.subTree
const oldInnerChild = oldChild && getInnerChild(oldChild)
let oldInnerChild = instance.subTree && getInnerChild(instance.subTree)
// handle mode
if (
@ -208,7 +207,7 @@ const BaseTransitionImpl: ComponentOptions = {
!isSameVNodeType(innerChild, oldInnerChild) &&
recursiveGetSubtree(instance).type !== Comment
) {
const leavingHooks = resolveTransitionHooks(
let leavingHooks = resolveTransitionHooks(
oldInnerChild,
rawProps,
state,
@ -228,6 +227,7 @@ const BaseTransitionImpl: ComponentOptions = {
instance.update()
}
delete leavingHooks.afterLeave
oldInnerChild = undefined
}
return emptyPlaceholder(child)
} else if (mode === 'in-out' && innerChild.type !== Comment) {
@ -238,18 +238,27 @@ const BaseTransitionImpl: ComponentOptions = {
) => {
const leavingVNodesCache = getLeavingNodesForType(
state,
oldInnerChild,
oldInnerChild!,
)
leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
leavingVNodesCache[String(oldInnerChild!.key)] = oldInnerChild!
// early removal callback
el[leaveCbKey] = () => {
earlyRemove()
el[leaveCbKey] = undefined
delete enterHooks.delayedLeave
oldInnerChild = undefined
}
enterHooks.delayedLeave = () => {
delayedLeave()
delete enterHooks.delayedLeave
oldInnerChild = undefined
}
enterHooks.delayedLeave = delayedLeave
}
} else {
oldInnerChild = undefined
}
} else if (oldInnerChild) {
oldInnerChild = undefined
}
return child

View File

@ -147,7 +147,7 @@ export const TeleportImpl = {
}
if (!disabled) {
mount(target, targetAnchor)
updateCssVars(n2)
updateCssVars(n2, false)
}
} else if (__DEV__ && !disabled) {
warn(
@ -160,15 +160,36 @@ export const TeleportImpl = {
if (disabled) {
mount(container, mainAnchor)
updateCssVars(n2)
updateCssVars(n2, true)
}
if (isTeleportDeferred(n2.props)) {
queuePostRenderEffect(mountToTarget, parentSuspense)
queuePostRenderEffect(() => {
mountToTarget()
n2.el!.__isMounted = true
}, parentSuspense)
} else {
mountToTarget()
}
} else {
if (isTeleportDeferred(n2.props) && !n1.el!.__isMounted) {
queuePostRenderEffect(() => {
TeleportImpl.process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals,
)
delete n1.el!.__isMounted
}, parentSuspense)
return
}
// update content
n2.el = n1.el
n2.targetStart = n1.targetStart
@ -267,7 +288,7 @@ export const TeleportImpl = {
)
}
}
updateCssVars(n2)
updateCssVars(n2, disabled)
}
},
@ -389,12 +410,13 @@ function hydrateTeleport(
querySelector,
))
if (target) {
const disabled = isTeleportDisabled(vnode.props)
// if multiple teleports rendered to the same target element, we need to
// pick up from where the last teleport finished instead of the first node
const targetNode =
(target as TeleportTargetElement)._lpa || target.firstChild
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (isTeleportDisabled(vnode.props)) {
if (disabled) {
vnode.anchor = hydrateChildren(
nextSibling(node),
vnode,
@ -446,7 +468,7 @@ function hydrateTeleport(
)
}
}
updateCssVars(vnode)
updateCssVars(vnode, disabled)
}
return vnode.anchor && nextSibling(vnode.anchor as Node)
}
@ -462,13 +484,20 @@ export const Teleport = TeleportImpl as unknown as {
}
}
function updateCssVars(vnode: VNode) {
function updateCssVars(vnode: VNode, isDisabled: boolean) {
// presence of .ut method indicates owner component uses css vars.
// code path here can assume browser environment.
const ctx = vnode.ctx
if (ctx && ctx.ut) {
let node = vnode.targetStart
while (node && node !== vnode.targetAnchor) {
let node, anchor
if (isDisabled) {
node = vnode.el
anchor = vnode.anchor
} else {
node = vnode.targetStart
anchor = vnode.targetAnchor
}
while (node && node !== anchor) {
if (node.nodeType === 1) node.setAttribute('data-v-owner', ctx.uid)
node = node.nextSibling
}

View File

@ -14,7 +14,7 @@ import {
isVNode,
openBlock,
} from '../vnode'
import { PatchFlags, SlotFlags } from '@vue/shared'
import { PatchFlags, SlotFlags, isSymbol } from '@vue/shared'
import { warn } from '../warning'
import { isAsyncWrapper } from '../apiAsyncComponent'
@ -72,15 +72,16 @@ export function renderSlot(
}
openBlock()
const validSlotContent = slot && ensureValidVNode(slot(props))
const slotKey =
props.key ||
// slot content array of a dynamic conditional slot may have a branch
// key attached in the `createSlots` helper, respect that
(validSlotContent && (validSlotContent as any).key)
const rendered = createBlock(
Fragment,
{
key:
(props.key ||
// slot content array of a dynamic conditional slot may have a branch
// key attached in the `createSlots` helper, respect that
(validSlotContent && (validSlotContent as any).key) ||
`_${name}`) +
(slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) +
// #7256 force differentiate fallback content from actual content
(!validSlotContent && fallback ? '_fb' : ''),
},

View File

@ -28,14 +28,14 @@ export function useModel(
return ref() as any
}
if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {
const camelizedName = camelize(name)
if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[camelizedName]) {
warn(`useModel() called with prop "${name}" which is not declared.`)
return ref() as any
}
const camelizedName = camelize(name)
const hyphenatedName = hyphenate(name)
const modifiers = getModelModifiers(props, name)
const modifiers = getModelModifiers(props, camelizedName)
const res = customRef((track, trigger) => {
let localValue: any
@ -43,7 +43,7 @@ export function useModel(
let prevEmittedValue: any
watchSyncEffect(() => {
const propValue = props[name]
const propValue = props[camelizedName]
if (hasChanged(localValue, propValue)) {
localValue = propValue
trigger()

View File

@ -11,7 +11,7 @@ import {
normalizeVNode,
} from './vnode'
import { flushPostFlushCbs } from './scheduler'
import type { ComponentInternalInstance } from './component'
import type { ComponentInternalInstance, ComponentOptions } from './component'
import { invokeDirectiveHook } from './directives'
import { warn } from './warning'
import {
@ -41,6 +41,7 @@ import {
import type { TeleportImpl, TeleportVNode } from './components/Teleport'
import { isAsyncWrapper } from './apiAsyncComponent'
import { isReactive } from '@vue/reactivity'
import { updateHOCHostEl } from './componentRenderUtils'
export type RootHydrateFunction = (
vnode: VNode<Node, Element>,
@ -307,7 +308,10 @@ export function createHydrationFunctions(
// if component is async, it may get moved / unmounted before its
// inner component is loaded, so we need to give it a placeholder
// vnode that matches its adopted DOM.
if (isAsyncWrapper(vnode)) {
if (
isAsyncWrapper(vnode) &&
!(vnode.type as ComponentOptions).__asyncResolved
) {
let subTree
if (isFragmentStart) {
subTree = createVNode(Fragment)
@ -385,7 +389,10 @@ export function createHydrationFunctions(
let needCallTransitionHooks = false
if (isTemplateNode(el)) {
needCallTransitionHooks =
needTransition(parentSuspense, transition) &&
needTransition(
null, // no need check parentSuspense in hydration
transition,
) &&
parentComponent &&
parentComponent.vnode.props &&
parentComponent.vnode.props.appear
@ -713,6 +720,11 @@ export function createHydrationFunctions(
getContainerType(container),
slotScopeIds,
)
// the component vnode's el should be updated when a mismatch occurs.
if (parentComponent) {
parentComponent.vnode.el = vnode.el
updateHOCHostEl(parentComponent, vnode.el)
}
return next
}

View File

@ -1,6 +1,13 @@
import { isString } from '@vue/shared'
import { getGlobalThis, isString } from '@vue/shared'
import { DOMNodeTypes, isComment } from './hydration'
// Polyfills for Safari support
// see https://caniuse.com/requestidlecallback
const requestIdleCallback: Window['requestIdleCallback'] =
getGlobalThis().requestIdleCallback || (cb => setTimeout(cb, 1))
const cancelIdleCallback: Window['cancelIdleCallback'] =
getGlobalThis().cancelIdleCallback || (id => clearTimeout(id))
/**
* A lazy hydration strategy for async components.
* @param hydrate - call this to perform the actual hydration.

View File

@ -15,7 +15,7 @@ import { isRef, toRaw } from '@vue/reactivity'
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import type { SchedulerJob } from './scheduler'
import { queuePostRenderEffect } from './renderer'
import { getComponentPublicInstance } from './component'
import { type ComponentOptions, getComponentPublicInstance } from './component'
import { knownTemplateRefs } from './helpers/useTemplateRef'
/**
@ -42,8 +42,18 @@ export function setRef(
}
if (isAsyncWrapper(vnode) && !isUnmount) {
// when mounting async components, nothing needs to be done,
// because the template ref is forwarded to inner component
// #4999 if an async component already resolved and cached by KeepAlive,
// we need to set the ref to inner component
if (
vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE &&
(vnode.type as ComponentOptions).__asyncResolved &&
vnode.component!.subTree.component
) {
setRef(rawRef, oldRawRef, parentSuspense, vnode.component!.subTree)
}
// otherwise, nothing needs to be done because the template ref
// is forwarded to inner component
return
}
@ -69,8 +79,17 @@ export function setRef(
setupState === EMPTY_OBJ
? () => false
: (key: string) => {
if (__DEV__ && knownTemplateRefs.has(rawSetupState[key] as any)) {
return false
if (__DEV__) {
if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) {
warn(
`Template ref "${key}" used on a non-ref value. ` +
`It will not work in the production build.`,
)
}
if (knownTemplateRefs.has(rawSetupState[key] as any)) {
return false
}
}
return hasOwn(rawSetupState, key)
}

View File

@ -301,7 +301,7 @@ export let isBlockTreeEnabled = 1
*
* ``` js
* _cache[1] || (
* setBlockTracking(-1),
* setBlockTracking(-1, true),
* _cache[1] = createVNode(...),
* setBlockTracking(1),
* _cache[1]
@ -310,11 +310,11 @@ export let isBlockTreeEnabled = 1
*
* @private
*/
export function setBlockTracking(value: number): void {
export function setBlockTracking(value: number, inVOnce = false): void {
isBlockTreeEnabled += value
if (value < 0 && currentBlock) {
if (value < 0 && currentBlock && inVOnce) {
// mark current block so it doesn't take fast path and skip possible
// nested components duriung unmount
// nested components during unmount
currentBlock.hasOnce = true
}
}

View File

@ -396,6 +396,38 @@ describe('defineCustomElement', () => {
expect(e.value).toBe('hi')
})
// #12214
test('Boolean prop with default true', async () => {
const E = defineCustomElement({
props: {
foo: {
type: Boolean,
default: true,
},
},
render() {
return String(this.foo)
},
})
customElements.define('my-el-default-true', E)
container.innerHTML = `<my-el-default-true></my-el-default-true>`
const e = container.childNodes[0] as HTMLElement & { foo: any },
shadowRoot = e.shadowRoot as ShadowRoot
expect(shadowRoot.innerHTML).toBe('true')
e.foo = undefined
await nextTick()
expect(shadowRoot.innerHTML).toBe('true')
e.foo = false
await nextTick()
expect(shadowRoot.innerHTML).toBe('false')
e.foo = null
await nextTick()
expect(shadowRoot.innerHTML).toBe('null')
e.foo = ''
await nextTick()
expect(shadowRoot.innerHTML).toBe('true')
})
test('support direct setup function syntax with extra options', () => {
const E = defineCustomElement(
props => {
@ -1386,4 +1418,39 @@ describe('defineCustomElement', () => {
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe(`false,boolean`)
})
test('hyphenated attr removal', async () => {
const E = defineCustomElement({
props: {
fooBar: {
type: Boolean,
},
},
render() {
return this.fooBar
},
})
customElements.define('el-hyphenated-attr-removal', E)
const toggle = ref(true)
const Comp = {
render() {
return h('el-hyphenated-attr-removal', {
'foo-bar': toggle.value ? '' : null,
})
},
}
render(h(Comp), container)
const el = container.children[0]
expect(el.hasAttribute('foo-bar')).toBe(true)
expect((el as any).outerHTML).toBe(
`<el-hyphenated-attr-removal foo-bar=""></el-hyphenated-attr-removal>`,
)
toggle.value = false
await nextTick()
expect(el.hasAttribute('foo-bar')).toBe(false)
expect((el as any).outerHTML).toBe(
`<el-hyphenated-attr-removal></el-hyphenated-attr-removal>`,
)
})
})

View File

@ -7,6 +7,7 @@ import {
defineCustomElement,
h,
nextTick,
onMounted,
reactive,
ref,
render,
@ -350,6 +351,7 @@ describe('useCssVars', () => {
expect(() => render(h(App), root)).not.toThrow(TypeError)
await nextTick()
expect(target.children.length).toBe(0)
expect(root.children[0].outerHTML.includes('data-v-owner')).toBe(true)
})
test('with string style', async () => {
@ -383,6 +385,44 @@ describe('useCssVars', () => {
}
})
test('with delay mount child', async () => {
const state = reactive({ color: 'red' })
const value = ref(false)
const root = document.createElement('div')
const Child = {
setup() {
onMounted(() => {
const childEl = root.children[0]
expect(getComputedStyle(childEl!).getPropertyValue(`--color`)).toBe(
`red`,
)
})
return () => h('div', { id: 'childId' })
},
}
const App = {
setup() {
useCssVars(() => state)
return () => (value.value ? h(Child) : [h('span')])
},
}
render(h(App), root)
await nextTick()
// css vars use with fallback tree
for (const c of [].slice.call(root.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
}
// mount child
value.value = true
await nextTick()
for (const c of [].slice.call(root.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
}
})
// #8826
test('with custom element', async () => {
const state = reactive({ color: 'red' })
@ -404,4 +444,25 @@ describe('useCssVars', () => {
`<css-vars-ce style="--color: red;"></css-vars-ce>`,
)
})
test('should set vars before child component onMount hook', () => {
const state = reactive({ color: 'red' })
const root = document.createElement('div')
let colorInOnMount
const App = {
setup() {
useCssVars(() => state)
onMounted(() => {
colorInOnMount = (
root.children[0] as HTMLElement
).style.getPropertyValue(`--color`)
})
return () => h('div')
},
}
render(h(App), root)
expect(colorInOnMount).toBe(`red`)
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-dom",
"version": "3.5.11",
"version": "3.5.13",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",

View File

@ -505,6 +505,8 @@ export class VueElement
}
// reflect
if (shouldReflect) {
const ob = this._ob
ob && ob.disconnect()
if (val === true) {
this.setAttribute(hyphenate(key), '')
} else if (typeof val === 'string' || typeof val === 'number') {
@ -512,6 +514,7 @@ export class VueElement
} else if (!val) {
this.removeAttribute(hyphenate(key))
}
ob && ob.observe(this, { attributes: true })
}
}
}

View File

@ -181,7 +181,13 @@ export function resolveTransitionProps(
onAppearCancelled = onEnterCancelled,
} = baseProps
const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
const finishEnter = (
el: Element & { _enterCancelled?: boolean },
isAppear: boolean,
done?: () => void,
isCancelled?: boolean,
) => {
el._enterCancelled = isCancelled
removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
done && done()
@ -240,7 +246,10 @@ export function resolveTransitionProps(
},
onEnter: makeEnterHook(false),
onAppear: makeEnterHook(true),
onLeave(el: Element & { _isLeaving?: boolean }, done) {
onLeave(
el: Element & { _isLeaving?: boolean; _enterCancelled?: boolean },
done,
) {
el._isLeaving = true
const resolve = () => finishLeave(el, done)
addTransitionClass(el, leaveFromClass)
@ -249,9 +258,14 @@ export function resolveTransitionProps(
}
// add *-leave-active class before reflow so in the case of a cancelled enter transition
// the css will not get the final state (#10677)
addTransitionClass(el, leaveActiveClass)
// force reflow so *-leave-from classes immediately take effect (#2593)
forceReflow()
if (!el._enterCancelled) {
// force reflow so *-leave-from classes immediately take effect (#2593)
forceReflow()
addTransitionClass(el, leaveActiveClass)
} else {
addTransitionClass(el, leaveActiveClass)
forceReflow()
}
nextFrame(() => {
if (!el._isLeaving) {
// cancelled
@ -269,11 +283,11 @@ export function resolveTransitionProps(
callHook(onLeave, [el, resolve])
},
onEnterCancelled(el) {
finishEnter(el, false)
finishEnter(el, false, undefined, true)
callHook(onEnterCancelled, [el])
},
onAppearCancelled(el) {
finishEnter(el, true)
finishEnter(el, true, undefined, true)
callHook(onAppearCancelled, [el])
},
onLeaveCancelled(el) {

View File

@ -160,7 +160,7 @@ export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
function setChecked(
el: HTMLInputElement,
{ value }: DirectiveBinding,
{ value, oldValue }: DirectiveBinding,
vnode: VNode,
) {
// store the v-model value on the element so it can be accessed by the
@ -173,6 +173,7 @@ function setChecked(
} else if (isSet(value)) {
checked = value.has(vnode.props!.value)
} else {
if (value === oldValue) return
checked = looseEqual(value, getCheckboxValue(el, true))
}

View File

@ -3,13 +3,14 @@ import {
Static,
type VNode,
getCurrentInstance,
onBeforeMount,
onBeforeUpdate,
onMounted,
onUnmounted,
queuePostFlushCb,
warn,
watchPostEffect,
watch,
} from '@vue/runtime-core'
import { ShapeFlags } from '@vue/shared'
import { NOOP, ShapeFlags } from '@vue/shared'
export const CSS_VAR_TEXT: unique symbol = Symbol(__DEV__ ? 'CSS_VAR_TEXT' : '')
/**
@ -48,11 +49,15 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>): void {
updateTeleports(vars)
}
onBeforeMount(() => {
watchPostEffect(setVars)
// handle cases where child component root is affected
// and triggers reflow in onMounted
onBeforeUpdate(() => {
queuePostFlushCb(setVars)
})
onMounted(() => {
// run setVars synchronously here, but run as post-effect on changes
watch(setVars, NOOP, { flush: 'post' })
const ob = new MutationObserver(setVars)
ob.observe(instance.subTree.el!.parentNode, { childList: true })
onUnmounted(() => ob.disconnect())

View File

@ -8,6 +8,7 @@ export function patchDOMProp(
key: string,
value: any,
parentComponent: any,
attrName?: string,
): void {
// __UNSAFE__
// Reason: potentially setting innerHTML.
@ -106,5 +107,5 @@ export function patchDOMProp(
)
}
}
needRemove && el.removeAttribute(key)
needRemove && el.removeAttribute(attrName || key)
}

View File

@ -62,7 +62,7 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
(el as VueElement)._isVueCE &&
(/[A-Z]/.test(key) || !isString(nextValue))
) {
patchDOMProp(el, camelize(key), nextValue, parentComponent)
patchDOMProp(el, camelize(key), nextValue, parentComponent, key)
} else {
// special case for <input v-model type="checkbox"> with
// :true-value & :false-value

View File

@ -1,6 +1,10 @@
import { bench, describe } from 'vitest'
import { createBuffer } from '../src/render'
import { createBuffer as _createBuffer } from '../src/render'
// move to local const to avoid import access overhead
// https://github.com/vitest-dev/vitest/issues/6903
const createBuffer = _createBuffer
describe('createBuffer', () => {
let stringBuffer = createBuffer()

View File

@ -1,7 +1,11 @@
import { bench, describe } from 'vitest'
import { type SSRBuffer, createBuffer } from '../src/render'
import { unrollBuffer } from '../src/renderToString'
import { unrollBuffer as _unrollBuffer } from '../src/renderToString'
// move to local const to avoid import access overhead
// https://github.com/vitest-dev/vitest/issues/6903
const unrollBuffer = _unrollBuffer
function createSyncBuffer(levels: number, itemsPerLevel: number): SSRBuffer {
const buffer = createBuffer()

Some files were not shown because too many files have changed in this diff Show More