diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 493ab295000..b9e8078dd79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,31 @@ jobs: - name: Run ssr unit tests run: pnpm run test-unit server-renderer + benchmarks: + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + cache: 'pnpm' + + - run: pnpm install + + - name: Run benchmarks + uses: CodSpeedHQ/action@v2 + with: + run: pnpm vitest bench --run + token: ${{ secrets.CODSPEED_TOKEN }} + e2e-test: runs-on: ubuntu-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository diff --git a/CHANGELOG.md b/CHANGELOG.md index c8598e84614..d523b6731f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## [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) @@ -19,74 +36,6 @@ -# [3.4.0-alpha.3](https://github.com/vuejs/core/compare/v3.4.0-alpha.2...v3.4.0-alpha.3) (2023-11-28) - - -### Bug Fixes - -* **parser:** directive arg should be undefined on shorthands with no arg ([e49dffc](https://github.com/vuejs/core/commit/e49dffc9ece86bddf094b9ad4ad15eb4856d6277)) - - -### Features - -* **dx:** link errors to docs in prod build ([#9165](https://github.com/vuejs/core/issues/9165)) ([9f8ba98](https://github.com/vuejs/core/commit/9f8ba9821fe166f77e63fa940e9e7e13ec3344fa)) - - - -# [3.4.0-alpha.2](https://github.com/vuejs/core/compare/v3.3.9...v3.4.0-alpha.2) (2023-11-27) - - -### Bug Fixes - -* avoid confusing breakage in @vitejs/plugin-vue ([ceec69c](https://github.com/vuejs/core/commit/ceec69c8ccb96c433a4a506ad2e85e276998bade)) -* **compiler-core:** fix line/column tracking when fast forwarding ([2e65ea4](https://github.com/vuejs/core/commit/2e65ea481f74db8649df8110a031cbdc98f98c84)) -* **compiler-sfc:** fix ast reuse for ssr ([fb619cf](https://github.com/vuejs/core/commit/fb619cf9a440239f0ba88e327d10001a6a3c8171)) -* **compiler-sfc:** support `:is` and `:where` selector in scoped css rewrite ([#8929](https://github.com/vuejs/core/issues/8929)) ([c6083dc](https://github.com/vuejs/core/commit/c6083dcad31f3e9292c687fada9e32f287e2317f)) -* **compiler-sfc:** use correct compiler when re-parsing in ssr mode ([678378a](https://github.com/vuejs/core/commit/678378afd559481badb486b243722b6287862e09)) - - -* feat!: remove reactivity transform (#9321) ([79b8a09](https://github.com/vuejs/core/commit/79b8a0905bf363bf82edd2096fef10c3db6d9c3c)), closes [#9321](https://github.com/vuejs/core/issues/9321) - - -### Features - -* **compiler-core:** support specifying root namespace when parsing ([40f72d5](https://github.com/vuejs/core/commit/40f72d5e50b389cb11b7ca13461aa2a75ddacdb4)) -* **compiler-core:** support v-bind shorthand for key and value with the same name ([#9451](https://github.com/vuejs/core/issues/9451)) ([26399aa](https://github.com/vuejs/core/commit/26399aa6fac1596b294ffeba06bb498d86f5508c)) -* **compiler:** improve parsing tolerance for language-tools ([41ff68e](https://github.com/vuejs/core/commit/41ff68ea579d933333392146625560359acb728a)) -* **reactivity:** expose last result for computed getter ([#9497](https://github.com/vuejs/core/issues/9497)) ([48b47a1](https://github.com/vuejs/core/commit/48b47a1ab63577e2dbd91947eea544e3ef185b85)) - - -### Performance Improvements - -* avoid sfc source map unnecessary serialization and parsing ([f15d2f6](https://github.com/vuejs/core/commit/f15d2f6cf69c0c39f8dfb5c33122790c68bf92e2)) -* **codegen:** optimize line / column calculation during codegen ([3be53d9](https://github.com/vuejs/core/commit/3be53d9b974dae1a10eb795cade71ae765e17574)) -* **codegen:** optimize source map generation ([c11002f](https://github.com/vuejs/core/commit/c11002f16afd243a2b15b546816e73882eea9e4d)) -* **compiler-sfc:** remove magic-string trim on script ([e8e3ec6](https://github.com/vuejs/core/commit/e8e3ec6ca7392e43975c75b56eaaa711d5ea9410)) -* **compiler-sfc:** use faster source map addMapping ([50cde7c](https://github.com/vuejs/core/commit/50cde7cfbcc49022ba88f5f69fa9b930b483c282)) -* optimize away isBuiltInType ([66c0ed0](https://github.com/vuejs/core/commit/66c0ed0a3c1c6f37dafc6b1c52b75c6bf60e3136)) -* optimize makeMap ([ae6fba9](https://github.com/vuejs/core/commit/ae6fba94954bac6430902f77b0d1113a98a75b18)) -* optimize position cloning ([2073236](https://github.com/vuejs/core/commit/20732366b9b3530d33b842cf1fc985919afb9317)) - - -### BREAKING CHANGES - -* Reactivity Transform was marked deprecated in 3.3 and is now removed in 3.4. This change does not require a major due to the feature being experimental. Users who wish to continue using the feature can do so via the external plugin at https://vue-macros.dev/features/reactivity-transform.html - - - -# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28) - - -### Features - -* **compiler-core:** export error message ([#8729](https://github.com/vuejs/core/issues/8729)) ([f7e80ee](https://github.com/vuejs/core/commit/f7e80ee4a065a9eaba98720abf415d9e87756cbd)) -* **compiler-sfc:** expose resolve type-based props and emits ([#8874](https://github.com/vuejs/core/issues/8874)) ([9e77580](https://github.com/vuejs/core/commit/9e77580c0c2f0d977bd0031a1d43cc334769d433)) -* export runtime error strings ([#9301](https://github.com/vuejs/core/issues/9301)) ([feb2f2e](https://github.com/vuejs/core/commit/feb2f2edce2d91218a5e9a52c81e322e4033296b)) -* **reactivity:** more efficient reactivity system ([#5912](https://github.com/vuejs/core/issues/5912)) ([16e06ca](https://github.com/vuejs/core/commit/16e06ca08f5a1e2af3fc7fb35de153dbe0c3087d)), closes [#311](https://github.com/vuejs/core/issues/311) [#1811](https://github.com/vuejs/core/issues/1811) [#6018](https://github.com/vuejs/core/issues/6018) [#7160](https://github.com/vuejs/core/issues/7160) [#8714](https://github.com/vuejs/core/issues/8714) [#9149](https://github.com/vuejs/core/issues/9149) [#9419](https://github.com/vuejs/core/issues/9419) [#9464](https://github.com/vuejs/core/issues/9464) -* **runtime-core:** add `once` option to watch ([#9034](https://github.com/vuejs/core/issues/9034)) ([a645e7a](https://github.com/vuejs/core/commit/a645e7aa51006516ba668b3a4365d296eb92ee7d)) - - - ## [3.3.10](https://github.com/vuejs/core/compare/v3.3.9...v3.3.10) (2023-12-04) diff --git a/package.json b/package.json index 9564d3e51ff..5d38cae71e4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "version": "3.4.0-alpha.4", - "packageManager": "pnpm@8.11.0", + "packageManager": "pnpm@8.12.0", "type": "module", "scripts": { "dev": "node scripts/dev.js", @@ -22,6 +22,7 @@ "test-dts": "run-s build-dts test-dts-only", "test-dts-only": "tsc -p ./packages/dts-test/tsconfig.test.json", "test-coverage": "vitest -c vitest.unit.config.ts --coverage", + "test-bench": "vitest bench", "release": "node scripts/release.js", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "dev-esm": "node scripts/dev.js -if esm-bundler-runtime", @@ -60,6 +61,7 @@ "devDependencies": { "@babel/parser": "^7.23.5", "@babel/types": "^7.23.5", + "@codspeed/vitest-plugin": "^2.3.1", "@rollup/plugin-alias": "^5.0.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.0.1", @@ -68,33 +70,33 @@ "@rollup/plugin-terser": "^0.4.4", "@types/hash-sum": "^1.0.2", "@types/minimist": "^1.2.5", - "@types/node": "^20.10.3", + "@types/node": "^20.10.4", "@types/semver": "^7.5.5", - "@typescript-eslint/parser": "^6.13.0", - "@vitest/coverage-istanbul": "^0.34.6", + "@typescript-eslint/parser": "^6.13.2", + "@vitest/coverage-istanbul": "^1.0.4", "@vue/consolidate": "0.17.3", "conventional-changelog-cli": "^4.1.0", "enquirer": "^2.4.1", "esbuild": "^0.19.5", "esbuild-plugin-polyfill-node": "^0.3.0", - "eslint": "^8.54.0", + "eslint": "^8.55.0", "eslint-define-config": "^1.24.1", "eslint-plugin-jest": "^27.6.0", "estree-walker": "^2.0.2", "execa": "^8.0.1", - "jsdom": "^22.1.0", - "lint-staged": "^15.1.0", + "jsdom": "^23.0.1", + "lint-staged": "^15.2.0", "lodash": "^4.17.21", "magic-string": "^0.30.5", "markdown-table": "^3.0.3", - "marked": "^9.1.6", + "marked": "^11.0.1", "minimist": "^1.2.8", "npm-run-all": "^4.1.5", "picocolors": "^1.0.0", - "prettier": "^3.1.0", + "prettier": "^3.1.1", "pretty-bytes": "^6.1.1", "pug": "^3.0.2", - "puppeteer": "~21.5.2", + "puppeteer": "~21.6.0", "rimraf": "^5.0.5", "rollup": "^4.1.4", "rollup-plugin-dts": "^6.1.0", @@ -108,7 +110,7 @@ "tslib": "^2.6.2", "tsx": "^4.6.2", "typescript": "^5.2.2", - "vite": "^5.0.0", - "vitest": "^1.0.0" + "vite": "^5.0.5", + "vitest": "^1.0.4" } } diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap index 38c4419b5d3..ce5eaed18fd 100644 --- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineProps.spec.ts.snap @@ -18,6 +18,47 @@ return { props, bar } }" `; +exports[`defineProps > custom element retains the props type & default value & production mode 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' +interface Props { + foo?: number; + } + +export default /*#__PURE__*/_defineComponent({ + __name: 'app.ce', + props: { + foo: { default: 5.5, type: Number } + }, + setup(__props: any, { expose: __expose }) { + __expose(); + + const props = __props; + +return { props } +} + +})" +`; + +exports[`defineProps > custom element retains the props type & production mode 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + __name: 'app.ce', + props: { + foo: {type: Number} + }, + setup(__props: any, { expose: __expose }) { + __expose(); + + const props = __props + +return { props } +} + +})" +`; + exports[`defineProps > defineProps w/ runtime options 1`] = ` "import { defineComponent as _defineComponent } from 'vue' diff --git a/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts index 23d6a806f0d..7d46c4d2187 100644 --- a/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts @@ -710,4 +710,35 @@ const props = defineProps({ foo: String }) 'da-sh': BindingTypes.PROPS }) }) + + // #8989 + test('custom element retains the props type & production mode', () => { + const { content } = compile( + ``, + { isProd: true, customElement: filename => /\.ce\.vue$/.test(filename) }, + { filename: 'app.ce.vue' } + ) + + expect(content).toMatch(`foo: {type: Number}`) + assertCode(content) + }) + + test('custom element retains the props type & default value & production mode', () => { + const { content } = compile( + ``, + { isProd: true, customElement: filename => /\.ce\.vue$/.test(filename) }, + { filename: 'app.ce.vue' } + ) + expect(content).toMatch(`foo: { default: 5.5, type: Number }`) + assertCode(content) + }) }) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 1783c6db9ee..3a375a05d20 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -117,6 +117,10 @@ export interface SFCScriptCompileOptions { fileExists(file: string): boolean readFile(file: string): string | undefined } + /** + * Transform Vue SFCs into custom elements. + */ + customElement?: boolean | ((filename: string) => boolean) } export interface ImportBinding { diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 900cf109260..7643b28fd50 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -12,6 +12,7 @@ import { TypeScope } from './resolveType' export class ScriptCompileContext { isJS: boolean isTS: boolean + isCE = false scriptAst: Program | null scriptSetupAst: Program | null @@ -95,6 +96,14 @@ export class ScriptCompileContext { scriptSetupLang === 'ts' || scriptSetupLang === 'tsx' + const customElement = options.customElement + const filename = this.descriptor.filename + if (customElement) { + this.isCE = + typeof customElement === 'boolean' + ? customElement + : customElement(filename) + } // resolve parser plugins const plugins: ParserPlugin[] = resolveParserPlugins( (scriptLang || scriptSetupLang)!, diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts index 3df6daea7d6..cc7b86ba2cb 100644 --- a/packages/compiler-sfc/src/script/defineProps.ts +++ b/packages/compiler-sfc/src/script/defineProps.ts @@ -281,6 +281,17 @@ function genRuntimePropFromType( defaultString ])} }` } else { + // #8989 for custom element, should keep the type + if (ctx.isCE) { + if (defaultString) { + return `${finalKey}: ${`{ ${defaultString}, type: ${toRuntimeTypeString( + type + )} }`}` + } else { + return `${finalKey}: {type: ${toRuntimeTypeString(type)}}` + } + } + // production: checks are useless return `${finalKey}: ${defaultString ? `{ ${defaultString} }` : `{}`}` } diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index ed25ba9a734..f38374bd5bb 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -83,6 +83,9 @@ export type SimpleTypeResolveContext = Pick< // emits | 'emitsTypeDecl' + + // customElement + | 'isCE' > & Partial< Pick @@ -1475,6 +1478,7 @@ export function inferRuntimeType( scope ) } + break case 'TSMethodSignature': case 'TSFunctionType': return ['Function'] diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts index 542d9d6a9ef..a467c446d0a 100644 --- a/packages/dts-test/ref.test-d.ts +++ b/packages/dts-test/ref.test-d.ts @@ -18,7 +18,7 @@ import { computed, ShallowRef } from 'vue' -import { expectType, describe, IsUnion } from './utils' +import { expectType, describe, IsUnion, IsAny } from './utils' function plainType(arg: number | Ref) { // ref coercing @@ -79,6 +79,10 @@ function plainType(arg: number | Ref) { // should still unwrap in objects nested in arrays const arr2 = ref([{ a: ref(1) }]).value expectType(arr2[0].a) + + // any value should return Ref, not any + const a = ref(1 as any) + expectType>(false) } plainType(1) @@ -191,6 +195,12 @@ if (refStatus.value === 'initial') { expectType>(false) } +{ + // any value should return Ref, not any + const a = shallowRef(1 as any) + expectType>(false) +} + describe('shallowRef with generic', () => { const r = ref({}) as MaybeRef expectType | Ref>(shallowRef(r)) diff --git a/packages/dts-test/setupHelpers.test-d.ts b/packages/dts-test/setupHelpers.test-d.ts index 51f95c00944..53c4d859788 100644 --- a/packages/dts-test/setupHelpers.test-d.ts +++ b/packages/dts-test/setupHelpers.test-d.ts @@ -260,6 +260,30 @@ describe('defineSlots', () => { expectType(slotsUntype) }) +describe('defineSlots generic', >() => { + const props = defineProps<{ + item: T + }>() + + const slots = defineSlots< + { + [K in keyof T as `slot-${K & string}`]?: (props: { item: T }) => any + } & { + label?: (props: { item: T }) => any + } + >() + + for (const key of Object.keys(props.item) as (keyof T & string)[]) { + slots[`slot-${String(key)}`]?.({ + item: props.item + }) + } + slots.label?.({ item: props.item }) + + // @ts-expect-error calling wrong slot + slots.foo({}) +}) + describe('defineModel', () => { // overload 1 const modelValueRequired = defineModel({ required: true }) @@ -336,6 +360,78 @@ describe('useSlots', () => { expectType(slots) }) +describe('defineSlots generic', >() => { + const props = defineProps<{ + item: T + }>() + + const slots = defineSlots< + { + [K in keyof T as `slot-${K & string}`]?: (props: { item: T }) => any + } & { + label?: (props: { item: T }) => any + } + >() + + // @ts-expect-error slots should be readonly + slots.label = () => {} + + // @ts-expect-error non existing slot + slots['foo-asdas']?.({ + item: props.item + }) + for (const key in props.item) { + slots[`slot-${String(key)}`]?.({ + item: props.item + }) + slots[`slot-${String(key as keyof T)}`]?.({ + item: props.item + }) + } + + for (const key of Object.keys(props.item) as (keyof T)[]) { + slots[`slot-${String(key)}`]?.({ + item: props.item + }) + } + slots.label?.({ item: props.item }) + + // @ts-expect-error calling wrong slot + slots.foo({}) +}) + +describe('defineSlots generic strict', () => { + const props = defineProps<{ + item: T + }>() + + const slots = defineSlots< + { + [K in keyof T as `slot-${K & string}`]?: (props: { item: T }) => any + } & { + label?: (props: { item: T }) => any + } + >() + + // slot-bar/foo should be automatically inferred + slots['slot-bar']?.({ item: props.item }) + slots['slot-foo']?.({ item: props.item }) + + slots.label?.({ item: props.item }) + + // @ts-expect-error not part of the extends + slots['slot-RANDOM']?.({ item: props.item }) + + // @ts-expect-error slots should be readonly + slots.label = () => {} + + // @ts-expect-error calling wrong slot + slots.foo({}) +}) + // #6420 describe('toRefs w/ type declaration', () => { const props = defineProps<{ diff --git a/packages/dts-test/tsx.test-d.tsx b/packages/dts-test/tsx.test-d.tsx index 4b4a0dbf9df..5e171e1f75f 100644 --- a/packages/dts-test/tsx.test-d.tsx +++ b/packages/dts-test/tsx.test-d.tsx @@ -112,3 +112,11 @@ expectType( ) // @ts-expect-error ; + +// svg +expectType( + +) diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json new file mode 100644 index 00000000000..4baea5d3a55 --- /dev/null +++ b/packages/reactivity-transform/package.json @@ -0,0 +1,41 @@ +{ + "name": "@vue/reactivity-transform", + "version": "3.3.11", + "description": "@vue/reactivity-transform", + "main": "dist/reactivity-transform.cjs.js", + "files": [ + "dist" + ], + "buildOptions": { + "formats": [ + "cjs" + ], + "prod": false + }, + "types": "dist/reactivity-transform.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/core.git", + "directory": "packages/reactivity-transform" + }, + "keywords": [ + "vue" + ], + "author": "Evan You", + "license": "MIT", + "bugs": { + "url": "https://github.com/vuejs/core/issues" + }, + "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", + "dependencies": { + "@babel/parser": "^7.23.5", + "@vue/compiler-core": "workspace:*", + "@vue/shared": "workspace:*", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5" + }, + "devDependencies": { + "@babel/core": "^7.23.5", + "@babel/types": "^7.23.5" + } +} diff --git a/packages/reactivity/__tests__/computed.bench.ts b/packages/reactivity/__tests__/computed.bench.ts new file mode 100644 index 00000000000..73c20acdfad --- /dev/null +++ b/packages/reactivity/__tests__/computed.bench.ts @@ -0,0 +1,126 @@ +import { describe, bench } from 'vitest' +import { ComputedRef, Ref, computed, ref } from '../src/index' + +describe('computed', () => { + bench('create computed', () => { + computed(() => 100) + }) + + { + let i = 0 + const o = ref(100) + bench('write independent ref dep', () => { + o.value = i++ + }) + } + + { + const v = ref(100) + computed(() => v.value * 2) + let i = 0 + bench("write ref, don't read computed (never invoked)", () => { + v.value = i++ + }) + } + + { + const v = ref(100) + computed(() => { + return v.value * 2 + }) + let i = 0 + bench("write ref, don't read computed (never invoked)", () => { + v.value = i++ + }) + } + + { + const v = ref(100) + const c = computed(() => { + return v.value * 2 + }) + c.value + let i = 0 + bench("write ref, don't read computed (invoked)", () => { + v.value = i++ + }) + } + + { + const v = ref(100) + const c = computed(() => { + return v.value * 2 + }) + let i = 0 + bench('write ref, read computed', () => { + v.value = i++ + c.value + }) + } + + { + const v = ref(100) + const computeds = [] + for (let i = 0, n = 1000; i < n; i++) { + const c = computed(() => { + return v.value * 2 + }) + computeds.push(c) + } + let i = 0 + bench("write ref, don't read 1000 computeds (never invoked)", () => { + v.value = i++ + }) + } + + { + const v = ref(100) + const computeds = [] + for (let i = 0, n = 1000; i < n; i++) { + const c = computed(() => { + return v.value * 2 + }) + c.value + computeds.push(c) + } + let i = 0 + bench("write ref, don't read 1000 computeds (invoked)", () => { + v.value = i++ + }) + } + + { + const v = ref(100) + const computeds: ComputedRef[] = [] + for (let i = 0, n = 1000; i < n; i++) { + const c = computed(() => { + return v.value * 2 + }) + c.value + computeds.push(c) + } + let i = 0 + bench('write ref, read 1000 computeds', () => { + v.value = i++ + computeds.forEach(c => c.value) + }) + } + + { + const refs: Ref[] = [] + for (let i = 0, n = 1000; i < n; i++) { + refs.push(ref(i)) + } + const c = computed(() => { + let total = 0 + refs.forEach(ref => (total += ref.value)) + return total + }) + let i = 0 + const n = refs.length + bench('1000 refs, 1 computed', () => { + refs[i++ % n].value++ + c.value + }) + } +}) diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index e7fe18252ab..1ea1ba4a901 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -158,6 +158,21 @@ describe('reactivity/reactive', () => { expect(original.bar).toBe(original2) }) + // #1246 + test('mutation on objects using reactive as prototype should not trigger', () => { + const observed = reactive({ foo: 1 }) + const original = Object.create(observed) + let dummy + effect(() => (dummy = original.foo)) + expect(dummy).toBe(1) + observed.foo = 2 + expect(dummy).toBe(2) + original.foo = 3 + expect(dummy).toBe(2) + original.foo = 4 + expect(dummy).toBe(2) + }) + test('toRaw', () => { const original = { foo: 1 } const observed = reactive(original) @@ -166,11 +181,18 @@ describe('reactivity/reactive', () => { }) test('toRaw on object using reactive as prototype', () => { - const original = reactive({}) - const obj = Object.create(original) + const original = { foo: 1 } + const observed = reactive(original) + const inherted = Object.create(observed) + expect(toRaw(inherted)).toBe(inherted) + }) + + test('toRaw on user Proxy wrapping reactive', () => { + const original = {} + const re = reactive(original) + const obj = new Proxy(re, {}) const raw = toRaw(obj) - expect(raw).toBe(obj) - expect(raw).not.toBe(toRaw(original)) + expect(raw).toBe(original) }) test('should not unwrap Ref', () => { diff --git a/packages/reactivity/__tests__/reactiveArray.bench.ts b/packages/reactivity/__tests__/reactiveArray.bench.ts new file mode 100644 index 00000000000..596b9f1a33e --- /dev/null +++ b/packages/reactivity/__tests__/reactiveArray.bench.ts @@ -0,0 +1,92 @@ +import { bench } from 'vitest' +import { computed, reactive, readonly, shallowRef, triggerRef } from '../src' + +for (let amount = 1e1; amount < 1e4; amount *= 10) { + { + const rawArray = [] + for (let i = 0, n = amount; i < n; i++) { + rawArray.push(i) + } + const r = reactive(rawArray) + const c = computed(() => { + return r.reduce((v, a) => a + v, 0) + }) + + bench(`reduce *reactive* array, ${amount} elements`, () => { + for (let i = 0, n = r.length; i < n; i++) { + r[i]++ + } + c.value + }) + } + + { + const rawArray = [] + for (let i = 0, n = amount; i < n; i++) { + rawArray.push(i) + } + const r = reactive(rawArray) + const c = computed(() => { + return r.reduce((v, a) => a + v, 0) + }) + + bench( + `reduce *reactive* array, ${amount} elements, only change first value`, + () => { + r[0]++ + c.value + } + ) + } + + { + const rawArray = [] + for (let i = 0, n = amount; i < n; i++) { + rawArray.push(i) + } + const r = reactive({ arr: readonly(rawArray) }) + const c = computed(() => { + return r.arr.reduce((v, a) => a + v, 0) + }) + + bench(`reduce *readonly* array, ${amount} elements`, () => { + r.arr = r.arr.map(v => v + 1) + c.value + }) + } + + { + const rawArray = [] + for (let i = 0, n = amount; i < n; i++) { + rawArray.push(i) + } + const r = shallowRef(rawArray) + const c = computed(() => { + return r.value.reduce((v, a) => a + v, 0) + }) + + bench(`reduce *raw* array, copied, ${amount} elements`, () => { + r.value = r.value.map(v => v + 1) + c.value + }) + } + + { + const rawArray: number[] = [] + for (let i = 0, n = amount; i < n; i++) { + rawArray.push(i) + } + const r = shallowRef(rawArray) + const c = computed(() => { + return r.value.reduce((v, a) => a + v, 0) + }) + + bench(`reduce *raw* array, manually triggered, ${amount} elements`, () => { + for (let i = 0, n = rawArray.length; i < n; i++) { + rawArray[i]++ + } + triggerRef(r) + c.value + }) + } +} diff --git a/packages/reactivity/__tests__/reactiveArray.spec.ts b/packages/reactivity/__tests__/reactiveArray.spec.ts index f4eb7b58384..7eb800b80bb 100644 --- a/packages/reactivity/__tests__/reactiveArray.spec.ts +++ b/packages/reactivity/__tests__/reactiveArray.spec.ts @@ -175,6 +175,15 @@ describe('reactivity/reactive/Array', () => { expect(length).toBe('01') }) + // #9742 + test('mutation on user proxy of reactive Array', () => { + const array = reactive([]) + const proxy = new Proxy(array, {}) + proxy.push(1) + expect(array).toHaveLength(1) + expect(proxy).toHaveLength(1) + }) + describe('Array methods w/ refs', () => { let original: any[] beforeEach(() => { diff --git a/packages/reactivity/__tests__/reactiveMap.bench.ts b/packages/reactivity/__tests__/reactiveMap.bench.ts new file mode 100644 index 00000000000..579904bafe7 --- /dev/null +++ b/packages/reactivity/__tests__/reactiveMap.bench.ts @@ -0,0 +1,143 @@ +import { bench } from 'vitest' +import { reactive, computed, ComputedRef } from '../src' + +function createMap(obj: Record) { + const map = new Map() + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + map.set(key, obj[key]) + } + } + return map +} + +bench('create reactive map', () => { + reactive(createMap({ a: 1 })) +}) + +{ + let i = 0 + const r = reactive(createMap({ a: 1 })) + bench('write reactive map property', () => { + r.set('a', i++) + }) +} + +{ + const r = reactive(createMap({ a: 1 })) + computed(() => { + return r.get('a') * 2 + }) + let i = 0 + bench("write reactive map, don't read computed (never invoked)", () => { + r.set('a', i++) + }) +} + +{ + const r = reactive(createMap({ a: 1 })) + const c = computed(() => { + return r.get('a') * 2 + }) + c.value + let i = 0 + bench("write reactive map, don't read computed (invoked)", () => { + r.set('a', i++) + }) +} + +{ + const r = reactive(createMap({ a: 1 })) + const c = computed(() => { + return r.get('a') * 2 + }) + let i = 0 + bench('write reactive map, read computed', () => { + r.set('a', i++) + c.value + }) +} + +{ + const _m = new Map() + for (let i = 0; i < 10000; i++) { + _m.set(i, i) + } + const r = reactive(_m) + const c = computed(() => { + let total = 0 + r.forEach((value, key) => { + total += value + }) + return total + }) + bench("write reactive map (10'000 items), read computed", () => { + r.set(5000, r.get(5000) + 1) + c.value + }) +} + +{ + const r = reactive(createMap({ a: 1 })) + const computeds = [] + for (let i = 0, n = 1000; i < n; i++) { + const c = computed(() => { + return r.get('a') * 2 + }) + computeds.push(c) + } + let i = 0 + bench("write reactive map, don't read 1000 computeds (never invoked)", () => { + r.set('a', i++) + }) +} + +{ + const r = reactive(createMap({ a: 1 })) + const computeds = [] + for (let i = 0, n = 1000; i < n; i++) { + const c = computed(() => { + return r.get('a') * 2 + }) + c.value + computeds.push(c) + } + let i = 0 + bench("write reactive map, don't read 1000 computeds (invoked)", () => { + r.set('a', i++) + }) +} + +{ + const r = reactive(createMap({ a: 1 })) + const computeds: ComputedRef[] = [] + for (let i = 0, n = 1000; i < n; i++) { + const c = computed(() => { + return r.get('a') * 2 + }) + computeds.push(c) + } + let i = 0 + bench('write reactive map, read 1000 computeds', () => { + r.set('a', i++) + computeds.forEach(c => c.value) + }) +} + +{ + const reactives: Map[] = [] + for (let i = 0, n = 1000; i < n; i++) { + reactives.push(reactive(createMap({ a: i }))) + } + const c = computed(() => { + let total = 0 + reactives.forEach(r => (total += r.get('a'))) + return total + }) + let i = 0 + const n = reactives.length + bench('1000 reactive maps, 1 computed', () => { + reactives[i++ % n].set('a', reactives[i++ % n].get('a') + 1) + c.value + }) +} diff --git a/packages/reactivity/__tests__/reactiveObject.bench.ts b/packages/reactivity/__tests__/reactiveObject.bench.ts new file mode 100644 index 00000000000..43af8a96c51 --- /dev/null +++ b/packages/reactivity/__tests__/reactiveObject.bench.ts @@ -0,0 +1,114 @@ +import { bench } from 'vitest' +import { ComputedRef, computed, reactive } from '../src' + +bench('create reactive obj', () => { + reactive({ a: 1 }) +}) + +{ + let i = 0 + const r = reactive({ a: 1 }) + bench('write reactive obj property', () => { + r.a = i++ + }) +} + +{ + const r = reactive({ a: 1 }) + computed(() => { + return r.a * 2 + }) + let i = 0 + bench("write reactive obj, don't read computed (never invoked)", () => { + r.a = i++ + }) +} + +{ + const r = reactive({ a: 1 }) + const c = computed(() => { + return r.a * 2 + }) + c.value + let i = 0 + bench("write reactive obj, don't read computed (invoked)", () => { + r.a = i++ + }) +} + +{ + const r = reactive({ a: 1 }) + const c = computed(() => { + return r.a * 2 + }) + let i = 0 + bench('write reactive obj, read computed', () => { + r.a = i++ + c.value + }) +} + +{ + const r = reactive({ a: 1 }) + const computeds = [] + for (let i = 0, n = 1000; i < n; i++) { + const c = computed(() => { + return r.a * 2 + }) + computeds.push(c) + } + let i = 0 + bench("write reactive obj, don't read 1000 computeds (never invoked)", () => { + r.a = i++ + }) +} + +{ + const r = reactive({ a: 1 }) + const computeds = [] + for (let i = 0, n = 1000; i < n; i++) { + const c = computed(() => { + return r.a * 2 + }) + c.value + computeds.push(c) + } + let i = 0 + bench("write reactive obj, don't read 1000 computeds (invoked)", () => { + r.a = i++ + }) +} + +{ + const r = reactive({ a: 1 }) + const computeds: ComputedRef[] = [] + for (let i = 0, n = 1000; i < n; i++) { + const c = computed(() => { + return r.a * 2 + }) + computeds.push(c) + } + let i = 0 + bench('write reactive obj, read 1000 computeds', () => { + r.a = i++ + computeds.forEach(c => c.value) + }) +} + +{ + const reactives: Record[] = [] + for (let i = 0, n = 1000; i < n; i++) { + reactives.push(reactive({ a: i })) + } + const c = computed(() => { + let total = 0 + reactives.forEach(r => (total += r.a)) + return total + }) + let i = 0 + const n = reactives.length + bench('1000 reactive objs, 1 computed', () => { + reactives[i++ % n].a++ + c.value + }) +} diff --git a/packages/reactivity/__tests__/ref.bench.ts b/packages/reactivity/__tests__/ref.bench.ts new file mode 100644 index 00000000000..a1b625ab7ac --- /dev/null +++ b/packages/reactivity/__tests__/ref.bench.ts @@ -0,0 +1,33 @@ +import { describe, bench } from 'vitest' +import { ref } from '../src/index' + +describe('ref', () => { + bench('create ref', () => { + ref(100) + }) + + { + let i = 0 + const v = ref(100) + bench('write ref', () => { + v.value = i++ + }) + } + + { + const v = ref(100) + bench('read ref', () => { + v.value + }) + } + + { + let i = 0 + const v = ref(100) + bench('write/read ref', () => { + v.value = i++ + + v.value + }) + } +}) diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index d365b673360..0dffcf35889 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -101,19 +101,25 @@ class BaseReactiveHandler implements ProxyHandler { return isReadonly } else if (key === ReactiveFlags.IS_SHALLOW) { return shallow - } else if ( - key === ReactiveFlags.RAW && - receiver === - (isReadonly - ? shallow - ? shallowReadonlyMap - : readonlyMap - : shallow - ? shallowReactiveMap - : reactiveMap - ).get(target) - ) { - return target + } else if (key === ReactiveFlags.RAW) { + if ( + receiver === + (isReadonly + ? shallow + ? shallowReadonlyMap + : readonlyMap + : shallow + ? shallowReactiveMap + : reactiveMap + ).get(target) || + // receiver is not the reactive proxy, but has the same prototype + // this means the reciever is a user proxy of the reactive proxy + Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver) + ) { + return target + } + // early return undefined + return } const targetIsArray = isArray(target) @@ -169,17 +175,19 @@ class MutableReactiveHandler extends BaseReactiveHandler { receiver: object ): boolean { let oldValue = (target as any)[key] - if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) { - return false - } if (!this._shallow) { + const isOldValueReadonly = isReadonly(oldValue) if (!isShallow(value) && !isReadonly(value)) { oldValue = toRaw(oldValue) value = toRaw(value) } if (!isArray(target) && isRef(oldValue) && !isRef(value)) { - oldValue.value = value - return true + if (isOldValueReadonly) { + return false + } else { + oldValue.value = value + return true + } } } else { // in shallow mode, objects are set as-is regardless of reactive or not diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 56dc7667c99..eb399dffdbd 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -100,7 +100,6 @@ export function isRef(r: any): r is Ref { * @param value - The object to wrap in the ref. * @see {@link https://vuejs.org/api/reactivity-core.html#ref} */ -export function ref(value: T): T export function ref(value: T): Ref> export function ref(): Ref export function ref(value?: unknown) { diff --git a/packages/runtime-core/__tests__/apiWatch.bench.ts b/packages/runtime-core/__tests__/apiWatch.bench.ts new file mode 100644 index 00000000000..89693a446ac --- /dev/null +++ b/packages/runtime-core/__tests__/apiWatch.bench.ts @@ -0,0 +1,58 @@ +import { nextTick, ref, watch, watchEffect } from '../src' +import { bench } from 'vitest' + +bench('create watcher', () => { + const v = ref(100) + watch(v, v => {}) +}) + +{ + const v = ref(100) + watch(v, v => {}) + let i = 0 + bench('update ref to trigger watcher (scheduled but not executed)', () => { + v.value = i++ + }) +} + +{ + const v = ref(100) + watch(v, v => {}) + let i = 0 + bench('update ref to trigger watcher (executed)', async () => { + v.value = i++ + return nextTick() + }) +} + +{ + bench('create watchEffect', () => { + watchEffect(() => {}) + }) +} + +{ + const v = ref(100) + watchEffect(() => { + v.value + }) + let i = 0 + bench( + 'update ref to trigger watchEffect (scheduled but not executed)', + () => { + v.value = i++ + } + ) +} + +{ + const v = ref(100) + watchEffect(() => { + v.value + }) + let i = 0 + bench('update ref to trigger watchEffect (executed)', async () => { + v.value = i++ + await nextTick() + }) +} diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 1bc012bb36b..0f5782e3009 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -549,6 +549,98 @@ describe('api: watch', () => { expect(cb).not.toHaveBeenCalled() }) + // #7030 + it('should not fire on child component unmount w/ flush: pre', async () => { + const visible = ref(true) + const cb = vi.fn() + const Parent = defineComponent({ + props: ['visible'], + render() { + return visible.value ? h(Comp) : null + } + }) + const Comp = { + setup() { + watch(visible, cb, { flush: 'pre' }) + }, + render() {} + } + const App = { + render() { + return h(Parent, { + visible: visible.value + }) + } + } + render(h(App), nodeOps.createElement('div')) + expect(cb).not.toHaveBeenCalled() + visible.value = false + await nextTick() + expect(cb).not.toHaveBeenCalled() + }) + + // #7030 + it('flush: pre watcher in child component should not fire before parent update', async () => { + const b = ref(0) + const calls: string[] = [] + + const Comp = { + setup() { + watch( + () => b.value, + val => { + calls.push('watcher child') + }, + { flush: 'pre' } + ) + return () => { + b.value + calls.push('render child') + } + } + } + + const Parent = { + props: ['a'], + setup() { + watch( + () => b.value, + val => { + calls.push('watcher parent') + }, + { flush: 'pre' } + ) + return () => { + b.value + calls.push('render parent') + return h(Comp) + } + } + } + + const App = { + render() { + return h(Parent, { + a: b.value + }) + } + } + + render(h(App), nodeOps.createElement('div')) + expect(calls).toEqual(['render parent', 'render child']) + + b.value++ + await nextTick() + expect(calls).toEqual([ + 'render parent', + 'render child', + 'watcher parent', + 'render parent', + 'watcher child', + 'render child' + ]) + }) + // #1763 it('flush: pre watcher watching props should fire before child update', async () => { const a = ref(0) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index d822a992816..6fec755106a 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -1185,6 +1185,72 @@ describe('Suspense', () => { expect(calls).toEqual([`one mounted`, `one unmounted`, `two mounted`]) }) + test('mount the fallback content is in the correct position', async () => { + const makeComp = (name: string, delay = 0) => + defineAsyncComponent( + { + setup() { + return () => h('div', [name]) + } + }, + delay + ) + + const One = makeComp('one') + const Two = makeComp('two', 20) + + const view = shallowRef(One) + + const Comp = { + setup() { + return () => + h('div', [ + h( + Suspense, + { + timeout: 10 + }, + { + default: h(view.value), + fallback: h('div', 'fallback') + } + ), + h('div', 'three') + ]) + } + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe( + `
fallback
three
` + ) + + await deps[0] + await nextTick() + expect(serializeInner(root)).toBe( + `
one
three
` + ) + + view.value = Two + await nextTick() + expect(serializeInner(root)).toBe( + `
one
three
` + ) + + await new Promise(r => setTimeout(r, 10)) + await nextTick() + expect(serializeInner(root)).toBe( + `
fallback
three
` + ) + + await deps[1] + await nextTick() + expect(serializeInner(root)).toBe( + `
two
three
` + ) + }) + // #2214 // Since suspense renders its own root like a component, it should not patch // its content in optimized mode. diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index e3910182509..2c2f0d8326b 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -66,9 +66,9 @@ const warnRuntimeUsage = (method: string) => * foo?: string * bar: number * }>() + * ``` * * @see {@link https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits} - * ``` * * This is only usable inside `