chore: Merge branch 'minor' into vapor

This commit is contained in:
Evan You 2025-01-29 15:37:54 +08:00
commit 674151c9b9
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
34 changed files with 2096 additions and 1903 deletions

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

@ -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.13",
"packageManager": "pnpm@9.12.3",
"packageManager": "pnpm@9.15.4",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
@ -64,40 +64,40 @@
"@babel/parser": "catalog:",
"@babel/types": "catalog:",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.1",
"@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.9.1",
"@swc/core": "^1.10.8",
"@types/hash-sum": "^1.0.2",
"@types/node": "^22.8.7",
"@types/node": "^22.10.7",
"@types/semver": "^7.5.8",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-v8": "^2.1.1",
"@vitest/ui": "^2.1.1",
"@vitest/coverage-v8": "^3.0.2",
"@vitest/ui": "^3.0.2",
"@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.14.0",
"eslint-plugin-import-x": "^4.4.0",
"@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.4.1",
"lodash": "^4.17.21",
"magic-string": "^0.30.12",
"magic-string": "^0.30.17",
"markdown-table": "^3.0.4",
"marked": "13.0.3",
"npm-run-all2": "^7.0.1",
"npm-run-all2": "^7.0.2",
"picocolors": "^1.1.1",
"prettier": "^3.3.3",
"prettier": "^3.4.2",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~23.3.0",
"puppeteer": "~24.1.0",
"rimraf": "^6.0.1",
"rollup": "^4.25.0",
"rollup": "^4.31.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
@ -108,9 +108,9 @@
"todomvc-app-css": "^2.4.3",
"tslib": "^2.8.1",
"typescript": "~5.6.2",
"typescript-eslint": "^8.12.2",
"typescript-eslint": "^8.20.0",
"vite": "catalog:",
"vitest": "^2.1.1"
"vitest": "^3.0.2"
},
"pnpm": {
"peerDependencyRules": {

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

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

View File

@ -12,7 +12,7 @@
},
"dependencies": {
"@vue/compiler-vapor": "workspace:^",
"monaco-editor": "^0.52.0",
"monaco-editor": "^0.52.2",
"source-map-js": "^1.2.1"
}
}

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

@ -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

@ -50,7 +50,7 @@
"@vue/shared": "workspace:*",
"estree-walker": "catalog:",
"magic-string": "catalog:",
"postcss": "^8.4.48",
"postcss": "^8.5.1",
"source-map-js": "catalog:"
},
"devDependencies": {
@ -59,10 +59,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",
"minimatch": "~10.0.1",
"postcss-modules": "^6.0.1",
"postcss-selector-parser": "^7.0.0",
"pug": "^3.0.3",
"sass": "^1.80.6"
"sass": "^1.83.4"
}
}

View File

@ -176,8 +176,6 @@ export function compileScript(
const scriptSetupLang = scriptSetup && scriptSetup.lang
const vapor = sfc.vapor || options.vapor
let refBindings: string[] | undefined
if (!scriptSetup) {
if (!script) {
throw new Error(`[@vue/compiler-sfc] SFC contains no <script> tags.`)
@ -746,12 +744,6 @@ export function compileScript(
for (const key in setupBindings) {
ctx.bindingMetadata[key] = setupBindings[key]
}
// known ref bindings
if (refBindings) {
for (const key of refBindings) {
ctx.bindingMetadata[key] = BindingTypes.SETUP_REF
}
}
// 7. inject `useCssVars` calls
if (

View File

@ -25,8 +25,9 @@ import {
toRaw,
triggerRef,
} from '../src'
import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
import type { ComputedRef, ComputedRefImpl } from '../src/computed'
import { pauseTracking, resetTracking } from '../src/effect'
import { SubscriberFlags } from '../src/system'
describe('reactivity/computed', () => {
it('should return updated value', () => {
@ -409,9 +410,9 @@ describe('reactivity/computed', () => {
a.value++
e.value
expect(e.deps!.dep).toBe(b.dep)
expect(e.deps!.nextDep!.dep).toBe(d.dep)
expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
expect(e.deps!.dep).toBe(b)
expect(e.deps!.nextDep!.dep).toBe(d)
expect(e.deps!.nextDep!.nextDep!.dep).toBe(c)
expect(cSpy).toHaveBeenCalledTimes(2)
a.value++
@ -466,8 +467,12 @@ describe('reactivity/computed', () => {
const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
c2.value
expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
expect(
c1.flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed),
).toBe(0)
expect(
c2.flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed),
).toBe(0)
})
it('should chained computeds dirtyLevel update with first computed effect', () => {

View File

@ -1,3 +1,14 @@
import {
computed,
h,
nextTick,
nodeOps,
ref,
render,
serializeInner,
} from '@vue/runtime-test'
import { ITERATE_KEY, getDepFromReactive } from '../src/dep'
import { onEffectCleanup, pauseTracking, resetTracking } from '../src/effect'
import {
type DebuggerEvent,
type ReactiveEffectRunner,
@ -11,23 +22,7 @@ import {
stop,
toRaw,
} from '../src/index'
import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
import {
computed,
h,
nextTick,
nodeOps,
ref,
render,
serializeInner,
} from '@vue/runtime-test'
import {
endBatch,
onEffectCleanup,
pauseTracking,
resetTracking,
startBatch,
} from '../src/effect'
import { type Dependency, endBatch, startBatch } from '../src/system'
describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
@ -1183,12 +1178,12 @@ describe('reactivity/effect', () => {
})
describe('dep unsubscribe', () => {
function getSubCount(dep: Dep | undefined) {
function getSubCount(dep: Dependency | undefined) {
let count = 0
let sub = dep!.subs
while (sub) {
count++
sub = sub.prevSub
sub = sub.nextSub
}
return count
}

View File

@ -2,6 +2,7 @@ import {
type ComputedRef,
computed,
effect,
effectScope,
reactive,
shallowRef as ref,
toRaw,
@ -19,7 +20,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
}
// #9233
it('should release computed cache', async () => {
it.todo('should release computed cache', async () => {
const src = ref<{} | undefined>({})
// @ts-expect-error ES2021 API
const srcRef = new WeakRef(src.value!)
@ -34,7 +35,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
expect(srcRef.deref()).toBeUndefined()
})
it('should release reactive property dep', async () => {
it.todo('should release reactive property dep', async () => {
const src = reactive({ foo: 1 })
let c: ComputedRef | undefined = computed(() => src.foo)
@ -79,4 +80,36 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
src.foo++
expect(spy).toHaveBeenCalledTimes(2)
})
it('should release computed that untrack by effect', async () => {
const src = ref(0)
// @ts-expect-error ES2021 API
const c = new WeakRef(computed(() => src.value))
const scope = effectScope()
scope.run(() => {
effect(() => c.deref().value)
})
expect(c.deref()).toBeDefined()
scope.stop()
await gc()
expect(c.deref()).toBeUndefined()
})
it('should release computed that untrack by effectScope', async () => {
const src = ref(0)
// @ts-expect-error ES2021 API
const c = new WeakRef(computed(() => src.value))
const scope = effectScope()
scope.run(() => {
c.deref().value
})
expect(c.deref()).toBeDefined()
scope.stop()
await gc()
expect(c.deref()).toBeUndefined()
})
})

View File

@ -1,8 +1,9 @@
import { TrackOpTypes } from './constants'
import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
import { isProxy, isShallow, toRaw, toReactive } from './reactive'
import { ARRAY_ITERATE_KEY, track } from './dep'
import { isArray } from '@vue/shared'
import { TrackOpTypes } from './constants'
import { ARRAY_ITERATE_KEY, track } from './dep'
import { pauseTracking, resetTracking } from './effect'
import { isProxy, isShallow, toRaw, toReactive } from './reactive'
import { endBatch, startBatch } from './system'
/**
* Track array iteration and return:

View File

@ -1,17 +1,26 @@
import { isFunction } from '@vue/shared'
import { hasChanged, isFunction } from '@vue/shared'
import { ReactiveFlags, TrackOpTypes } from './constants'
import { onTrack, setupOnTrigger } from './debug'
import {
type DebuggerEvent,
type DebuggerOptions,
EffectFlags,
type Subscriber,
activeSub,
batch,
refreshComputed,
setActiveSub,
} from './effect'
import { activeEffectScope } from './effectScope'
import type { Ref } from './ref'
import {
type Dependency,
type Link,
type Subscriber,
SubscriberFlags,
endTracking,
link,
processComputedUpdate,
startTracking,
updateDirtyFlag,
} from './system'
import { warn } from './warning'
import { Dep, type Link, globalVersion } from './dep'
import { ReactiveFlags, TrackOpTypes } from './constants'
declare const ComputedRefSymbol: unique symbol
declare const WritableComputedRefSymbol: unique symbol
@ -44,15 +53,21 @@ export interface WritableComputedOptions<T, S = T> {
* @private exported by @vue/reactivity for Vue core use, but not exported from
* the main vue package
*/
export class ComputedRefImpl<T = any> implements Subscriber {
export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
/**
* @internal
*/
_value: any = undefined
/**
* @internal
*/
readonly dep: Dep = new Dep(this)
_value: T | undefined = undefined
// Dependency
subs: Link | undefined = undefined
subsTail: Link | undefined = undefined
// Subscriber
deps: Link | undefined = undefined
depsTail: Link | undefined = undefined
flags: SubscriberFlags = SubscriberFlags.Computed | SubscriberFlags.Dirty
/**
* @internal
*/
@ -63,34 +78,35 @@ export class ComputedRefImpl<T = any> implements Subscriber {
*/
readonly __v_isReadonly: boolean
// TODO isolatedDeclarations ReactiveFlags.IS_READONLY
// A computed is also a subscriber that tracks other deps
/**
* @internal
*/
deps?: Link = undefined
/**
* @internal
*/
depsTail?: Link = undefined
/**
* @internal
*/
flags: EffectFlags = EffectFlags.DIRTY
/**
* @internal
*/
globalVersion: number = globalVersion - 1
/**
* @internal
*/
isSSR: boolean
/**
* @internal
*/
next?: Subscriber = undefined
// for backwards compat
effect: this = this
get effect(): this {
return this
}
// for backwards compat
get dep(): Dependency {
return this
}
// for backwards compat
get _dirty(): boolean {
const flags = this.flags
if (
flags & SubscriberFlags.Dirty ||
(flags & SubscriberFlags.PendingComputed &&
updateDirtyFlag(this, this.flags))
) {
return true
}
return false
}
set _dirty(v: boolean) {
if (v) {
this.flags |= SubscriberFlags.Dirty
} else {
this.flags &= ~(SubscriberFlags.Dirty | SubscriberFlags.PendingComputed)
}
}
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
@ -105,43 +121,28 @@ export class ComputedRefImpl<T = any> implements Subscriber {
constructor(
public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined,
isSSR: boolean,
) {
this[ReactiveFlags.IS_READONLY] = !setter
this.isSSR = isSSR
}
/**
* @internal
*/
notify(): true | void {
this.flags |= EffectFlags.DIRTY
if (
!(this.flags & EffectFlags.NOTIFIED) &&
// avoid infinite self recursion
activeSub !== this
) {
batch(this, true)
return true
} else if (__DEV__) {
// TODO warn
}
}
get value(): T {
const link = __DEV__
? this.dep.track({
const flags = this.flags
if (flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed)) {
processComputedUpdate(this, flags)
}
if (activeSub !== undefined) {
if (__DEV__) {
onTrack(activeSub!, {
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
: this.dep.track()
refreshComputed(this)
// sync version after evaluation
if (link) {
link.version = this.dep.version
}
return this._value
link(this, activeSub)
} else if (activeEffectScope !== undefined) {
link(this, activeEffectScope)
}
return this._value!
}
set value(newValue) {
@ -151,6 +152,28 @@ export class ComputedRefImpl<T = any> implements Subscriber {
warn('Write operation failed: computed value is readonly')
}
}
update(): boolean {
const prevSub = activeSub
setActiveSub(this)
startTracking(this)
try {
const oldValue = this._value
const newValue = this.fn(oldValue)
if (hasChanged(oldValue, newValue)) {
this._value = newValue
return true
}
return false
} finally {
setActiveSub(prevSub)
endTracking(this)
}
}
}
if (__DEV__) {
setupOnTrigger(ComputedRefImpl)
}
/**
@ -209,7 +232,7 @@ export function computed<T>(
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, isSSR)
const cRef = new ComputedRefImpl(getter, setter)
if (__DEV__ && debugOptions && !isSSR) {
cRef.onTrack = debugOptions.onTrack

View File

@ -0,0 +1,83 @@
import { extend } from '@vue/shared'
import type { DebuggerEventExtraInfo, ReactiveEffectOptions } from './effect'
import { type Link, type Subscriber, SubscriberFlags } from './system'
export const triggerEventInfos: DebuggerEventExtraInfo[] = []
export function onTrack(
sub: Link['sub'],
debugInfo: DebuggerEventExtraInfo,
): void {
if (!__DEV__) {
throw new Error(
`Internal error: onTrack should be called only in development.`,
)
}
if ((sub as ReactiveEffectOptions).onTrack) {
;(sub as ReactiveEffectOptions).onTrack!(
extend(
{
effect: sub,
},
debugInfo,
),
)
}
}
export function onTrigger(sub: Link['sub']): void {
if (!__DEV__) {
throw new Error(
`Internal error: onTrigger should be called only in development.`,
)
}
if ((sub as ReactiveEffectOptions).onTrigger) {
const debugInfo = triggerEventInfos[triggerEventInfos.length - 1]
;(sub as ReactiveEffectOptions).onTrigger!(
extend(
{
effect: sub,
},
debugInfo,
),
)
}
}
export function setupOnTrigger(target: { new (...args: any[]): any }): void {
if (!__DEV__) {
throw new Error(
`Internal error: setupOnTrigger should be called only in development.`,
)
}
Object.defineProperty(target.prototype, 'onTrigger', {
get() {
return this._onTrigger
},
set(val) {
if (!this._onTrigger) setupFlagsHandler(this)
this._onTrigger = val
},
})
}
function setupFlagsHandler(target: Subscriber): void {
;(target as any)._flags = target.flags
Object.defineProperty(target, 'flags', {
get() {
return (target as any)._flags
},
set(value) {
if (
!(
(target as any)._flags &
(SubscriberFlags.PendingComputed | SubscriberFlags.Dirty)
) &&
!!(value & (SubscriberFlags.PendingComputed | SubscriberFlags.Dirty))
) {
onTrigger(this)
}
;(target as any)._flags = value
},
})
}

View File

@ -1,227 +1,34 @@
import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
import type { ComputedRefImpl } from './computed'
import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
import { type TrackOpTypes, TriggerOpTypes } from './constants'
import { onTrack, triggerEventInfos } from './debug'
import { activeSub } from './effect'
import {
type DebuggerEventExtraInfo,
EffectFlags,
type Subscriber,
activeSub,
type Dependency,
type Link,
endBatch,
shouldTrack,
link,
propagate,
startBatch,
} from './effect'
} from './system'
/**
* Incremented every time a reactive change happens
* This is used to give computed a fast path to avoid re-compute when nothing
* has changed.
*/
export let globalVersion = 0
/**
* Represents a link between a source (Dep) and a subscriber (Effect or Computed).
* Deps and subs have a many-to-many relationship - each link between a
* dep and a sub is represented by a Link instance.
*
* A Link is also a node in two doubly-linked lists - one for the associated
* sub to track all its deps, and one for the associated dep to track all its
* subs.
*
* @internal
*/
export class Link {
/**
* - Before each effect run, all previous dep links' version are reset to -1
* - During the run, a link's version is synced with the source dep on access
* - After the run, links with version -1 (that were never used) are cleaned
* up
*/
version: number
/**
* Pointers for doubly-linked lists
*/
nextDep?: Link
prevDep?: Link
nextSub?: Link
prevSub?: Link
prevActiveLink?: Link
class Dep implements Dependency {
_subs: Link | undefined = undefined
subsTail: Link | undefined = undefined
constructor(
public sub: Subscriber,
public dep: Dep,
) {
this.version = dep.version
this.nextDep =
this.prevDep =
this.nextSub =
this.prevSub =
this.prevActiveLink =
undefined
}
}
private map: KeyToDepMap,
private key: unknown,
) {}
/**
* @internal
*/
export class Dep {
version = 0
/**
* Link between this dep and the current active effect
*/
activeLink?: Link = undefined
/**
* Doubly linked list representing the subscribing effects (tail)
*/
subs?: Link = undefined
/**
* Doubly linked list representing the subscribing effects (head)
* DEV only, for invoking onTrigger hooks in correct order
*/
subsHead?: Link
/**
* For object property deps cleanup
*/
map?: KeyToDepMap = undefined
key?: unknown = undefined
/**
* Subscriber counter
*/
sc: number = 0
constructor(public computed?: ComputedRefImpl | undefined) {
if (__DEV__) {
this.subsHead = undefined
}
get subs(): Link | undefined {
return this._subs
}
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return
set subs(value: Link | undefined) {
this._subs = value
if (value === undefined) {
this.map.delete(this.key)
}
let link = this.activeLink
if (link === undefined || link.sub !== activeSub) {
link = this.activeLink = new Link(activeSub, this)
// add the link to the activeEffect as a dep (as tail)
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link
} else {
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
}
addSub(link)
} else if (link.version === -1) {
// reused from last run - already a sub, just sync version
link.version = this.version
// If this dep has a next, it means it's not at the tail - move it to the
// tail. This ensures the effect's dep list is in the order they are
// accessed during evaluation.
if (link.nextDep) {
const next = link.nextDep
next.prevDep = link.prevDep
if (link.prevDep) {
link.prevDep.nextDep = next
}
link.prevDep = activeSub.depsTail
link.nextDep = undefined
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
// this was the head - point to the new head
if (activeSub.deps === link) {
activeSub.deps = next
}
}
}
if (__DEV__ && activeSub.onTrack) {
activeSub.onTrack(
extend(
{
effect: activeSub,
},
debugInfo,
),
)
}
return link
}
trigger(debugInfo?: DebuggerEventExtraInfo): void {
this.version++
globalVersion++
this.notify(debugInfo)
}
notify(debugInfo?: DebuggerEventExtraInfo): void {
startBatch()
try {
if (__DEV__) {
// subs are notified and batched in reverse-order and then invoked in
// original order at the end of the batch, but onTrigger hooks should
// be invoked in original order here.
for (let head = this.subsHead; head; head = head.nextSub) {
if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
head.sub.onTrigger(
extend(
{
effect: head.sub,
},
debugInfo,
),
)
}
}
}
for (let link = this.subs; link; link = link.prevSub) {
if (link.sub.notify()) {
// if notify() returns `true`, this is a computed. Also call notify
// on its dep - it's called here instead of inside computed's notify
// in order to reduce call stack depth.
;(link.sub as ComputedRefImpl).dep.notify()
}
}
} finally {
endBatch()
}
}
}
function addSub(link: Link) {
link.dep.sc++
if (link.sub.flags & EffectFlags.TRACKING) {
const computed = link.dep.computed
// computed getting its first subscriber
// enable tracking + lazily subscribe to all its deps
if (computed && !link.dep.subs) {
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
for (let l = computed.deps; l; l = l.nextDep) {
addSub(l)
}
}
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
if (__DEV__ && link.dep.subsHead === undefined) {
link.dep.subsHead = link
}
link.dep.subs = link
}
}
@ -254,26 +61,23 @@ export const ARRAY_ITERATE_KEY: unique symbol = Symbol(
* @param key - Identifier of the reactive property to track.
*/
export function track(target: object, type: TrackOpTypes, key: unknown): void {
if (shouldTrack && activeSub) {
if (activeSub !== undefined) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Dep()))
dep.map = depsMap
dep.key = key
depsMap.set(key, (dep = new Dep(depsMap, key)))
}
if (__DEV__) {
dep.track({
onTrack(activeSub!, {
target,
type,
key,
})
} else {
dep.track()
}
link(dep, activeSub!)
}
}
@ -296,14 +100,13 @@ export function trigger(
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
globalVersion++
return
}
const run = (dep: Dep | undefined) => {
if (dep) {
const run = (dep: Dependency | undefined) => {
if (dep !== undefined && dep.subs !== undefined) {
if (__DEV__) {
dep.trigger({
triggerEventInfos.push({
target,
type,
key,
@ -311,8 +114,10 @@ export function trigger(
oldValue,
oldTarget,
})
} else {
dep.trigger()
}
propagate(dep.subs)
if (__DEV__) {
triggerEventInfos.pop()
}
}
}
@ -385,7 +190,7 @@ export function trigger(
export function getDepFromReactive(
object: any,
key: string | number | symbol,
): Dep | undefined {
): Dependency | undefined {
const depMap = targetMap.get(object)
return depMap && depMap.get(key)
}

View File

@ -1,8 +1,15 @@
import { extend, hasChanged } from '@vue/shared'
import type { ComputedRefImpl } from './computed'
import { extend } from '@vue/shared'
import type { TrackOpTypes, TriggerOpTypes } from './constants'
import { type Link, globalVersion } from './dep'
import { setupOnTrigger } from './debug'
import { activeEffectScope } from './effectScope'
import {
type Link,
type Subscriber,
SubscriberFlags,
endTracking,
startTracking,
updateDirtyFlag,
} from './system'
import { warn } from './warning'
export type EffectScheduler = (...args: any[]) => any
@ -27,7 +34,6 @@ export interface DebuggerOptions {
export interface ReactiveEffectOptions extends DebuggerOptions {
scheduler?: EffectScheduler
allowRecurse?: boolean
onStop?: () => void
}
@ -36,78 +42,27 @@ export interface ReactiveEffectRunner<T = any> {
effect: ReactiveEffect
}
export let activeSub: Subscriber | undefined
export enum EffectFlags {
/**
* ReactiveEffect only
*/
ACTIVE = 1 << 0,
RUNNING = 1 << 1,
TRACKING = 1 << 2,
NOTIFIED = 1 << 3,
DIRTY = 1 << 4,
ALLOW_RECURSE = 1 << 5,
PAUSED = 1 << 6,
ALLOW_RECURSE = 1 << 7,
PAUSED = 1 << 8,
NOTIFIED = 1 << 9,
STOP = 1 << 10,
}
/**
* Subscriber is a type that tracks (or subscribes to) a list of deps.
*/
export interface Subscriber extends DebuggerOptions {
/**
* Head of the doubly linked list representing the deps
* @internal
*/
deps?: Link
/**
* Tail of the same list
* @internal
*/
depsTail?: Link
/**
* @internal
*/
flags: EffectFlags
/**
* @internal
*/
next?: Subscriber
/**
* returning `true` indicates it's a computed that needs to call notify
* on its dep too
* @internal
*/
notify(): true | void
}
export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
// Subscriber
deps: Link | undefined = undefined
depsTail: Link | undefined = undefined
flags: number = SubscriberFlags.Effect
const pausedQueueEffects = new WeakSet<ReactiveEffect>()
export class ReactiveEffect<T = any>
implements Subscriber, ReactiveEffectOptions
{
/**
* @internal
*/
deps?: Link = undefined
/**
* @internal
*/
depsTail?: Link = undefined
/**
* @internal
*/
flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
/**
* @internal
*/
next?: Subscriber = undefined
/**
* @internal
*/
cleanup?: () => void = undefined
scheduler?: EffectScheduler = undefined
onStop?: () => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
@ -118,50 +73,53 @@ export class ReactiveEffect<T = any>
}
}
get active(): boolean {
return !(this.flags & EffectFlags.STOP)
}
pause(): void {
if (!(this.flags & EffectFlags.PAUSED)) {
this.flags |= EffectFlags.PAUSED
}
}
resume(): void {
if (this.flags & EffectFlags.PAUSED) {
const flags = this.flags
if (flags & EffectFlags.PAUSED) {
this.flags &= ~EffectFlags.PAUSED
if (pausedQueueEffects.has(this)) {
pausedQueueEffects.delete(this)
this.trigger()
}
if (flags & EffectFlags.NOTIFIED) {
this.flags &= ~EffectFlags.NOTIFIED
this.notify()
}
}
/**
* @internal
*/
notify(): void {
if (
this.flags & EffectFlags.RUNNING &&
!(this.flags & EffectFlags.ALLOW_RECURSE)
) {
return
const flags = this.flags
if (!(flags & EffectFlags.PAUSED)) {
this.scheduler()
} else {
this.flags |= EffectFlags.NOTIFIED
}
if (!(this.flags & EffectFlags.NOTIFIED)) {
batch(this)
}
scheduler(): void {
if (this.dirty) {
this.run()
}
}
run(): T {
// TODO cleanupEffect
if (!(this.flags & EffectFlags.ACTIVE)) {
if (!this.active) {
// stopped during cleanup
return this.fn()
}
this.flags |= EffectFlags.RUNNING
cleanupEffect(this)
prepareDeps(this)
const prevEffect = activeSub
const prevShouldTrack = shouldTrack
activeSub = this
shouldTrack = true
const prevSub = activeSub
setActiveSub(this)
startTracking(this)
try {
return this.fn()
@ -172,300 +130,42 @@ export class ReactiveEffect<T = any>
'this is likely a Vue internal bug.',
)
}
cleanupDeps(this)
activeSub = prevEffect
shouldTrack = prevShouldTrack
this.flags &= ~EffectFlags.RUNNING
setActiveSub(prevSub)
endTracking(this)
if (
this.flags & SubscriberFlags.Recursed &&
this.flags & EffectFlags.ALLOW_RECURSE
) {
this.flags &= ~SubscriberFlags.Recursed
this.notify()
}
}
}
stop(): void {
if (this.flags & EffectFlags.ACTIVE) {
for (let link = this.deps; link; link = link.nextDep) {
removeSub(link)
}
this.deps = this.depsTail = undefined
if (this.active) {
startTracking(this)
endTracking(this)
cleanupEffect(this)
this.onStop && this.onStop()
this.flags &= ~EffectFlags.ACTIVE
}
}
trigger(): void {
if (this.flags & EffectFlags.PAUSED) {
pausedQueueEffects.add(this)
} else if (this.scheduler) {
this.scheduler()
} else {
this.runIfDirty()
}
}
/**
* @internal
*/
runIfDirty(): void {
if (isDirty(this)) {
this.run()
this.flags |= EffectFlags.STOP
}
}
get dirty(): boolean {
return isDirty(this)
}
}
/**
* For debugging
*/
// function printDeps(sub: Subscriber) {
// let d = sub.deps
// let ds = []
// while (d) {
// ds.push(d)
// d = d.nextDep
// }
// return ds.map(d => ({
// id: d.id,
// prev: d.prevDep?.id,
// next: d.nextDep?.id,
// }))
// }
let batchDepth = 0
let batchedSub: Subscriber | undefined
let batchedComputed: Subscriber | undefined
export function batch(sub: Subscriber, isComputed = false): void {
sub.flags |= EffectFlags.NOTIFIED
if (isComputed) {
sub.next = batchedComputed
batchedComputed = sub
return
}
sub.next = batchedSub
batchedSub = sub
}
/**
* @internal
*/
export function startBatch(): void {
batchDepth++
}
/**
* Run batched effects when all batches have ended
* @internal
*/
export function endBatch(): void {
if (--batchDepth > 0) {
return
}
if (batchedComputed) {
let e: Subscriber | undefined = batchedComputed
batchedComputed = undefined
while (e) {
const next: Subscriber | undefined = e.next
e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED
e = next
}
}
let error: unknown
while (batchedSub) {
let e: Subscriber | undefined = batchedSub
batchedSub = undefined
while (e) {
const next: Subscriber | undefined = e.next
e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED
if (e.flags & EffectFlags.ACTIVE) {
try {
// ACTIVE flag is effect-only
;(e as ReactiveEffect).trigger()
} catch (err) {
if (!error) error = err
}
}
e = next
}
}
if (error) throw error
}
function prepareDeps(sub: Subscriber) {
// Prepare deps for tracking, starting from the head
for (let link = sub.deps; link; link = link.nextDep) {
// set all previous deps' (if any) version to -1 so that we can track
// which ones are unused after the run
link.version = -1
// store previous active sub if link was being used in another context
link.prevActiveLink = link.dep.activeLink
link.dep.activeLink = link
}
}
function cleanupDeps(sub: Subscriber) {
// Cleanup unsued deps
let head
let tail = sub.depsTail
let link = tail
while (link) {
const prev = link.prevDep
if (link.version === -1) {
if (link === tail) tail = prev
// unused - remove it from the dep's subscribing effect list
removeSub(link)
// also remove it from this effect's dep list
removeDep(link)
} else {
// The new head is the last node seen which wasn't removed
// from the doubly-linked list
head = link
}
// restore previous active link if any
link.dep.activeLink = link.prevActiveLink
link.prevActiveLink = undefined
link = prev
}
// set the new head & tail
sub.deps = head
sub.depsTail = tail
}
function isDirty(sub: Subscriber): boolean {
for (let link = sub.deps; link; link = link.nextDep) {
const flags = this.flags
if (
link.dep.version !== link.version ||
(link.dep.computed &&
(refreshComputed(link.dep.computed) ||
link.dep.version !== link.version))
flags & SubscriberFlags.Dirty ||
(flags & SubscriberFlags.PendingComputed && updateDirtyFlag(this, flags))
) {
return true
}
}
// @ts-expect-error only for backwards compatibility where libs manually set
// this flag - e.g. Pinia's testing module
if (sub._dirty) {
return true
}
return false
}
/**
* Returning false indicates the refresh failed
* @internal
*/
export function refreshComputed(computed: ComputedRefImpl): undefined {
if (
computed.flags & EffectFlags.TRACKING &&
!(computed.flags & EffectFlags.DIRTY)
) {
return
}
computed.flags &= ~EffectFlags.DIRTY
// Global version fast path when no reactive changes has happened since
// last refresh.
if (computed.globalVersion === globalVersion) {
return
}
computed.globalVersion = globalVersion
const dep = computed.dep
computed.flags |= EffectFlags.RUNNING
// In SSR there will be no render effect, so the computed has no subscriber
// and therefore tracks no deps, thus we cannot rely on the dirty check.
// Instead, computed always re-evaluate and relies on the globalVersion
// fast path above for caching.
if (
dep.version > 0 &&
!computed.isSSR &&
computed.deps &&
!isDirty(computed)
) {
computed.flags &= ~EffectFlags.RUNNING
return
}
const prevSub = activeSub
const prevShouldTrack = shouldTrack
activeSub = computed
shouldTrack = true
try {
prepareDeps(computed)
const value = computed.fn(computed._value)
if (dep.version === 0 || hasChanged(value, computed._value)) {
computed._value = value
dep.version++
}
} catch (err) {
dep.version++
throw err
} finally {
activeSub = prevSub
shouldTrack = prevShouldTrack
cleanupDeps(computed)
computed.flags &= ~EffectFlags.RUNNING
}
}
function removeSub(link: Link, soft = false) {
const { dep, prevSub, nextSub } = link
if (prevSub) {
prevSub.nextSub = nextSub
link.prevSub = undefined
}
if (nextSub) {
nextSub.prevSub = prevSub
link.nextSub = undefined
}
if (__DEV__ && dep.subsHead === link) {
// was previous head, point new head to next
dep.subsHead = nextSub
}
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)
}
}
}
if (!soft && !--dep.sc && dep.map) {
// #11979
// property dep no longer has effect subscribers, delete it
// this mostly is for the case where an object is kept in memory but only a
// subset of its properties is tracked at one time
dep.map.delete(dep.key)
}
}
function removeDep(link: Link) {
const { prevDep, nextDep } = link
if (prevDep) {
prevDep.nextDep = nextDep
link.prevDep = undefined
}
if (nextDep) {
nextDep.prevDep = prevDep
link.nextDep = undefined
}
if (__DEV__) {
setupOnTrigger(ReactiveEffect)
}
export interface ReactiveEffectRunner<T = any> {
@ -505,34 +205,53 @@ export function stop(runner: ReactiveEffectRunner): void {
runner.effect.stop()
}
/**
* @internal
*/
export let shouldTrack = true
const trackStack: boolean[] = []
const resetTrackingStack: (Subscriber | undefined)[] = []
/**
* Temporarily pauses tracking.
*/
export function pauseTracking(): void {
trackStack.push(shouldTrack)
shouldTrack = false
resetTrackingStack.push(activeSub)
activeSub = undefined
}
/**
* Re-enables effect tracking (if it was paused).
*/
export function enableTracking(): void {
trackStack.push(shouldTrack)
shouldTrack = true
const isPaused = activeSub === undefined
if (!isPaused) {
// Add the current active effect to the trackResetStack so it can be
// restored by calling resetTracking.
resetTrackingStack.push(activeSub)
} else {
// Add a placeholder to the trackResetStack so we can it can be popped
// to restore the previous active effect.
resetTrackingStack.push(undefined)
for (let i = resetTrackingStack.length - 1; i >= 0; i--) {
if (resetTrackingStack[i] !== undefined) {
activeSub = resetTrackingStack[i]
break
}
}
}
}
/**
* Resets the previous global effect tracking state.
*/
export function resetTracking(): void {
const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
if (__DEV__ && resetTrackingStack.length === 0) {
warn(
`resetTracking() was called when there was no active tracking ` +
`to reset.`,
)
}
if (resetTrackingStack.length) {
activeSub = resetTrackingStack.pop()!
} else {
activeSub = undefined
}
}
/**
@ -561,7 +280,7 @@ export function onEffectCleanup(fn: () => void, failSilently = false): void {
function cleanupEffect(e: ReactiveEffect) {
const { cleanup } = e
e.cleanup = undefined
if (cleanup) {
if (cleanup !== undefined) {
// run cleanup without active effect
const prevSub = activeSub
activeSub = undefined
@ -572,3 +291,9 @@ function cleanupEffect(e: ReactiveEffect) {
}
}
}
export let activeSub: Subscriber | undefined = undefined
export function setActiveSub(sub: Subscriber | undefined): void {
activeSub = sub
}

View File

@ -1,13 +1,20 @@
import type { ReactiveEffect } from './effect'
import { EffectFlags, type ReactiveEffect } from './effect'
import {
type Link,
type Subscriber,
endTracking,
startTracking,
} from './system'
import { warn } from './warning'
export let activeEffectScope: EffectScope | undefined
export class EffectScope {
/**
* @internal
*/
private _active = true
export class EffectScope implements Subscriber {
// Subscriber: In order to collect orphans computeds
deps: Link | undefined = undefined
depsTail: Link | undefined = undefined
flags: number = 0
/**
* @internal track `on` calls, allow `on` call multiple times
*/
@ -21,8 +28,6 @@ export class EffectScope {
*/
cleanups: (() => void)[] = []
private _isPaused = false
/**
* only assigned by undetached scope
* @internal
@ -51,12 +56,12 @@ export class EffectScope {
}
get active(): boolean {
return this._active
return !(this.flags & EffectFlags.STOP)
}
pause(): void {
if (this._active) {
this._isPaused = true
if (!(this.flags & EffectFlags.PAUSED)) {
this.flags |= EffectFlags.PAUSED
let i, l
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
@ -73,9 +78,8 @@ export class EffectScope {
* Resumes the effect scope, including all child scopes and effects.
*/
resume(): void {
if (this._active) {
if (this._isPaused) {
this._isPaused = false
if (this.flags & EffectFlags.PAUSED) {
this.flags &= ~EffectFlags.PAUSED
let i, l
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
@ -87,16 +91,15 @@ export class EffectScope {
}
}
}
}
run<T>(fn: () => T): T | undefined {
if (this._active) {
const currentEffectScope = activeEffectScope
if (this.active) {
const prevEffectScope = activeEffectScope
try {
activeEffectScope = this
return fn()
} finally {
activeEffectScope = currentEffectScope
activeEffectScope = prevEffectScope
}
} else if (__DEV__) {
warn(`cannot run an inactive effect scope.`)
@ -127,8 +130,10 @@ export class EffectScope {
}
stop(fromParent?: boolean): void {
if (this._active) {
this._active = false
if (this.active) {
this.flags |= EffectFlags.STOP
startTracking(this)
endTracking(this)
let i, l
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop()

View File

@ -5,7 +5,11 @@ import {
isFunction,
isObject,
} from '@vue/shared'
import { Dep, getDepFromReactive } from './dep'
import type { ComputedRef, WritableComputedRef } from './computed'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { onTrack, triggerEventInfos } from './debug'
import { getDepFromReactive } from './dep'
import { activeSub } from './effect'
import {
type Builtin,
type ShallowReactiveMarker,
@ -15,8 +19,7 @@ import {
toRaw,
toReactive,
} from './reactive'
import type { ComputedRef, WritableComputedRef } from './computed'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { type Dependency, type Link, link, propagate } from './system'
declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol
@ -103,12 +106,14 @@ function createRef(rawValue: unknown, shallow: boolean) {
/**
* @internal
*/
class RefImpl<T = any> {
class RefImpl<T = any> implements Dependency {
// Dependency
subs: Link | undefined = undefined
subsTail: Link | undefined = undefined
_value: T
private _rawValue: T
dep: Dep = new Dep()
public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
@ -118,16 +123,12 @@ class RefImpl<T = any> {
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
get value() {
if (__DEV__) {
this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
} else {
this.dep.track()
get dep() {
return this
}
get value() {
trackRef(this)
return this._value
}
@ -142,15 +143,17 @@ class RefImpl<T = any> {
this._rawValue = newValue
this._value = useDirectValue ? newValue : toReactive(newValue)
if (__DEV__) {
this.dep.trigger({
triggerEventInfos.push({
target: this,
type: TriggerOpTypes.SET,
key: 'value',
newValue,
oldValue,
})
} else {
this.dep.trigger()
}
triggerRef(this as unknown as Ref)
if (__DEV__) {
triggerEventInfos.pop()
}
}
}
@ -183,17 +186,22 @@ class RefImpl<T = any> {
*/
export function triggerRef(ref: Ref): void {
// ref may be an instance of ObjectRefImpl
if ((ref as unknown as RefImpl).dep) {
if (__DEV__) {
;(ref as unknown as RefImpl).dep.trigger({
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: (ref as unknown as RefImpl)._value,
})
} else {
;(ref as unknown as RefImpl).dep.trigger()
const dep = (ref as unknown as RefImpl).dep
if (dep !== undefined && dep.subs !== undefined) {
propagate(dep.subs)
}
}
function trackRef(dep: Dependency) {
if (activeSub !== undefined) {
if (__DEV__) {
onTrack(activeSub!, {
target: dep,
type: TrackOpTypes.GET,
key: 'value',
})
}
link(dep, activeSub!)
}
}
@ -285,8 +293,10 @@ export type CustomRefFactory<T> = (
set: (value: T) => void
}
class CustomRefImpl<T> {
public dep: Dep
class CustomRefImpl<T> implements Dependency {
// Dependency
subs: Link | undefined = undefined
subsTail: Link | undefined = undefined
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
@ -296,12 +306,18 @@ class CustomRefImpl<T> {
public _value: T = undefined!
constructor(factory: CustomRefFactory<T>) {
const dep = (this.dep = new Dep())
const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
const { get, set } = factory(
() => trackRef(this),
() => triggerRef(this as unknown as Ref),
)
this._get = get
this._set = set
}
get dep() {
return this
}
get value() {
return (this._value = this._get())
}
@ -361,7 +377,7 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
this._object[this._key] = newVal
}
get dep(): Dep | undefined {
get dep(): Dependency | undefined {
return getDepFromReactive(toRaw(this._object), this._key)
}
}

View File

@ -0,0 +1,438 @@
/* eslint-disable */
// Ported from https://github.com/stackblitz/alien-signals/blob/v1.0.0/src/system.ts
import type { ComputedRefImpl as Computed } from './computed.js'
import type { ReactiveEffect as Effect } from './effect.js'
export interface Dependency {
subs: Link | undefined
subsTail: Link | undefined
}
export interface Subscriber {
flags: SubscriberFlags
deps: Link | undefined
depsTail: Link | undefined
}
export interface Link {
dep: Dependency | Computed
sub: Subscriber | Computed | Effect
prevSub: Link | undefined
nextSub: Link | undefined
nextDep: Link | undefined
}
export const enum SubscriberFlags {
Computed = 1 << 0,
Effect = 1 << 1,
Tracking = 1 << 2,
Recursed = 1 << 4,
Dirty = 1 << 5,
PendingComputed = 1 << 6,
Propagated = Dirty | PendingComputed,
}
let batchDepth = 0
let queuedEffects: Effect | undefined
let queuedEffectsTail: Effect | undefined
let linkPool: Link | undefined
export function startBatch(): void {
++batchDepth
}
export function endBatch(): void {
if (!--batchDepth) {
processEffectNotifications()
}
}
export function link(dep: Dependency, sub: Subscriber): Link | undefined {
const currentDep = sub.depsTail
if (currentDep !== undefined && currentDep.dep === dep) {
return
}
const nextDep = currentDep !== undefined ? currentDep.nextDep : sub.deps
if (nextDep !== undefined && nextDep.dep === dep) {
sub.depsTail = nextDep
return
}
const depLastSub = dep.subsTail
if (
depLastSub !== undefined &&
depLastSub.sub === sub &&
isValidLink(depLastSub, sub)
) {
return
}
return linkNewDep(dep, sub, nextDep, currentDep)
}
export function propagate(link: Link): void {
let targetFlag = SubscriberFlags.Dirty
let subs = link
let stack = 0
top: do {
const sub = link.sub
const subFlags = sub.flags
if (
(!(
subFlags &
(SubscriberFlags.Tracking |
SubscriberFlags.Recursed |
SubscriberFlags.Propagated)
) &&
((sub.flags = subFlags | targetFlag), true)) ||
(subFlags & SubscriberFlags.Recursed &&
!(subFlags & SubscriberFlags.Tracking) &&
((sub.flags = (subFlags & ~SubscriberFlags.Recursed) | targetFlag),
true)) ||
(!(subFlags & SubscriberFlags.Propagated) &&
isValidLink(link, sub) &&
((sub.flags = subFlags | SubscriberFlags.Recursed | targetFlag),
(sub as Dependency).subs !== undefined))
) {
const subSubs = (sub as Dependency).subs
if (subSubs !== undefined) {
if (subSubs.nextSub !== undefined) {
subSubs.prevSub = subs
link = subs = subSubs
targetFlag = SubscriberFlags.PendingComputed
++stack
} else {
link = subSubs
targetFlag = SubscriberFlags.PendingComputed
}
continue
}
if (subFlags & SubscriberFlags.Effect) {
if (queuedEffectsTail !== undefined) {
queuedEffectsTail.depsTail!.nextDep = sub.deps
} else {
queuedEffects = sub as Effect
}
queuedEffectsTail = sub as Effect
}
} else if (!(subFlags & (SubscriberFlags.Tracking | targetFlag))) {
sub.flags = subFlags | targetFlag
} else if (
!(subFlags & targetFlag) &&
subFlags & SubscriberFlags.Propagated &&
isValidLink(link, sub)
) {
sub.flags = subFlags | targetFlag
}
if ((link = subs.nextSub!) !== undefined) {
subs = link
targetFlag = stack
? SubscriberFlags.PendingComputed
: SubscriberFlags.Dirty
continue
}
while (stack) {
--stack
const dep = subs.dep
const depSubs = dep.subs!
subs = depSubs.prevSub!
depSubs.prevSub = undefined
if ((link = subs.nextSub!) !== undefined) {
subs = link
targetFlag = stack
? SubscriberFlags.PendingComputed
: SubscriberFlags.Dirty
continue top
}
}
break
} while (true)
if (!batchDepth) {
processEffectNotifications()
}
}
export function startTracking(sub: Subscriber): void {
sub.depsTail = undefined
sub.flags =
(sub.flags & ~(SubscriberFlags.Recursed | SubscriberFlags.Propagated)) |
SubscriberFlags.Tracking
}
export function endTracking(sub: Subscriber): void {
const depsTail = sub.depsTail
if (depsTail !== undefined) {
const nextDep = depsTail.nextDep
if (nextDep !== undefined) {
clearTracking(nextDep)
depsTail.nextDep = undefined
}
} else if (sub.deps !== undefined) {
clearTracking(sub.deps)
sub.deps = undefined
}
sub.flags &= ~SubscriberFlags.Tracking
}
export function updateDirtyFlag(
sub: Subscriber,
flags: SubscriberFlags,
): boolean {
if (checkDirty(sub.deps!)) {
sub.flags = flags | SubscriberFlags.Dirty
return true
} else {
sub.flags = flags & ~SubscriberFlags.PendingComputed
return false
}
}
export function processComputedUpdate(
computed: Computed,
flags: SubscriberFlags,
): void {
if (flags & SubscriberFlags.Dirty) {
if (computed.update()) {
const subs = computed.subs
if (subs !== undefined) {
shallowPropagate(subs)
}
}
} else if (flags & SubscriberFlags.PendingComputed) {
if (checkDirty(computed.deps!)) {
if (computed.update()) {
const subs = computed.subs
if (subs !== undefined) {
shallowPropagate(subs)
}
}
} else {
computed.flags = flags & ~SubscriberFlags.PendingComputed
}
}
}
export function processEffectNotifications(): void {
while (queuedEffects !== undefined) {
const effect = queuedEffects
const depsTail = effect.depsTail!
const queuedNext = depsTail.nextDep
if (queuedNext !== undefined) {
depsTail.nextDep = undefined
queuedEffects = queuedNext.sub as Effect
} else {
queuedEffects = undefined
queuedEffectsTail = undefined
}
effect.notify()
}
}
function linkNewDep(
dep: Dependency,
sub: Subscriber,
nextDep: Link | undefined,
depsTail: Link | undefined,
): Link {
let newLink: Link
if (linkPool !== undefined) {
newLink = linkPool
linkPool = newLink.nextDep
newLink.nextDep = nextDep
newLink.dep = dep
newLink.sub = sub
} else {
newLink = {
dep,
sub,
nextDep,
prevSub: undefined,
nextSub: undefined,
}
}
if (depsTail === undefined) {
sub.deps = newLink
} else {
depsTail.nextDep = newLink
}
if (dep.subs === undefined) {
dep.subs = newLink
} else {
const oldTail = dep.subsTail!
newLink.prevSub = oldTail
oldTail.nextSub = newLink
}
sub.depsTail = newLink
dep.subsTail = newLink
return newLink
}
function checkDirty(link: Link): boolean {
let stack = 0
let dirty: boolean
top: do {
dirty = false
const dep = link.dep
if ('flags' in dep) {
const depFlags = dep.flags
if (
(depFlags & (SubscriberFlags.Computed | SubscriberFlags.Dirty)) ===
(SubscriberFlags.Computed | SubscriberFlags.Dirty)
) {
if ((dep as Computed).update()) {
const subs = dep.subs!
if (subs.nextSub !== undefined) {
shallowPropagate(subs)
}
dirty = true
}
} else if (
(depFlags &
(SubscriberFlags.Computed | SubscriberFlags.PendingComputed)) ===
(SubscriberFlags.Computed | SubscriberFlags.PendingComputed)
) {
const depSubs = dep.subs!
if (depSubs.nextSub !== undefined) {
depSubs.prevSub = link
}
link = dep.deps!
++stack
continue
}
}
if (!dirty && link.nextDep !== undefined) {
link = link.nextDep
continue
}
if (stack) {
let sub = link.sub as Computed
do {
--stack
const subSubs = sub.subs!
if (dirty) {
if (sub.update()) {
if ((link = subSubs.prevSub!) !== undefined) {
subSubs.prevSub = undefined
shallowPropagate(sub.subs!)
sub = link.sub as Computed
} else {
sub = subSubs.sub as Computed
}
continue
}
} else {
sub.flags &= ~SubscriberFlags.PendingComputed
}
if ((link = subSubs.prevSub!) !== undefined) {
subSubs.prevSub = undefined
if (link.nextDep !== undefined) {
link = link.nextDep
continue top
}
sub = link.sub as Computed
} else {
if ((link = subSubs.nextDep!) !== undefined) {
continue top
}
sub = subSubs.sub as Computed
}
dirty = false
} while (stack)
}
return dirty
} while (true)
}
function shallowPropagate(link: Link): void {
do {
const sub = link.sub
const subFlags = sub.flags
if (
(subFlags & (SubscriberFlags.PendingComputed | SubscriberFlags.Dirty)) ===
SubscriberFlags.PendingComputed
) {
sub.flags = subFlags | SubscriberFlags.Dirty
}
link = link.nextSub!
} while (link !== undefined)
}
function isValidLink(checkLink: Link, sub: Subscriber): boolean {
const depsTail = sub.depsTail
if (depsTail !== undefined) {
let link = sub.deps!
do {
if (link === checkLink) {
return true
}
if (link === depsTail) {
break
}
link = link.nextDep!
} while (link !== undefined)
}
return false
}
function clearTracking(link: Link): void {
do {
const dep = link.dep
const nextDep = link.nextDep
const nextSub = link.nextSub
const prevSub = link.prevSub
if (nextSub !== undefined) {
nextSub.prevSub = prevSub
link.nextSub = undefined
} else {
dep.subsTail = prevSub
}
if (prevSub !== undefined) {
prevSub.nextSub = nextSub
link.prevSub = undefined
} else {
dep.subs = nextSub
}
// @ts-expect-error
link.dep = undefined
// @ts-expect-error
link.sub = undefined
link.nextDep = linkPool
linkPool = link
if (dep.subs === undefined && 'deps' in dep) {
const depFlags = dep.flags
if (!(depFlags & SubscriberFlags.Dirty)) {
dep.flags = depFlags | SubscriberFlags.Dirty
}
const depDeps = dep.deps
if (depDeps !== undefined) {
link = depDeps
dep.depsTail!.nextDep = nextDep
dep.deps = undefined
dep.depsTail = undefined
continue
}
}
link = nextDep!
} while (link !== undefined)
}

View File

@ -10,20 +10,19 @@ import {
isSet,
remove,
} from '@vue/shared'
import { warn } from './warning'
import type { ComputedRef } from './computed'
import { ReactiveFlags } from './constants'
import {
type DebuggerOptions,
EffectFlags,
type EffectScheduler,
ReactiveEffect,
pauseTracking,
resetTracking,
} from './effect'
import { getCurrentScope } from './effectScope'
import { isReactive, isShallow } from './reactive'
import { type Ref, isRef } from './ref'
import { getCurrentScope } from './effectScope'
import { warn } from './warning'
// These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
// to @vue/reactivity to allow co-location with the moved base watch logic, hence
@ -231,10 +230,7 @@ export function watch(
: INITIAL_WATCHER_VALUE
const job = (immediateFirstRun?: boolean) => {
if (
!(effect.flags & EffectFlags.ACTIVE) ||
(!effect.dirty && !immediateFirstRun)
) {
if (!effect.active || (!immediateFirstRun && !effect.dirty)) {
return
}
if (cb) {

View File

@ -1,3 +1,8 @@
import {
type ComputedRefImpl,
type ReactiveEffectRunner,
effect,
} from '@vue/reactivity'
import {
type ComponentInternalInstance,
type SetupContext,
@ -25,8 +30,6 @@ import {
withAsyncContext,
withDefaults,
} from '../src/apiSetupHelpers'
import type { ComputedRefImpl } from '../../reactivity/src/computed'
import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
describe('SFC <script setup> helpers', () => {
test('should warn runtime usage', () => {
@ -450,12 +453,12 @@ describe('SFC <script setup> helpers', () => {
app.mount(root)
await ready
expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
expect(e!.effect.active).toBeTruthy()
expect(c!.flags & 2 /* SubscriberFlags.Tracking */).toBe(0)
app.unmount()
expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
expect(e!.effect.active).toBeFalsy()
expect(c!.flags & 2 /* SubscriberFlags.Tracking */).toBe(0)
})
})
})

View File

@ -31,6 +31,7 @@ import {
TrackOpTypes,
TriggerOpTypes,
effectScope,
onScopeDispose,
shallowReactive,
shallowRef,
toRef,
@ -1982,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

@ -531,6 +531,9 @@ describe('error handling', () => {
caughtError = caught
}
expect(fn).toHaveBeenCalledWith(err, 'setup function')
expect(
`Active effect was not restored correctly - this is likely a Vue internal bug.`,
).toHaveBeenWarned()
expect(
`Unhandled error during execution of setup function`,
).toHaveBeenWarned()

View File

@ -38,10 +38,12 @@ export function injectHook(
// This assumes the hook does not synchronously trigger other hooks, which
// can only be false when the user does something really funky.
const reset = setCurrentInstance(target)
const res = callWithAsyncErrorHandling(hook, target, type, args)
try {
return callWithAsyncErrorHandling(hook, target, type, args)
} finally {
reset()
resetTracking()
return res
}
})
if (prepend) {
hooks.unshift(wrappedHook)

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

@ -1565,7 +1565,8 @@ function baseCreateRenderer(
instance.scope.off()
const update = (instance.update = effect.run.bind(effect))
const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
const job: SchedulerJob = (instance.job = () =>
effect.dirty && effect.run())
job.i = instance
job.id = instance.uid
effect.scheduler = () => queueJob(job)

View File

@ -48,7 +48,7 @@ export function renderEffect(fn: () => void, noLifecycle = false): void {
}
const effect = new ReactiveEffect(renderEffectFn)
const job: SchedulerJob = effect.runIfDirty.bind(effect)
const job: SchedulerJob = () => effect.dirty && effect.run()
if (instance) {
if (__DEV__) {

View File

@ -1,13 +1,13 @@
import puppeteer, {
type Browser,
type ClickOptions,
type LaunchOptions,
type Page,
type PuppeteerLaunchOptions,
} from 'puppeteer'
export const E2E_TIMEOUT: number = 30 * 1000
const puppeteerOptions: PuppeteerLaunchOptions = {
const puppeteerOptions: LaunchOptions = {
args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
headless: true,
}

View File

@ -14,6 +14,7 @@ describe('e2e: todomvc', () => {
classList,
enterValue,
clearValue,
timeout,
} = setupPuppeteer()
async function removeItemAt(n: number) {
@ -101,6 +102,7 @@ describe('e2e: todomvc', () => {
// active filter
await click('.filters li:nth-child(2) a')
await timeout(1)
expect(await count('.todo')).toBe(1)
expect(await count('.todo.completed')).toBe(0)
// add item with filter active
@ -109,6 +111,7 @@ describe('e2e: todomvc', () => {
// completed filter
await click('.filters li:nth-child(3) a')
await timeout(1)
expect(await count('.todo')).toBe(2)
expect(await count('.todo.completed')).toBe(2)
@ -128,12 +131,14 @@ describe('e2e: todomvc', () => {
await click('.todo .toggle')
expect(await count('.todo')).toBe(1)
await click('.filters li:nth-child(2) a')
await timeout(1)
expect(await count('.todo')).toBe(3)
await click('.todo .toggle')
expect(await count('.todo')).toBe(2)
// editing triggered by blur
await click('.filters li:nth-child(1) a')
await timeout(1)
await click('.todo:nth-child(1) label', { clickCount: 2 })
expect(await count('.todo.editing')).toBe(1)
expect(await isFocused('.todo:nth-child(1) .edit')).toBe(true)

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
import { configDefaults, defineConfig } from 'vitest/config'
import { defineConfig } from 'vitest/config'
import { entries } from './scripts/aliases.js'
export default defineConfig({
define: {
__DEV__: true,
__DEV__: process.env.MODE !== 'benchmark',
__TEST__: true,
__VERSION__: '"test"',
__BROWSER__: false,
@ -25,6 +25,11 @@ export default defineConfig({
test: {
globals: true,
pool: 'threads',
poolOptions: {
forks: {
execArgv: ['--expose-gc'],
},
},
setupFiles: 'scripts/setup-vitest.ts',
environmentMatchGlobs: [
['packages/{vue,vue-compat,runtime-dom,runtime-vapor}/**', 'jsdom'],