@@ -129,6 +129,7 @@ export function createTransformContext(
filename = '',
prefixIdentifiers = false,
hoistStatic = false,
+ hmr = false,
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
@@ -155,6 +156,7 @@ export function createTransformContext(
selfName: nameMatch && capitalize(camelize(nameMatch[1])),
prefixIdentifiers,
hoistStatic,
+ hmr,
cacheHandlers,
nodeTransforms,
directiveTransforms,
@@ -181,7 +183,7 @@ export function createTransformContext(
directives: new Set(),
hoists: [],
imports: [],
- constantCache: new Map(),
+ constantCache: new WeakMap(),
temps: 0,
cached: 0,
identifiers: Object.create(null),
diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts
index 5526163c6f9..fd443496ca7 100644
--- a/packages/compiler-core/src/transforms/hoistStatic.ts
+++ b/packages/compiler-core/src/transforms/hoistStatic.ts
@@ -140,9 +140,16 @@ function walk(
node.codegenNode.type === NodeTypes.VNODE_CALL &&
isArray(node.codegenNode.children)
) {
- node.codegenNode.children = context.hoist(
+ const hoisted = context.hoist(
createArrayExpression(node.codegenNode.children)
)
+ // #6978, #7138, #7114
+ // a hoisted children array inside v-for can caused HMR errors since
+ // it might be mutated when mounting the v-for list
+ if (context.hmr) {
+ hoisted.content = `[...${hoisted.content}]`
+ }
+ node.codegenNode.children = hoisted
}
}
diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts
index c4416dd45f7..ffa90ea1171 100644
--- a/packages/compiler-core/src/transforms/vSlot.ts
+++ b/packages/compiler-core/src/transforms/vSlot.ts
@@ -100,11 +100,12 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
export type SlotFnBuilder = (
slotProps: ExpressionNode | undefined,
+ vForExp: ExpressionNode | undefined,
slotChildren: TemplateChildNode[],
loc: SourceLocation
) => FunctionExpression
-const buildClientSlotFn: SlotFnBuilder = (props, children, loc) =>
+const buildClientSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) =>
createFunctionExpression(
props,
children,
@@ -149,7 +150,7 @@ export function buildSlots(
slotsProperties.push(
createObjectProperty(
arg || createSimpleExpression('default', true),
- buildSlotFn(exp, children, loc)
+ buildSlotFn(exp, undefined, children, loc)
)
)
}
@@ -201,11 +202,17 @@ export function buildSlots(
hasDynamicSlots = true
}
- const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc)
+ const vFor = findDir(slotElement, 'for')
+ const slotFunction = buildSlotFn(
+ slotProps,
+ vFor?.exp,
+ slotChildren,
+ slotLoc
+ )
+
// check if this slot is conditional (v-if/v-for)
let vIf: DirectiveNode | undefined
let vElse: DirectiveNode | undefined
- let vFor: DirectiveNode | undefined
if ((vIf = findDir(slotElement, 'if'))) {
hasDynamicSlots = true
dynamicSlots.push(
@@ -257,7 +264,7 @@ export function buildSlots(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc)
)
}
- } else if ((vFor = findDir(slotElement, 'for'))) {
+ } else if (vFor) {
hasDynamicSlots = true
const parseResult =
vFor.parseResult ||
@@ -306,7 +313,7 @@ export function buildSlots(
props: ExpressionNode | undefined,
children: TemplateChildNode[]
) => {
- const fn = buildSlotFn(props, children, loc)
+ const fn = buildSlotFn(props, undefined, children, loc)
if (__COMPAT__ && context.compatConfig) {
fn.isNonScopedSlot = true
}
diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json
index d21043af9e6..f39057c076b 100644
--- a/packages/compiler-dom/package.json
+++ b/packages/compiler-dom/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
- "version": "3.3.4",
+ "version": "3.4.0-alpha.1",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",
@@ -37,7 +37,7 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme",
"dependencies": {
- "@vue/shared": "3.3.4",
- "@vue/compiler-core": "3.3.4"
+ "@vue/shared": "3.4.0-alpha.1",
+ "@vue/compiler-core": "3.4.0-alpha.1"
}
}
diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts
index 2c6f71cefbb..a2f4aff2e4c 100644
--- a/packages/compiler-dom/src/index.ts
+++ b/packages/compiler-dom/src/index.ts
@@ -68,5 +68,9 @@ export function parse(template: string, options: ParserOptions = {}): RootNode {
export * from './runtimeHelpers'
export { transformStyle } from './transforms/transformStyle'
-export { createDOMCompilerError, DOMErrorCodes } from './errors'
+export {
+ createDOMCompilerError,
+ DOMErrorCodes,
+ DOMErrorMessages
+} from './errors'
export * from '@vue/compiler-core'
diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap
index fdfd3710efc..297ee62724c 100644
--- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineModel.spec.ts.snap
@@ -7,15 +7,17 @@ export default {
props: {
\\"modelValue\\": { required: true },
\\"count\\": {},
+ \\"toString\\": { type: Function },
},
- emits: [\\"update:modelValue\\", \\"update:count\\"],
+ emits: [\\"update:modelValue\\", \\"update:count\\", \\"update:toString\\"],
setup(__props, { expose: __expose }) {
__expose();
const modelValue = _useModel(__props, \\"modelValue\\")
const c = _useModel(__props, \\"count\\")
+ const toString = _useModel(__props, \\"toString\\")
-return { modelValue, c }
+return { modelValue, c, toString }
}
}"
diff --git a/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts
index 61a9adcbe0d..10fab947c13 100644
--- a/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScript/defineModel.spec.ts
@@ -8,6 +8,7 @@ describe('defineModel()', () => {
`,
{ defineModel: true }
@@ -16,18 +17,22 @@ describe('defineModel()', () => {
expect(content).toMatch('props: {')
expect(content).toMatch('"modelValue": { required: true },')
expect(content).toMatch('"count": {},')
- expect(content).toMatch('emits: ["update:modelValue", "update:count"],')
+ expect(content).toMatch('"toString": { type: Function },')
+ expect(content).toMatch(
+ 'emits: ["update:modelValue", "update:count", "update:toString"],'
+ )
expect(content).toMatch(
`const modelValue = _useModel(__props, "modelValue")`
)
expect(content).toMatch(`const c = _useModel(__props, "count")`)
- expect(content).toMatch(`return { modelValue, c }`)
+ expect(content).toMatch(`return { modelValue, c, toString }`)
expect(content).not.toMatch('defineModel')
expect(bindings).toStrictEqual({
modelValue: BindingTypes.SETUP_REF,
count: BindingTypes.PROPS,
- c: BindingTypes.SETUP_REF
+ c: BindingTypes.SETUP_REF,
+ toString: BindingTypes.SETUP_REF
})
})
diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
index 607654a952b..fc600f1a518 100644
--- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
@@ -1,3 +1,4 @@
+import { normalize } from 'node:path'
import { Identifier } from '@babel/types'
import { SFCScriptCompileOptions, parse } from '../../src'
import { ScriptCompileContext } from '../../src/script/context'
@@ -478,6 +479,33 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(Object.keys(files))
})
+ test.runIf(process.platform === 'win32')('relative ts on Windows', () => {
+ const files = {
+ 'C:\\Test\\foo.ts': 'export type P = { foo: number }',
+ 'C:\\Test\\bar.d.ts':
+ 'type X = { bar: string }; export { X as Y };' +
+ // verify that we can parse syntax that is only valid in d.ts
+ 'export const baz: boolean'
+ }
+ const { props, deps } = resolve(
+ `
+ import { P } from './foo'
+ import { Y as PP } from './bar'
+ defineProps()
+ `,
+ files,
+ {},
+ 'C:\\Test\\Test.vue'
+ )
+ expect(props).toStrictEqual({
+ foo: ['Number'],
+ bar: ['String']
+ })
+ expect(deps && [...deps].map(normalize)).toStrictEqual(
+ Object.keys(files).map(normalize)
+ )
+ })
+
// #8244
test('utility type in external file', () => {
const files = {
@@ -898,19 +926,20 @@ describe('resolveType', () => {
function resolve(
code: string,
files: Record = {},
- options?: Partial
+ options?: Partial,
+ sourceFileName: string = '/Test.vue'
) {
const { descriptor } = parse(``, {
- filename: '/Test.vue'
+ filename: sourceFileName
})
const ctx = new ScriptCompileContext(descriptor, {
id: 'test',
fs: {
fileExists(file) {
- return !!files[file]
+ return !!(files[file] ?? files[normalize(file)])
},
readFile(file) {
- return files[file]
+ return files[file] ?? files[normalize(file)]
}
},
...options
diff --git a/packages/compiler-sfc/__tests__/cssVars.spec.ts b/packages/compiler-sfc/__tests__/cssVars.spec.ts
index 5b01d73d772..9fb72d7ad50 100644
--- a/packages/compiler-sfc/__tests__/cssVars.spec.ts
+++ b/packages/compiler-sfc/__tests__/cssVars.spec.ts
@@ -272,5 +272,73 @@ describe('CSS vars injection', () => {
`export default {\n setup(__props, { expose: __expose }) {\n __expose();\n\n_useCssVars(_ctx => ({\n "xxxxxxxx-background": (_unref(background))\n}))`
)
})
+
+ describe('skip codegen in SSR', () => {
+ test('script setup, inline', () => {
+ const { content } = compileSFCScript(
+ `\n` +
+ ``,
+ {
+ inlineTemplate: true,
+ templateOptions: {
+ ssr: true
+ }
+ }
+ )
+ expect(content).not.toMatch(`_useCssVars`)
+ })
+
+ // #6926
+ test('script, non-inline', () => {
+ const { content } = compileSFCScript(
+ `\n` +
+ ``,
+ {
+ inlineTemplate: false,
+ templateOptions: {
+ ssr: true
+ }
+ }
+ )
+ expect(content).not.toMatch(`_useCssVars`)
+ })
+
+ test('normal script', () => {
+ const { content } = compileSFCScript(
+ `\n` +
+ ``,
+ {
+ templateOptions: {
+ ssr: true
+ }
+ }
+ )
+ expect(content).not.toMatch(`_useCssVars`)
+ })
+ })
})
})
diff --git a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts
index f267e73ede0..44c13e47ea2 100644
--- a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts
+++ b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts
@@ -166,4 +166,34 @@ describe('compiler sfc: transform asset url', () => {
expect(code).toMatch(`_createStaticVNode`)
expect(code).toMatchSnapshot()
})
+
+ test('transform with stringify with space in absolute filename', () => {
+ const { code } = compileWithAssetUrls(
+ ``,
+ {
+ includeAbsolute: true
+ },
+ {
+ hoistStatic: true,
+ transformHoist: stringifyStatic
+ }
+ )
+ expect(code).toMatch(`_createElementVNode`)
+ expect(code).toContain(`import _imports_0 from '/foo bar.png'`)
+ })
+
+ test('transform with stringify with space in relative filename', () => {
+ const { code } = compileWithAssetUrls(
+ ``,
+ {
+ includeAbsolute: true
+ },
+ {
+ hoistStatic: true,
+ transformHoist: stringifyStatic
+ }
+ )
+ expect(code).toMatch(`_createElementVNode`)
+ expect(code).toContain(`import _imports_0 from './foo bar.png'`)
+ })
})
diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json
index 33a8c40d185..6c28d1c7f03 100644
--- a/packages/compiler-sfc/package.json
+++ b/packages/compiler-sfc/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
- "version": "3.3.4",
+ "version": "3.4.0-alpha.1",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"module": "dist/compiler-sfc.esm-browser.js",
@@ -32,29 +32,27 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme",
"dependencies": {
- "@babel/parser": "^7.20.15",
- "@vue/compiler-core": "3.3.4",
- "@vue/compiler-dom": "3.3.4",
- "@vue/compiler-ssr": "3.3.4",
- "@vue/reactivity-transform": "3.3.4",
- "@vue/shared": "3.3.4",
+ "@babel/parser": "^7.23.0",
+ "@vue/compiler-core": "3.4.0-alpha.1",
+ "@vue/compiler-dom": "3.4.0-alpha.1",
+ "@vue/compiler-ssr": "3.4.0-alpha.1",
+ "@vue/reactivity-transform": "3.4.0-alpha.1",
+ "@vue/shared": "3.4.0-alpha.1",
"estree-walker": "^2.0.2",
- "magic-string": "^0.30.0",
- "postcss": "^8.1.10",
+ "magic-string": "^0.30.5",
+ "postcss": "^8.4.31",
"source-map-js": "^1.0.2"
},
"devDependencies": {
- "@babel/types": "^7.21.3",
- "@types/estree": "^0.0.48",
- "@types/lru-cache": "^5.1.0",
+ "@babel/types": "^7.23.0",
"@vue/consolidate": "^0.17.3",
"hash-sum": "^2.0.0",
- "lru-cache": "^5.1.1",
+ "lru-cache": "^10.0.1",
"merge-source-map": "^1.1.0",
- "minimatch": "^9.0.0",
- "postcss-modules": "^4.0.0",
- "postcss-selector-parser": "^6.0.4",
- "pug": "^3.0.1",
- "sass": "^1.26.9"
+ "minimatch": "^9.0.3",
+ "postcss-modules": "^4.3.1",
+ "postcss-selector-parser": "^6.0.13",
+ "pug": "^3.0.2",
+ "sass": "^1.69.4"
}
}
diff --git a/packages/compiler-sfc/src/cache.ts b/packages/compiler-sfc/src/cache.ts
index 36d240810c7..f04b231f6d9 100644
--- a/packages/compiler-sfc/src/cache.ts
+++ b/packages/compiler-sfc/src/cache.ts
@@ -1,11 +1,10 @@
-import LRU from 'lru-cache'
+import { LRUCache } from 'lru-cache'
-export function createCache(size = 500): Map & { max?: number } {
+export function createCache(
+ max = 500
+): Map | LRUCache {
if (__GLOBAL__ || __ESM_BROWSER__) {
return new Map()
}
- const cache = new LRU(size)
- // @ts-expect-error
- cache.delete = cache.del.bind(cache)
- return cache as any as Map
+ return new LRUCache({ max })
}
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index cfcc607c72d..2a33f69936d 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -765,7 +765,7 @@ export function compileScript(
if (
sfc.cssVars.length &&
// no need to do this when targeting SSR
- !(options.inlineTemplate && options.templateOptions?.ssr)
+ !options.templateOptions?.ssr
) {
ctx.helperImports.add(CSS_VARS_HELPER)
ctx.helperImports.add('unref')
diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts
index fbd100c9784..b036619c794 100644
--- a/packages/compiler-sfc/src/compileTemplate.ts
+++ b/packages/compiler-sfc/src/compileTemplate.ts
@@ -212,6 +212,7 @@ function doCompileTemplate({
slotted,
sourceMap: true,
...compilerOptions,
+ hmr: !isProd,
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
filename,
onError: e => errors.push(e),
diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts
index 76b4900d46d..c6ee604146e 100644
--- a/packages/compiler-sfc/src/index.ts
+++ b/packages/compiler-sfc/src/index.ts
@@ -33,6 +33,8 @@ export {
// Internals for type resolution
export { invalidateTypeCache, registerTS } from './script/resolveType'
+export { extractRuntimeProps } from './script/defineProps'
+export { extractRuntimeEmits } from './script/defineEmits'
// Types
export type {
@@ -58,6 +60,7 @@ export type { SFCScriptCompileOptions } from './compileScript'
export type { ScriptCompileContext } from './script/context'
export type {
TypeResolveContext,
+ SimpleTypeResolveOptions,
SimpleTypeResolveContext
} from './script/resolveType'
export type {
diff --git a/packages/compiler-sfc/src/rewriteDefault.ts b/packages/compiler-sfc/src/rewriteDefault.ts
index ae5e7366bde..277eedce011 100644
--- a/packages/compiler-sfc/src/rewriteDefault.ts
+++ b/packages/compiler-sfc/src/rewriteDefault.ts
@@ -37,7 +37,7 @@ export function rewriteDefaultAST(
// multi-line comments or template strings. fallback to a full parse.
ast.forEach(node => {
if (node.type === 'ExportDefaultDeclaration') {
- if (node.declaration.type === 'ClassDeclaration') {
+ if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
let start: number =
node.declaration.decorators && node.declaration.decorators.length > 0
? node.declaration.decorators[
diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts
index 5fe09d28a42..692eab3ab9e 100644
--- a/packages/compiler-sfc/src/script/context.ts
+++ b/packages/compiler-sfc/src/script/context.ts
@@ -53,7 +53,7 @@ export class ScriptCompileContext {
emitDecl: Node | undefined
// defineModel
- modelDecls: Record = {}
+ modelDecls: Record = Object.create(null)
// defineOptions
optionsRuntimeDecl: Node | undefined
diff --git a/packages/compiler-sfc/src/script/defineEmits.ts b/packages/compiler-sfc/src/script/defineEmits.ts
index 02014d1b276..b7453076cfe 100644
--- a/packages/compiler-sfc/src/script/defineEmits.ts
+++ b/packages/compiler-sfc/src/script/defineEmits.ts
@@ -1,7 +1,18 @@
-import { Identifier, LVal, Node, RestElement } from '@babel/types'
+import {
+ ArrayPattern,
+ Identifier,
+ LVal,
+ Node,
+ ObjectPattern,
+ RestElement
+} from '@babel/types'
import { isCallOf } from './utils'
import { ScriptCompileContext } from './context'
-import { resolveTypeElements, resolveUnionType } from './resolveType'
+import {
+ TypeResolveContext,
+ resolveTypeElements,
+ resolveUnionType
+} from './resolveType'
export const DEFINE_EMITS = 'defineEmits'
@@ -57,7 +68,7 @@ export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
return emitsDecl
}
-function extractRuntimeEmits(ctx: ScriptCompileContext): Set {
+export function extractRuntimeEmits(ctx: TypeResolveContext): Set {
const emits = new Set()
const node = ctx.emitsTypeDecl!
@@ -90,8 +101,8 @@ function extractRuntimeEmits(ctx: ScriptCompileContext): Set {
}
function extractEventNames(
- ctx: ScriptCompileContext,
- eventName: Identifier | RestElement,
+ ctx: TypeResolveContext,
+ eventName: ArrayPattern | Identifier | ObjectPattern | RestElement,
emits: Set
) {
if (
diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts
index 5004e314da1..449ed250d1d 100644
--- a/packages/compiler-sfc/src/script/defineProps.ts
+++ b/packages/compiler-sfc/src/script/defineProps.ts
@@ -8,7 +8,11 @@ import {
} from '@babel/types'
import { BindingTypes, isFunctionType } from '@vue/compiler-dom'
import { ScriptCompileContext } from './context'
-import { inferRuntimeType, resolveTypeElements } from './resolveType'
+import {
+ TypeResolveContext,
+ inferRuntimeType,
+ resolveTypeElements
+} from './resolveType'
import {
resolveObjectKey,
UNKNOWN_TYPE,
@@ -150,7 +154,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
}
}
} else if (ctx.propsTypeDecl) {
- propsDecls = genRuntimePropsFromTypes(ctx)
+ propsDecls = extractRuntimeProps(ctx)
}
const modelsDecls = genModelProps(ctx)
@@ -162,7 +166,9 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
}
}
-function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
+export function extractRuntimeProps(
+ ctx: TypeResolveContext
+): string | undefined {
// this is only called if propsTypeDecl exists
const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!)
if (!props.length) {
@@ -175,7 +181,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
for (const prop of props) {
propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
// register bindings
- if (!(prop.key in ctx.bindingMetadata)) {
+ if ('bindingMetadata' in ctx && !(prop.key in ctx.bindingMetadata)) {
ctx.bindingMetadata[prop.key] = BindingTypes.PROPS
}
}
@@ -193,7 +199,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) {
}
function resolveRuntimePropsFromType(
- ctx: ScriptCompileContext,
+ ctx: TypeResolveContext,
node: Node
): PropTypeData[] {
const props: PropTypeData[] = []
@@ -222,7 +228,7 @@ function resolveRuntimePropsFromType(
}
function genRuntimePropFromType(
- ctx: ScriptCompileContext,
+ ctx: TypeResolveContext,
{ key, required, type, skipCheck }: PropTypeData,
hasStaticDefaults: boolean
): string {
@@ -284,7 +290,7 @@ function genRuntimePropFromType(
* static properties, we can directly generate more optimized default
* declarations. Otherwise we will have to fallback to runtime merging.
*/
-function hasStaticWithDefaults(ctx: ScriptCompileContext) {
+function hasStaticWithDefaults(ctx: TypeResolveContext) {
return !!(
ctx.propsRuntimeDefaults &&
ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
@@ -297,7 +303,7 @@ function hasStaticWithDefaults(ctx: ScriptCompileContext) {
}
function genDestructuredDefaultValue(
- ctx: ScriptCompileContext,
+ ctx: TypeResolveContext,
key: string,
inferredType?: string[]
):
diff --git a/packages/compiler-sfc/src/script/normalScript.ts b/packages/compiler-sfc/src/script/normalScript.ts
index 76b25c66350..d0f16134273 100644
--- a/packages/compiler-sfc/src/script/normalScript.ts
+++ b/packages/compiler-sfc/src/script/normalScript.ts
@@ -55,7 +55,7 @@ export function processNormalScript(
const s = new MagicString(content)
rewriteDefaultAST(scriptAst.body, s, defaultVar)
content = s.toString()
- if (cssVars.length) {
+ if (cssVars.length && !ctx.options.templateOptions?.ssr) {
content += genNormalScriptCssVarsCode(
cssVars,
bindings,
diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts
index 146c454729c..12666341e73 100644
--- a/packages/compiler-sfc/src/script/resolveType.ts
+++ b/packages/compiler-sfc/src/script/resolveType.ts
@@ -42,6 +42,13 @@ import type TS from 'typescript'
import { extname, dirname } from 'path'
import { minimatch as isMatch } from 'minimatch'
+export type SimpleTypeResolveOptions = Partial<
+ Pick<
+ SFCScriptCompileOptions,
+ 'globalTypeFiles' | 'fs' | 'babelParserPlugins' | 'isProd'
+ >
+>
+
/**
* TypeResolveContext is compatible with ScriptCompileContext
* but also allows a simpler version of it with minimal required properties
@@ -59,13 +66,28 @@ import { minimatch as isMatch } from 'minimatch'
*/
export type SimpleTypeResolveContext = Pick<
ScriptCompileContext,
- // required
- 'source' | 'filename' | 'error' | 'options'
+ // file
+ | 'source'
+ | 'filename'
+
+ // utils
+ | 'error'
+ | 'helper'
+ | 'getString'
+
+ // props
+ | 'propsTypeDecl'
+ | 'propsRuntimeDefaults'
+ | 'propsDestructuredBindings'
+
+ // emits
+ | 'emitsTypeDecl'
> &
Partial<
Pick
> & {
ast: Statement[]
+ options: SimpleTypeResolveOptions
}
export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext
@@ -778,7 +800,7 @@ function importSourceToScope(
if (!resolved) {
if (source.startsWith('.')) {
// relative import - fast path
- const filename = joinPaths(scope.filename, '..', source)
+ const filename = joinPaths(dirname(scope.filename), source)
resolved = resolveExt(filename, fs)
} else {
// module or aliased import - use full TS resolution, only supported in Node
@@ -1227,7 +1249,7 @@ function recordType(
break
}
case 'ClassDeclaration':
- types[overwriteId || getId(node.id)] = node
+ if (overwriteId || node.id) types[overwriteId || getId(node.id!)] = node
break
case 'TSTypeAliasDeclaration':
types[node.id.name] = node.typeAnnotation
diff --git a/packages/compiler-sfc/src/template/transformAssetUrl.ts b/packages/compiler-sfc/src/template/transformAssetUrl.ts
index 32bf33bcea1..4267d4ea352 100644
--- a/packages/compiler-sfc/src/template/transformAssetUrl.ts
+++ b/packages/compiler-sfc/src/template/transformAssetUrl.ts
@@ -168,7 +168,13 @@ function getImportsExpressionExp(
loc,
ConstantTypes.CAN_STRINGIFY
)
- context.imports.push({ exp, path })
+
+ // We need to ensure the path is not encoded (to %2F),
+ // so we decode it back in case it is encoded
+ context.imports.push({
+ exp,
+ path: decodeURIComponent(path)
+ })
}
if (!hash) {
diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
index 9391c01e37e..a8ea08a5349 100644
--- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
@@ -181,11 +181,14 @@ describe('ssr: components', () => {
})
test('v-for slot', () => {
- expect(
- compile(`
- {{ msg + key + bar }}
- `).code
- ).toMatchInlineSnapshot(`
+ const { code } = compile(`
+ {{ msg + key + index + bar }}
+ `)
+ expect(code).not.toMatch(`_ctx.msg`)
+ expect(code).not.toMatch(`_ctx.key`)
+ expect(code).not.toMatch(`_ctx.index`)
+ expect(code).toMatch(`_ctx.bar`)
+ expect(code).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, renderList: _renderList, createSlots: _createSlots } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require(\\"vue/server-renderer\\")
@@ -193,15 +196,15 @@ describe('ssr: components', () => {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, _attrs, _createSlots({ _: 2 /* DYNAMIC */ }, [
- _renderList(_ctx.names, (key) => {
+ _renderList(_ctx.names, (key, index) => {
return {
name: key,
fn: _withCtx(({ msg }, _push, _parent, _scopeId) => {
if (_push) {
- _push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`)
+ _push(\`\${_ssrInterpolate(msg + key + index + _ctx.bar)}\`)
} else {
return [
- _createTextVNode(_toDisplayString(msg + _ctx.key + _ctx.bar), 1 /* TEXT */)
+ _createTextVNode(_toDisplayString(msg + key + index + _ctx.bar), 1 /* TEXT */)
]
}
})
diff --git a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts
index 3445a84fda9..1be6a2c180c 100644
--- a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts
@@ -124,4 +124,48 @@ describe('ssr: scopeId', () => {
}"
`)
})
+
+ // #7554
+ test('scopeId is correctly transform to scope attribute of transition-group ', () => {
+ expect(
+ compile(
+ `hello`,
+ {
+ scopeId,
+ mode: 'module'
+ }
+ ).code
+ ).toMatchInlineSnapshot(`
+ "import { mergeProps as _mergeProps } from \\"vue\\"
+ import { ssrRenderAttrs as _ssrRenderAttrs } from \\"vue/server-renderer\\"
+
+ export function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`hello
\`)
+ }"
+ `)
+
+ // with dynamic tag
+ expect(
+ compile(
+ `hello`,
+ {
+ scopeId,
+ mode: 'module'
+ }
+ ).code
+ ).toMatchInlineSnapshot(`
+ "import { mergeProps as _mergeProps } from \\"vue\\"
+ import { ssrRenderAttrs as _ssrRenderAttrs } from \\"vue/server-renderer\\"
+
+ export function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`<\${
+ _ctx.someTag
+ }\${
+ _ssrRenderAttrs(_mergeProps({ class: \\"red\\" }, _attrs))
+ } data-v-xxxxxxx>hello\${
+ _ctx.someTag
+ }>\`)
+ }"
+ `)
+ })
})
diff --git a/packages/compiler-ssr/__tests__/ssrTransition.spec.ts b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts
new file mode 100644
index 00000000000..319b3902239
--- /dev/null
+++ b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts
@@ -0,0 +1,25 @@
+import { compile } from '../src'
+
+describe('transition', () => {
+ test('basic', () => {
+ expect(compile(`foo
`).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`foo
\`)
+ }"
+ `)
+ })
+
+ test('with appear', () => {
+ expect(compile(`foo
`).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`foo
\`)
+ }"
+ `)
+ })
+})
diff --git a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts
index 5bccbcb788c..f28c38d4026 100644
--- a/packages/compiler-ssr/__tests__/ssrVModel.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrVModel.spec.ts
@@ -69,6 +69,57 @@ describe('ssr: v-model', () => {
}>\`)
}"
`)
+
+ expect(
+ compileWithWrapper(``)
+ .code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderSlot: _ssrRenderSlot, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ }"
+ `)
+
+ expect(
+ compileWithWrapper(`
+ `).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ }"
+ `)
+
+ expect(
+ compileWithWrapper(`
+ `).code
+ ).toMatchInlineSnapshot(`
+ "const { ssrRenderSlot: _ssrRenderSlot, ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`\`)
+ }"
+ `)
})
test('', () => {
diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json
index c8e4d33bf4e..df467affd10 100644
--- a/packages/compiler-ssr/package.json
+++ b/packages/compiler-ssr/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
- "version": "3.3.4",
+ "version": "3.4.0-alpha.1",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",
@@ -28,7 +28,7 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme",
"dependencies": {
- "@vue/shared": "3.3.4",
- "@vue/compiler-dom": "3.3.4"
+ "@vue/shared": "3.4.0-alpha.1",
+ "@vue/compiler-dom": "3.4.0-alpha.1"
}
}
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
index dc8c6a4ae4f..7a12cb29009 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
@@ -56,6 +56,10 @@ import {
} from './ssrTransformTransitionGroup'
import { isSymbol, isObject, isArray } from '@vue/shared'
import { buildSSRProps } from './ssrTransformElement'
+import {
+ ssrProcessTransition,
+ ssrTransformTransition
+} from './ssrTransformTransition'
// We need to construct the slot functions in the 1st pass to ensure proper
// scope tracking, but the children of each slot cannot be processed until
@@ -99,9 +103,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
if (isSymbol(component)) {
if (component === SUSPENSE) {
return ssrTransformSuspense(node, context)
- }
- if (component === TRANSITION_GROUP) {
+ } else if (component === TRANSITION_GROUP) {
return ssrTransformTransitionGroup(node, context)
+ } else if (component === TRANSITION) {
+ return ssrTransformTransition(node, context)
}
return // other built-in components: fallthrough
}
@@ -120,8 +125,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
// fallback in case the child is render-fn based). Store them in an array
// for later use.
if (clonedNode.children.length) {
- buildSlots(clonedNode, context, (props, children) => {
- vnodeBranches.push(createVNodeSlotBranch(props, children, context))
+ buildSlots(clonedNode, context, (props, vFor, children) => {
+ vnodeBranches.push(
+ createVNodeSlotBranch(props, vFor, children, context)
+ )
return createFunctionExpression(undefined)
})
}
@@ -145,7 +152,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
const wipEntries: WIPSlotEntry[] = []
wipMap.set(node, wipEntries)
- const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
+ const buildSSRSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => {
const param0 = (props && stringifyExpression(props)) || `_`
const fn = createFunctionExpression(
[param0, `_push`, `_parent`, `_scopeId`],
@@ -216,9 +223,8 @@ export function ssrProcessComponent(
if ((parent as WIPSlotEntry).type === WIP_SLOT) {
context.pushStringPart(``)
}
- // #5351: filter out comment children inside transition
if (component === TRANSITION) {
- node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
+ return ssrProcessTransition(node, context)
}
processChildren(node, context)
}
@@ -273,6 +279,7 @@ const vnodeDirectiveTransforms = {
function createVNodeSlotBranch(
props: ExpressionNode | undefined,
+ vForExp: ExpressionNode | undefined,
children: TemplateChildNode[],
parentContext: TransformContext
): ReturnStatement {
@@ -299,8 +306,8 @@ function createVNodeSlotBranch(
tag: 'template',
tagType: ElementTypes.TEMPLATE,
isSelfClosing: false,
- // important: provide v-slot="props" on the wrapper for proper
- // scope analysis
+ // important: provide v-slot="props" and v-for="exp" on the wrapper for
+ // proper scope analysis
props: [
{
type: NodeTypes.DIRECTIVE,
@@ -309,6 +316,14 @@ function createVNodeSlotBranch(
arg: undefined,
modifiers: [],
loc: locStub
+ },
+ {
+ type: NodeTypes.DIRECTIVE,
+ name: 'for',
+ exp: vForExp,
+ arg: undefined,
+ modifiers: [],
+ loc: locStub
}
],
children,
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts
index 207e9348eef..e7efbe1fb73 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformSuspense.ts
@@ -36,20 +36,24 @@ export function ssrTransformSuspense(
wipSlots: []
}
wipMap.set(node, wipEntry)
- wipEntry.slotsExp = buildSlots(node, context, (_props, children, loc) => {
- const fn = createFunctionExpression(
- [],
- undefined, // no return, assign body later
- true, // newline
- false, // suspense slots are not treated as normal slots
- loc
- )
- wipEntry.wipSlots.push({
- fn,
- children
- })
- return fn
- }).slots
+ wipEntry.slotsExp = buildSlots(
+ node,
+ context,
+ (_props, _vForExp, children, loc) => {
+ const fn = createFunctionExpression(
+ [],
+ undefined, // no return, assign body later
+ true, // newline
+ false, // suspense slots are not treated as normal slots
+ loc
+ )
+ wipEntry.wipSlots.push({
+ fn,
+ children
+ })
+ return fn
+ }
+ ).slots
}
}
}
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts
new file mode 100644
index 00000000000..d09a806f7b0
--- /dev/null
+++ b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts
@@ -0,0 +1,36 @@
+import {
+ ComponentNode,
+ findProp,
+ NodeTypes,
+ TransformContext
+} from '@vue/compiler-dom'
+import { processChildren, SSRTransformContext } from '../ssrCodegenTransform'
+
+const wipMap = new WeakMap()
+
+export function ssrTransformTransition(
+ node: ComponentNode,
+ context: TransformContext
+) {
+ return () => {
+ const appear = findProp(node, 'appear', false, true)
+ wipMap.set(node, !!appear)
+ }
+}
+
+export function ssrProcessTransition(
+ node: ComponentNode,
+ context: SSRTransformContext
+) {
+ // #5351: filter out comment children inside transition
+ node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
+
+ const appear = wipMap.get(node)
+ if (appear) {
+ context.pushStringPart(``)
+ processChildren(node, context, false, true)
+ context.pushStringPart(``)
+ } else {
+ processChildren(node, context, false, true)
+ }
+}
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts
index 00b0d9dd45a..b0f96e4dd6c 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformTransitionGroup.ts
@@ -18,6 +18,7 @@ const wipMap = new WeakMap()
interface WIPEntry {
tag: AttributeNode | DirectiveNode
propsExp: string | JSChildNode | null
+ scopeId: string | null
}
// phase 1: build props
@@ -45,7 +46,8 @@ export function ssrTransformTransitionGroup(
}
wipMap.set(node, {
tag,
- propsExp
+ propsExp,
+ scopeId: context.scopeId || null
})
}
}
@@ -58,7 +60,7 @@ export function ssrProcessTransitionGroup(
) {
const entry = wipMap.get(node)
if (entry) {
- const { tag, propsExp } = entry
+ const { tag, propsExp, scopeId } = entry
if (tag.type === NodeTypes.DIRECTIVE) {
// dynamic :tag
context.pushStringPart(`<`)
@@ -66,6 +68,9 @@ export function ssrProcessTransitionGroup(
if (propsExp) {
context.pushStringPart(propsExp)
}
+ if (scopeId) {
+ context.pushStringPart(` ${scopeId}`)
+ }
context.pushStringPart(`>`)
processChildren(
@@ -89,6 +94,9 @@ export function ssrProcessTransitionGroup(
if (propsExp) {
context.pushStringPart(propsExp)
}
+ if (scopeId) {
+ context.pushStringPart(` ${scopeId}`)
+ }
context.pushStringPart(`>`)
processChildren(node, context, false, true)
context.pushStringPart(`${tag.value!.content}>`)
diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts
index bd587edcb9c..0c4bd177875 100644
--- a/packages/compiler-ssr/src/transforms/ssrVModel.ts
+++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts
@@ -38,6 +38,38 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
}
}
+ function processOption(plainNode: PlainElementNode) {
+ if (plainNode.tag === 'option') {
+ if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
+ const value = findValueBinding(plainNode)
+ plainNode.ssrCodegenNode!.elements.push(
+ createConditionalExpression(
+ createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [
+ createConditionalExpression(
+ createCallExpression(`Array.isArray`, [model]),
+ createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [
+ model,
+ value
+ ]),
+ createCallExpression(context.helper(SSR_LOOSE_EQUAL), [
+ model,
+ value
+ ])
+ )
+ ]),
+ createSimpleExpression(' selected', true),
+ createSimpleExpression('', true),
+ false /* no newline */
+ )
+ )
+ }
+ } else if (plainNode.tag === 'optgroup') {
+ plainNode.children.forEach(option =>
+ processOption(option as PlainElementNode)
+ )
+ }
+ }
+
if (node.tagType === ElementTypes.ELEMENT) {
const res: DirectiveTransformResult = { props: [] }
const defaultProps = [
@@ -130,32 +162,9 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)]
} else if (node.tag === 'select') {
- node.children.forEach(option => {
- if (option.type === NodeTypes.ELEMENT) {
- const plainNode = option as PlainElementNode
- if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
- const value = findValueBinding(plainNode)
- plainNode.ssrCodegenNode!.elements.push(
- createConditionalExpression(
- createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [
- createConditionalExpression(
- createCallExpression(`Array.isArray`, [model]),
- createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [
- model,
- value
- ]),
- createCallExpression(context.helper(SSR_LOOSE_EQUAL), [
- model,
- value
- ])
- )
- ]),
- createSimpleExpression(' selected', true),
- createSimpleExpression('', true),
- false /* no newline */
- )
- )
- }
+ node.children.forEach(child => {
+ if (child.type === NodeTypes.ELEMENT) {
+ processOption(child as PlainElementNode)
}
})
} else {
diff --git a/packages/dts-built-test/README.md b/packages/dts-built-test/README.md
new file mode 100644
index 00000000000..8191d66e32e
--- /dev/null
+++ b/packages/dts-built-test/README.md
@@ -0,0 +1,5 @@
+# dts built-package test
+
+This package is private and for testing only. It is used to verify edge cases for external libraries that build their types using Vue core types - e.g. Vuetify as in [#8376](https://github.com/vuejs/core/issues/8376).
+
+When running the `build-dts` task, this package's types are built alongside other packages. Then, during `test-dts-only` it is imported and used in [`packages/dts-test/built.test-d.ts`](https://github.com/vuejs/core/blob/main/packages/dts-test/built.test-d.ts) to verify that the built types work correctly.
diff --git a/packages/dts-built-test/package.json b/packages/dts-built-test/package.json
new file mode 100644
index 00000000000..0a544787753
--- /dev/null
+++ b/packages/dts-built-test/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@vue/dts-built-test",
+ "private": true,
+ "types": "dist/dts-built-test.d.ts",
+ "dependencies": {
+ "@vue/shared": "workspace:*",
+ "@vue/reactivity": "workspace:*",
+ "vue": "workspace:*"
+ },
+ "version": "3.4.0-alpha.1"
+}
diff --git a/packages/dts-built-test/src/index.ts b/packages/dts-built-test/src/index.ts
new file mode 100644
index 00000000000..2d9d4033254
--- /dev/null
+++ b/packages/dts-built-test/src/index.ts
@@ -0,0 +1,12 @@
+import { defineComponent } from 'vue'
+
+const _CustomPropsNotErased = defineComponent({
+ props: {},
+ setup() {}
+})
+
+// #8376
+export const CustomPropsNotErased =
+ _CustomPropsNotErased as typeof _CustomPropsNotErased & {
+ foo: string
+ }
diff --git a/packages/dts-test/built.test-d.ts b/packages/dts-test/built.test-d.ts
new file mode 100644
index 00000000000..8ac3e333f99
--- /dev/null
+++ b/packages/dts-test/built.test-d.ts
@@ -0,0 +1,13 @@
+import { CustomPropsNotErased } from '@vue/dts-built-test'
+import { expectType, describe } from './utils'
+
+declare module 'vue' {
+ interface ComponentCustomProps {
+ custom?: number
+ }
+}
+
+// #8376 - custom props should not be erased
+describe('Custom Props not erased', () => {
+ expectType(new CustomPropsNotErased().$props.custom)
+})
diff --git a/packages/dts-test/h.test-d.ts b/packages/dts-test/h.test-d.ts
index 5c700800e94..f2e984b49b8 100644
--- a/packages/dts-test/h.test-d.ts
+++ b/packages/dts-test/h.test-d.ts
@@ -1,6 +1,7 @@
import {
h,
defineComponent,
+ DefineComponent,
ref,
Fragment,
Teleport,
@@ -231,3 +232,18 @@ describe('resolveComponent should work', () => {
message: '1'
})
})
+
+// #5431
+describe('h should work with multiple types', () => {
+ const serializers = {
+ Paragraph: 'p',
+ Component: {} as Component,
+ DefineComponent: {} as DefineComponent
+ }
+
+ const sampleComponent = serializers['' as keyof typeof serializers]
+
+ h(sampleComponent)
+ h(sampleComponent, {})
+ h(sampleComponent, {}, [])
+})
diff --git a/packages/dts-test/package.json b/packages/dts-test/package.json
index a47f064f4eb..ff3c8313ea4 100644
--- a/packages/dts-test/package.json
+++ b/packages/dts-test/package.json
@@ -2,7 +2,8 @@
"name": "dts-test",
"private": true,
"dependencies": {
- "vue": "workspace:*"
+ "vue": "workspace:*",
+ "@vue/dts-built-test": "workspace:*"
},
- "version": "3.3.4"
+ "version": "3.4.0-alpha.1"
}
diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json
index d23e1ee8a3c..8e43685c5dd 100644
--- a/packages/reactivity-transform/package.json
+++ b/packages/reactivity-transform/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/reactivity-transform",
- "version": "3.3.4",
+ "version": "3.4.0-alpha.1",
"description": "@vue/reactivity-transform",
"main": "dist/reactivity-transform.cjs.js",
"files": [
@@ -28,14 +28,14 @@
},
"homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme",
"dependencies": {
- "@babel/parser": "^7.20.15",
- "@vue/compiler-core": "3.3.4",
- "@vue/shared": "3.3.4",
+ "@babel/parser": "^7.23.0",
+ "@vue/compiler-core": "3.4.0-alpha.1",
+ "@vue/shared": "3.4.0-alpha.1",
"estree-walker": "^2.0.2",
- "magic-string": "^0.30.0"
+ "magic-string": "^0.30.5"
},
"devDependencies": {
- "@babel/core": "^7.21.3",
- "@babel/types": "^7.21.3"
+ "@babel/core": "^7.23.2",
+ "@babel/types": "^7.23.0"
}
}
diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts
index c044b5feb35..d9b8f888caf 100644
--- a/packages/reactivity/__tests__/computed.spec.ts
+++ b/packages/reactivity/__tests__/computed.spec.ts
@@ -184,7 +184,7 @@ describe('reactivity/computed', () => {
// mutate n
n.value++
// on the 2nd run, plusOne.value should have already updated.
- expect(plusOneValues).toMatchObject([1, 2, 2])
+ expect(plusOneValues).toMatchObject([1, 2])
})
it('should warn if trying to set a readonly computed', () => {
@@ -288,4 +288,167 @@ describe('reactivity/computed', () => {
oldValue: 2
})
})
+
+ // https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
+ it('should query deps dirty sequentially', () => {
+ const cSpy = vi.fn()
+
+ const a = ref({
+ v: 1
+ })
+ const b = computed(() => {
+ return a.value
+ })
+ const c = computed(() => {
+ cSpy()
+ return b.value?.v
+ })
+ const d = computed(() => {
+ if (b.value) {
+ return c.value
+ }
+ return 0
+ })
+
+ d.value
+ a.value!.v = 2
+ a.value = null
+ d.value
+ expect(cSpy).toHaveBeenCalledTimes(1)
+ })
+
+ // https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
+ it('chained computed dirty reallocation after querying dirty', () => {
+ let _msg: string | undefined
+
+ const items = ref()
+ const isLoaded = computed(() => {
+ return !!items.value
+ })
+ const msg = computed(() => {
+ if (isLoaded.value) {
+ return 'The items are loaded'
+ } else {
+ return 'The items are not loaded'
+ }
+ })
+
+ effect(() => {
+ _msg = msg.value
+ })
+
+ items.value = [1, 2, 3]
+ items.value = [1, 2, 3]
+ items.value = undefined
+
+ expect(_msg).toBe('The items are not loaded')
+ })
+
+ it('chained computed dirty reallocation after trigger computed getter', () => {
+ let _msg: string | undefined
+
+ const items = ref()
+ const isLoaded = computed(() => {
+ return !!items.value
+ })
+ const msg = computed(() => {
+ if (isLoaded.value) {
+ return 'The items are loaded'
+ } else {
+ return 'The items are not loaded'
+ }
+ })
+
+ _msg = msg.value
+ items.value = [1, 2, 3]
+ isLoaded.value // <- trigger computed getter
+ _msg = msg.value
+ items.value = undefined
+ _msg = msg.value
+
+ expect(_msg).toBe('The items are not loaded')
+ })
+
+ // https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
+ it('deps order should be consistent with the last time get value', () => {
+ const cSpy = vi.fn()
+
+ const a = ref(0)
+ const b = computed(() => {
+ return a.value % 3 !== 0
+ })
+ const c = computed(() => {
+ cSpy()
+ if (a.value % 3 === 2) {
+ return 'expensive'
+ }
+ return 'cheap'
+ })
+ const d = computed(() => {
+ return a.value % 3 === 2
+ })
+ const e = computed(() => {
+ if (b.value) {
+ if (d.value) {
+ return 'Avoiding expensive calculation'
+ }
+ }
+ return c.value
+ })
+
+ e.value
+ a.value++
+ e.value
+
+ expect(e.effect.deps.length).toBe(3)
+ expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
+ expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
+ expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
+ expect(cSpy).toHaveBeenCalledTimes(2)
+
+ a.value++
+ e.value
+
+ expect(cSpy).toHaveBeenCalledTimes(2)
+ })
+
+ it('should trigger by the second computed that maybe dirty', () => {
+ const cSpy = vi.fn()
+
+ const src1 = ref(0)
+ const src2 = ref(0)
+ const c1 = computed(() => src1.value)
+ const c2 = computed(() => (src1.value % 2) + src2.value)
+ const c3 = computed(() => {
+ cSpy()
+ c1.value
+ c2.value
+ })
+
+ c3.value
+ src1.value = 2
+ c3.value
+ expect(cSpy).toHaveBeenCalledTimes(2)
+ src2.value = 1
+ c3.value
+ expect(cSpy).toHaveBeenCalledTimes(3)
+ })
+
+ it('should trigger the second effect', () => {
+ const fnSpy = vi.fn()
+ const v = ref(1)
+ const c = computed(() => v.value)
+
+ effect(() => {
+ c.value
+ })
+ effect(() => {
+ c.value
+ fnSpy()
+ })
+
+ expect(fnSpy).toBeCalledTimes(1)
+ v.value = 2
+ expect(fnSpy).toBeCalledTimes(2)
+ })
})
diff --git a/packages/reactivity/__tests__/deferredComputed.spec.ts b/packages/reactivity/__tests__/deferredComputed.spec.ts
index 100f14ae358..8e78ba959c3 100644
--- a/packages/reactivity/__tests__/deferredComputed.spec.ts
+++ b/packages/reactivity/__tests__/deferredComputed.spec.ts
@@ -1,57 +1,32 @@
-import { computed, deferredComputed, effect, ref } from '../src'
+import { computed, effect, ref } from '../src'
describe('deferred computed', () => {
- const tick = Promise.resolve()
-
- test('should only trigger once on multiple mutations', async () => {
+ test('should not trigger if value did not change', () => {
const src = ref(0)
- const c = deferredComputed(() => src.value)
+ const c = computed(() => src.value % 2)
const spy = vi.fn()
effect(() => {
spy(c.value)
})
expect(spy).toHaveBeenCalledTimes(1)
- src.value = 1
src.value = 2
- src.value = 3
- // not called yet
- expect(spy).toHaveBeenCalledTimes(1)
- await tick
- // should only trigger once
- expect(spy).toHaveBeenCalledTimes(2)
- expect(spy).toHaveBeenCalledWith(c.value)
- })
- test('should not trigger if value did not change', async () => {
- const src = ref(0)
- const c = deferredComputed(() => src.value % 2)
- const spy = vi.fn()
- effect(() => {
- spy(c.value)
- })
- expect(spy).toHaveBeenCalledTimes(1)
- src.value = 1
- src.value = 2
-
- await tick
// should not trigger
expect(spy).toHaveBeenCalledTimes(1)
src.value = 3
- src.value = 4
src.value = 5
- await tick
// should trigger because latest value changes
expect(spy).toHaveBeenCalledTimes(2)
})
- test('chained computed trigger', async () => {
+ test('chained computed trigger', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
@@ -69,19 +44,18 @@ describe('deferred computed', () => {
expect(effectSpy).toHaveBeenCalledTimes(1)
src.value = 1
- await tick
expect(c1Spy).toHaveBeenCalledTimes(2)
expect(c2Spy).toHaveBeenCalledTimes(2)
expect(effectSpy).toHaveBeenCalledTimes(2)
})
- test('chained computed avoid re-compute', async () => {
+ test('chained computed avoid re-compute', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
@@ -98,26 +72,24 @@ describe('deferred computed', () => {
src.value = 2
src.value = 4
src.value = 6
- await tick
- // c1 should re-compute once.
- expect(c1Spy).toHaveBeenCalledTimes(2)
+ expect(c1Spy).toHaveBeenCalledTimes(4)
// c2 should not have to re-compute because c1 did not change.
expect(c2Spy).toHaveBeenCalledTimes(1)
// effect should not trigger because c2 did not change.
expect(effectSpy).toHaveBeenCalledTimes(1)
})
- test('chained computed value invalidation', async () => {
+ test('chained computed value invalidation', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
- const c2 = deferredComputed(() => {
+ const c2 = computed(() => {
c2Spy()
return c1.value + 1
})
@@ -139,17 +111,17 @@ describe('deferred computed', () => {
expect(c2Spy).toHaveBeenCalledTimes(2)
})
- test('sync access of invalidated chained computed should not prevent final effect from running', async () => {
+ test('sync access of invalidated chained computed should not prevent final effect from running', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
- const c2 = deferredComputed(() => {
+ const c2 = computed(() => {
c2Spy()
return c1.value + 1
})
@@ -162,14 +134,13 @@ describe('deferred computed', () => {
src.value = 1
// sync access c2
c2.value
- await tick
expect(effectSpy).toHaveBeenCalledTimes(2)
})
- test('should not compute if deactivated before scheduler is called', async () => {
+ test('should not compute if deactivated before scheduler is called', () => {
const c1Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
@@ -179,7 +150,6 @@ describe('deferred computed', () => {
c1.effect.stop()
// trigger
src.value++
- await tick
expect(c1Spy).toHaveBeenCalledTimes(1)
})
})
diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts
index 69d24a76520..2ebb2edea8a 100644
--- a/packages/reactivity/__tests__/effect.spec.ts
+++ b/packages/reactivity/__tests__/effect.spec.ts
@@ -1,5 +1,4 @@
import {
- ref,
reactive,
effect,
stop,
@@ -12,7 +11,8 @@ import {
readonly,
ReactiveEffectRunner
} from '../src/index'
-import { ITERATE_KEY } from '../src/effect'
+import { pauseScheduling, resetScheduling } from '../src/effect'
+import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
@@ -243,6 +243,22 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(undefined)
})
+ it('should support manipulating an array while observing symbol keyed properties', () => {
+ const key = Symbol()
+ let dummy
+ const array: any = reactive([1, 2, 3])
+ effect(() => (dummy = array[key]))
+
+ expect(dummy).toBe(undefined)
+ array.pop()
+ array.shift()
+ array.splice(0, 1)
+ expect(dummy).toBe(undefined)
+ array[key] = 'value'
+ array.length = 0
+ expect(dummy).toBe('value')
+ })
+
it('should observe function valued properties', () => {
const oldFunc = () => {}
const newFunc = () => {}
@@ -558,8 +574,8 @@ describe('reactivity/effect', () => {
expect(output.fx2).toBe(1 + 3 + 3)
expect(fx1Spy).toHaveBeenCalledTimes(1)
- // Invoked twice due to change of fx1.
- expect(fx2Spy).toHaveBeenCalledTimes(2)
+ // Invoked due to change of fx1.
+ expect(fx2Spy).toHaveBeenCalledTimes(1)
fx1Spy.mockClear()
fx2Spy.mockClear()
@@ -585,6 +601,14 @@ describe('reactivity/effect', () => {
expect(runner.effect.fn).toBe(otherRunner.effect.fn)
})
+ it('should wrap if the passed function is a fake effect', () => {
+ const fakeRunner = () => {}
+ fakeRunner.effect = {}
+ const runner = effect(fakeRunner)
+ expect(fakeRunner).not.toBe(runner)
+ expect(runner.effect.fn).toBe(fakeRunner)
+ })
+
it('should not run multiple times for a single mutation', () => {
let dummy
const obj = reactive>({})
@@ -797,26 +821,6 @@ describe('reactivity/effect', () => {
expect(dummy).toBe(3)
})
- // #5707
- // when an effect completes its run, it should clear the tracking bits of
- // its tracked deps. However, if the effect stops itself, the deps list is
- // emptied so their bits are never cleared.
- it('edge case: self-stopping effect tracking ref', () => {
- const c = ref(true)
- const runner = effect(() => {
- // reference ref
- if (!c.value) {
- // stop itself while running
- stop(runner)
- }
- })
- // trigger run
- c.value = !c.value
- // should clear bits
- expect((c as any).dep.w).toBe(0)
- expect((c as any).dep.n).toBe(0)
- })
-
it('events: onStop', () => {
const onStop = vi.fn()
const runner = effect(() => {}, {
@@ -991,4 +995,83 @@ describe('reactivity/effect', () => {
expect(has).toBe(false)
})
})
+
+ it('should be triggered once with pauseScheduling', () => {
+ const counter = reactive({ num: 0 })
+
+ const counterSpy = vi.fn(() => counter.num)
+ effect(counterSpy)
+
+ counterSpy.mockClear()
+
+ pauseScheduling()
+ counter.num++
+ counter.num++
+ resetScheduling()
+ expect(counterSpy).toHaveBeenCalledTimes(1)
+ })
+
+ describe('empty dep cleanup', () => {
+ it('should remove the dep when the effect is stopped', () => {
+ const obj = reactive({ prop: 1 })
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ const runner = effect(() => obj.prop)
+ const dep = getDepFromReactive(toRaw(obj), 'prop')
+ expect(dep).toHaveLength(1)
+ obj.prop = 2
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(1)
+ stop(runner)
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ obj.prop = 3
+ runner()
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ })
+
+ it('should only remove the dep when the last effect is stopped', () => {
+ const obj = reactive({ prop: 1 })
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ const runner1 = effect(() => obj.prop)
+ const dep = getDepFromReactive(toRaw(obj), 'prop')
+ expect(dep).toHaveLength(1)
+ const runner2 = effect(() => obj.prop)
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(2)
+ obj.prop = 2
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(2)
+ stop(runner1)
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(1)
+ obj.prop = 3
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(1)
+ stop(runner2)
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ obj.prop = 4
+ runner1()
+ runner2()
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ })
+
+ it('should remove the dep when it is no longer used by the effect', () => {
+ const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({
+ a: 1,
+ b: 2,
+ c: 'a'
+ })
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ effect(() => obj[obj.c])
+ const depC = getDepFromReactive(toRaw(obj), 'c')
+ expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
+ expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
+ expect(depC).toHaveLength(1)
+ obj.c = 'b'
+ obj.a = 4
+ expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
+ expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
+ expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
+ expect(depC).toHaveLength(1)
+ })
+ })
})
diff --git a/packages/reactivity/__tests__/gc.spec.ts b/packages/reactivity/__tests__/gc.spec.ts
new file mode 100644
index 00000000000..7676a0e12d0
--- /dev/null
+++ b/packages/reactivity/__tests__/gc.spec.ts
@@ -0,0 +1,81 @@
+import {
+ ComputedRef,
+ computed,
+ effect,
+ reactive,
+ shallowRef as ref,
+ toRaw
+} from '../src/index'
+import { getDepFromReactive } from '../src/reactiveEffect'
+
+describe.skipIf(!global.gc)('reactivity/gc', () => {
+ const gc = () => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ global.gc!()
+ resolve()
+ })
+ })
+ }
+
+ // #9233
+ it('should release computed cache', async () => {
+ const src = ref<{} | undefined>({})
+ const srcRef = new WeakRef(src.value!)
+
+ let c: ComputedRef | undefined = computed(() => src.value)
+
+ c.value // cache src value
+ src.value = undefined // release value
+ c = undefined // release computed
+
+ await gc()
+ expect(srcRef.deref()).toBeUndefined()
+ })
+
+ it('should release reactive property dep', async () => {
+ const src = reactive({ foo: 1 })
+
+ let c: ComputedRef | undefined = computed(() => src.foo)
+
+ c.value
+ expect(getDepFromReactive(toRaw(src), 'foo')).not.toBeUndefined()
+
+ c = undefined
+ await gc()
+ await gc()
+ expect(getDepFromReactive(toRaw(src), 'foo')).toBeUndefined()
+ })
+
+ it('should not release effect for ref', async () => {
+ const spy = vi.fn()
+ const src = ref(0)
+
+ effect(() => {
+ spy()
+ src.value
+ })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ await gc()
+ src.value++
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
+
+ it('should not release effect for reactive', async () => {
+ const spy = vi.fn()
+ const src = reactive({ foo: 1 })
+
+ effect(() => {
+ spy()
+ src.foo
+ })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ await gc()
+ src.foo++
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/packages/reactivity/__tests__/reactiveArray.spec.ts b/packages/reactivity/__tests__/reactiveArray.spec.ts
index 808c5aa5529..f4eb7b58384 100644
--- a/packages/reactivity/__tests__/reactiveArray.spec.ts
+++ b/packages/reactivity/__tests__/reactiveArray.spec.ts
@@ -99,6 +99,39 @@ describe('reactivity/reactive/Array', () => {
expect(fn).toHaveBeenCalledTimes(1)
})
+ test('shift on Array should trigger dependency once', () => {
+ const arr = reactive([1, 2, 3])
+ const fn = vi.fn()
+ effect(() => {
+ for (let i = 0; i < arr.length; i++) {
+ arr[i]
+ }
+ fn()
+ })
+ expect(fn).toHaveBeenCalledTimes(1)
+ arr.shift()
+ expect(fn).toHaveBeenCalledTimes(2)
+ })
+
+ //#6018
+ test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => {
+ const arr = ref([1])
+ const fn1 = vi.fn()
+ const fn2 = vi.fn()
+ effect(() => {
+ fn1()
+ if (arr.value.length > 0) {
+ arr.value.slice()
+ fn2()
+ }
+ })
+ expect(fn1).toHaveBeenCalledTimes(1)
+ expect(fn2).toHaveBeenCalledTimes(1)
+ arr.value.splice(0)
+ expect(fn1).toHaveBeenCalledTimes(2)
+ expect(fn2).toHaveBeenCalledTimes(1)
+ })
+
test('add existing index on Array should not trigger length dependency', () => {
const array = new Array(3)
const observed = reactive(array)
diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json
index f9d5c77b856..7244b9a926a 100644
--- a/packages/reactivity/package.json
+++ b/packages/reactivity/package.json
@@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
- "version": "3.3.4",
+ "version": "3.4.0-alpha.1",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",
@@ -36,6 +36,6 @@
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme",
"dependencies": {
- "@vue/shared": "3.3.4"
+ "@vue/shared": "3.4.0-alpha.1"
}
}
diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts
index 259b44a1edc..36e4d311b4b 100644
--- a/packages/reactivity/src/baseHandlers.ts
+++ b/packages/reactivity/src/baseHandlers.ts
@@ -2,7 +2,6 @@ import {
reactive,
readonly,
toRaw,
- ReactiveFlags,
Target,
readonlyMap,
reactiveMap,
@@ -11,14 +10,14 @@ import {
isReadonly,
isShallow
} from './reactive'
-import { TrackOpTypes, TriggerOpTypes } from './operations'
+import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import {
- track,
- trigger,
- ITERATE_KEY,
pauseTracking,
- resetTracking
+ resetTracking,
+ pauseScheduling,
+ resetScheduling
} from './effect'
+import { track, trigger, ITERATE_KEY } from './reactiveEffect'
import {
isObject,
hasOwn,
@@ -71,7 +70,9 @@ function createArrayInstrumentations() {
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
+ pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args)
+ resetScheduling()
resetTracking()
return res
}
diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts
index 1d07af3be8c..e8d99840f71 100644
--- a/packages/reactivity/src/collectionHandlers.ts
+++ b/packages/reactivity/src/collectionHandlers.ts
@@ -1,6 +1,11 @@
-import { toRaw, ReactiveFlags, toReactive, toReadonly } from './reactive'
-import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
-import { TrackOpTypes, TriggerOpTypes } from './operations'
+import { toRaw, toReactive, toReadonly } from './reactive'
+import {
+ track,
+ trigger,
+ ITERATE_KEY,
+ MAP_KEY_ITERATE_KEY
+} from './reactiveEffect'
+import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasOwn, hasChanged, toRawType, isMap } from '@vue/shared'
export type CollectionTypes = IterableCollections | WeakCollections
diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts
index b24484c9e62..09247360d06 100644
--- a/packages/reactivity/src/computed.ts
+++ b/packages/reactivity/src/computed.ts
@@ -1,8 +1,9 @@
import { DebuggerOptions, ReactiveEffect } from './effect'
import { Ref, trackRefValue, triggerRefValue } from './ref'
-import { isFunction, NOOP } from '@vue/shared'
-import { ReactiveFlags, toRaw } from './reactive'
+import { hasChanged, isFunction, NOOP } from '@vue/shared'
+import { toRaw } from './reactive'
import { Dep } from './dep'
+import { DirtyLevels, ReactiveFlags } from './constants'
declare const ComputedRefSymbol: unique symbol
@@ -32,7 +33,6 @@ export class ComputedRefImpl {
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
- public _dirty = true
public _cacheable: boolean
constructor(
@@ -42,10 +42,7 @@ export class ComputedRefImpl {
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
- if (!this._dirty) {
- this._dirty = true
- triggerRefValue(this)
- }
+ triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty)
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
@@ -56,9 +53,10 @@ export class ComputedRefImpl {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self)
- if (self._dirty || !self._cacheable) {
- self._dirty = false
- self._value = self.effect.run()!
+ if (!self._cacheable || self.effect.dirty) {
+ if (hasChanged(self._value, (self._value = self.effect.run()!))) {
+ triggerRefValue(self, DirtyLevels.ComputedValueDirty)
+ }
}
return self._value
}
@@ -66,6 +64,16 @@ export class ComputedRefImpl {
set value(newValue: T) {
this._setter(newValue)
}
+
+ // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
+ get _dirty() {
+ return this.effect.dirty
+ }
+
+ set _dirty(v) {
+ this.effect.dirty = v
+ }
+ // #endregion
}
/**
diff --git a/packages/reactivity/src/constants.ts b/packages/reactivity/src/constants.ts
new file mode 100644
index 00000000000..4ad2ec3c7da
--- /dev/null
+++ b/packages/reactivity/src/constants.ts
@@ -0,0 +1,30 @@
+// using literal strings instead of numbers so that it's easier to inspect
+// debugger events
+
+export const enum TrackOpTypes {
+ GET = 'get',
+ HAS = 'has',
+ ITERATE = 'iterate'
+}
+
+export const enum TriggerOpTypes {
+ SET = 'set',
+ ADD = 'add',
+ DELETE = 'delete',
+ CLEAR = 'clear'
+}
+
+export const enum ReactiveFlags {
+ SKIP = '__v_skip',
+ IS_REACTIVE = '__v_isReactive',
+ IS_READONLY = '__v_isReadonly',
+ IS_SHALLOW = '__v_isShallow',
+ RAW = '__v_raw'
+}
+
+export const enum DirtyLevels {
+ NotDirty = 0,
+ ComputedValueMaybeDirty = 1,
+ ComputedValueDirty = 2,
+ Dirty = 3
+}
diff --git a/packages/reactivity/src/deferredComputed.ts b/packages/reactivity/src/deferredComputed.ts
index a23122046a4..1dbba1f3f03 100644
--- a/packages/reactivity/src/deferredComputed.ts
+++ b/packages/reactivity/src/deferredComputed.ts
@@ -1,88 +1,6 @@
-import { Dep } from './dep'
-import { ReactiveEffect } from './effect'
-import { ComputedGetter, ComputedRef } from './computed'
-import { ReactiveFlags, toRaw } from './reactive'
-import { trackRefValue, triggerRefValue } from './ref'
+import { computed } from './computed'
-const tick = /*#__PURE__*/ Promise.resolve()
-const queue: any[] = []
-let queued = false
-
-const scheduler = (fn: any) => {
- queue.push(fn)
- if (!queued) {
- queued = true
- tick.then(flush)
- }
-}
-
-const flush = () => {
- for (let i = 0; i < queue.length; i++) {
- queue[i]()
- }
- queue.length = 0
- queued = false
-}
-
-class DeferredComputedRefImpl {
- public dep?: Dep = undefined
-
- private _value!: T
- private _dirty = true
- public readonly effect: ReactiveEffect
-
- public readonly __v_isRef = true
- public readonly [ReactiveFlags.IS_READONLY] = true
-
- constructor(getter: ComputedGetter) {
- let compareTarget: any
- let hasCompareTarget = false
- let scheduled = false
- this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
- if (this.dep) {
- if (computedTrigger) {
- compareTarget = this._value
- hasCompareTarget = true
- } else if (!scheduled) {
- const valueToCompare = hasCompareTarget ? compareTarget : this._value
- scheduled = true
- hasCompareTarget = false
- scheduler(() => {
- if (this.effect.active && this._get() !== valueToCompare) {
- triggerRefValue(this)
- }
- scheduled = false
- })
- }
- // chained upstream computeds are notified synchronously to ensure
- // value invalidation in case of sync access; normal effects are
- // deferred to be triggered in scheduler.
- for (const e of this.dep) {
- if (e.computed instanceof DeferredComputedRefImpl) {
- e.scheduler!(true /* computedTrigger */)
- }
- }
- }
- this._dirty = true
- })
- this.effect.computed = this as any
- }
-
- private _get() {
- if (this._dirty) {
- this._dirty = false
- return (this._value = this.effect.run()!)
- }
- return this._value
- }
-
- get value() {
- trackRefValue(this)
- // the computed ref may get wrapped by other proxies e.g. readonly() #3376
- return toRaw(this)._get()
- }
-}
-
-export function deferredComputed(getter: () => T): ComputedRef {
- return new DeferredComputedRefImpl(getter) as any
-}
+/**
+ * @deprecated use `computed` instead. See #5912
+ */
+export const deferredComputed = computed
diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts
index 8677f575756..eafb2a8af3f 100644
--- a/packages/reactivity/src/dep.ts
+++ b/packages/reactivity/src/dep.ts
@@ -1,57 +1,17 @@
-import { ReactiveEffect, trackOpBit } from './effect'
+import type { ReactiveEffect } from './effect'
+import type { ComputedRefImpl } from './computed'
-export type Dep = Set & TrackedMarkers
-
-/**
- * wasTracked and newTracked maintain the status for several levels of effect
- * tracking recursion. One bit per level is used to define whether the dependency
- * was/is tracked.
- */
-type TrackedMarkers = {
- /**
- * wasTracked
- */
- w: number
- /**
- * newTracked
- */
- n: number
+export type Dep = Map & {
+ cleanup: () => void
+ computed?: ComputedRefImpl
}
-export const createDep = (effects?: ReactiveEffect[]): Dep => {
- const dep = new Set(effects) as Dep
- dep.w = 0
- dep.n = 0
+export const createDep = (
+ cleanup: () => void,
+ computed?: ComputedRefImpl
+): Dep => {
+ const dep = new Map() as Dep
+ dep.cleanup = cleanup
+ dep.computed = computed
return dep
}
-
-export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
-
-export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
-
-export const initDepMarkers = ({ deps }: ReactiveEffect) => {
- if (deps.length) {
- for (let i = 0; i < deps.length; i++) {
- deps[i].w |= trackOpBit // set was tracked
- }
- }
-}
-
-export const finalizeDepMarkers = (effect: ReactiveEffect) => {
- const { deps } = effect
- if (deps.length) {
- let ptr = 0
- for (let i = 0; i < deps.length; i++) {
- const dep = deps[i]
- if (wasTracked(dep) && !newTracked(dep)) {
- dep.delete(effect)
- } else {
- deps[ptr++] = dep
- }
- // clear bits
- dep.w &= ~trackOpBit
- dep.n &= ~trackOpBit
- }
- deps.length = ptr
- }
-}
diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts
index d4a34edfef4..3a25295011c 100644
--- a/packages/reactivity/src/effect.ts
+++ b/packages/reactivity/src/effect.ts
@@ -1,34 +1,8 @@
-import { TrackOpTypes, TriggerOpTypes } from './operations'
-import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
+import { NOOP, extend } from '@vue/shared'
+import type { ComputedRefImpl } from './computed'
+import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
+import type { Dep } from './dep'
import { EffectScope, recordEffectScope } from './effectScope'
-import {
- createDep,
- Dep,
- finalizeDepMarkers,
- initDepMarkers,
- newTracked,
- wasTracked
-} from './dep'
-import { ComputedRefImpl } from './computed'
-
-// The main WeakMap that stores {target -> key -> dep} connections.
-// Conceptually, it's easier to think of a dependency as a Dep class
-// which maintains a Set of subscribers, but we simply store them as
-// raw Sets to reduce memory overhead.
-type KeyToDepMap = Map
-const targetMap = new WeakMap()
-
-// The number of effects currently being tracked recursively.
-let effectTrackDepth = 0
-
-export let trackOpBit = 1
-
-/**
- * The bitwise track markers support at most 30 levels of recursion.
- * This value is chosen to enable modern JS engines to use a SMI on all platforms.
- * When recursion depth is greater, fall back to using a full cleanup.
- */
-const maxMarkerBits = 30
export type EffectScheduler = (...args: any[]) => any
@@ -47,13 +21,9 @@ export type DebuggerEventExtraInfo = {
export let activeEffect: ReactiveEffect | undefined
-export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
-export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
-
export class ReactiveEffect {
active = true
deps: Dep[] = []
- parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
@@ -64,10 +34,6 @@ export class ReactiveEffect {
* @internal
*/
allowRecurse?: boolean
- /**
- * @internal
- */
- private deferStop?: boolean
onStop?: () => void
// dev only
@@ -75,77 +41,115 @@ export class ReactiveEffect {
// dev only
onTrigger?: (event: DebuggerEvent) => void
+ /**
+ * @internal
+ */
+ _dirtyLevel = DirtyLevels.Dirty
+ /**
+ * @internal
+ */
+ _trackId = 0
+ /**
+ * @internal
+ */
+ _runnings = 0
+ /**
+ * @internal
+ */
+ _queryings = 0
+ /**
+ * @internal
+ */
+ _depsLength = 0
+
constructor(
public fn: () => T,
- public scheduler: EffectScheduler | null = null,
+ public trigger: () => void,
+ public scheduler?: EffectScheduler,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
+ public get dirty() {
+ if (this._dirtyLevel === DirtyLevels.ComputedValueMaybeDirty) {
+ this._dirtyLevel = DirtyLevels.NotDirty
+ this._queryings++
+ pauseTracking()
+ for (const dep of this.deps) {
+ if (dep.computed) {
+ triggerComputed(dep.computed)
+ if (this._dirtyLevel >= DirtyLevels.ComputedValueDirty) {
+ break
+ }
+ }
+ }
+ resetTracking()
+ this._queryings--
+ }
+ return this._dirtyLevel >= DirtyLevels.ComputedValueDirty
+ }
+
+ public set dirty(v) {
+ this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
+ }
+
run() {
+ this._dirtyLevel = DirtyLevels.NotDirty
if (!this.active) {
return this.fn()
}
- let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
- while (parent) {
- if (parent === this) {
- return
- }
- parent = parent.parent
- }
+ let lastEffect = activeEffect
try {
- this.parent = activeEffect
- activeEffect = this
shouldTrack = true
-
- trackOpBit = 1 << ++effectTrackDepth
-
- if (effectTrackDepth <= maxMarkerBits) {
- initDepMarkers(this)
- } else {
- cleanupEffect(this)
- }
+ activeEffect = this
+ this._runnings++
+ preCleanupEffect(this)
return this.fn()
} finally {
- if (effectTrackDepth <= maxMarkerBits) {
- finalizeDepMarkers(this)
- }
-
- trackOpBit = 1 << --effectTrackDepth
-
- activeEffect = this.parent
+ postCleanupEffect(this)
+ this._runnings--
+ activeEffect = lastEffect
shouldTrack = lastShouldTrack
- this.parent = undefined
-
- if (this.deferStop) {
- this.stop()
- }
}
}
stop() {
- // stopped while running itself - defer the cleanup
- if (activeEffect === this) {
- this.deferStop = true
- } else if (this.active) {
- cleanupEffect(this)
- if (this.onStop) {
- this.onStop()
- }
+ if (this.active) {
+ preCleanupEffect(this)
+ postCleanupEffect(this)
+ this.onStop?.()
this.active = false
}
}
}
-function cleanupEffect(effect: ReactiveEffect) {
- const { deps } = effect
- if (deps.length) {
- for (let i = 0; i < deps.length; i++) {
- deps[i].delete(effect)
+function triggerComputed(computed: ComputedRefImpl) {
+ return computed.value
+}
+
+function preCleanupEffect(effect: ReactiveEffect) {
+ effect._trackId++
+ effect._depsLength = 0
+}
+
+function postCleanupEffect(effect: ReactiveEffect) {
+ if (effect.deps && effect.deps.length > effect._depsLength) {
+ for (let i = effect._depsLength; i < effect.deps.length; i++) {
+ cleanupDepEffect(effect.deps[i], effect)
+ }
+ effect.deps.length = effect._depsLength
+ }
+}
+
+function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
+ const trackId = dep.get(effect)
+ if (trackId !== undefined && effect._trackId !== trackId) {
+ dep.delete(effect)
+ if (dep.size === 0) {
+ dep.cleanup()
}
- deps.length = 0
}
}
@@ -181,11 +185,15 @@ export function effect(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
- if ((fn as ReactiveEffectRunner).effect) {
+ if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
- const _effect = new ReactiveEffect(fn)
+ const _effect = new ReactiveEffect(fn, NOOP, () => {
+ if (_effect.dirty) {
+ _effect.run()
+ }
+ })
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
@@ -208,6 +216,8 @@ export function stop(runner: ReactiveEffectRunner) {
}
export let shouldTrack = true
+export let pauseScheduleStack = 0
+
const trackStack: boolean[] = []
/**
@@ -234,196 +244,70 @@ export function resetTracking() {
shouldTrack = last === undefined ? true : last
}
-/**
- * Tracks access to a reactive property.
- *
- * This will check which effect is running at the moment and record it as dep
- * which records all effects that depend on the reactive property.
- *
- * @param target - Object holding the reactive property.
- * @param type - Defines the type of access to the reactive property.
- * @param key - Identifier of the reactive property to track.
- */
-export function track(target: object, type: TrackOpTypes, key: unknown) {
- if (shouldTrack && activeEffect) {
- let depsMap = targetMap.get(target)
- if (!depsMap) {
- targetMap.set(target, (depsMap = new Map()))
- }
- let dep = depsMap.get(key)
- if (!dep) {
- depsMap.set(key, (dep = createDep()))
- }
-
- const eventInfo = __DEV__
- ? { effect: activeEffect, target, type, key }
- : undefined
+export function pauseScheduling() {
+ pauseScheduleStack++
+}
- trackEffects(dep, eventInfo)
+export function resetScheduling() {
+ pauseScheduleStack--
+ while (!pauseScheduleStack && queueEffectSchedulers.length) {
+ queueEffectSchedulers.shift()!()
}
}
-export function trackEffects(
+export function trackEffect(
+ effect: ReactiveEffect,
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
- let shouldTrack = false
- if (effectTrackDepth <= maxMarkerBits) {
- if (!newTracked(dep)) {
- dep.n |= trackOpBit // set newly tracked
- shouldTrack = !wasTracked(dep)
- }
- } else {
- // Full cleanup mode.
- shouldTrack = !dep.has(activeEffect!)
- }
-
- if (shouldTrack) {
- dep.add(activeEffect!)
- activeEffect!.deps.push(dep)
- if (__DEV__ && activeEffect!.onTrack) {
- activeEffect!.onTrack(
- extend(
- {
- effect: activeEffect!
- },
- debuggerEventExtraInfo!
- )
- )
- }
- }
-}
-
-/**
- * Finds all deps associated with the target (or a specific property) and
- * triggers the effects stored within.
- *
- * @param target - The reactive object.
- * @param type - Defines the type of the operation that needs to trigger effects.
- * @param key - Can be used to target a specific reactive property in the target object.
- */
-export function trigger(
- target: object,
- type: TriggerOpTypes,
- key?: unknown,
- newValue?: unknown,
- oldValue?: unknown,
- oldTarget?: Map | Set
-) {
- const depsMap = targetMap.get(target)
- if (!depsMap) {
- // never been tracked
- return
- }
-
- let deps: (Dep | undefined)[] = []
- if (type === TriggerOpTypes.CLEAR) {
- // collection being cleared
- // trigger all effects for target
- deps = [...depsMap.values()]
- } else if (key === 'length' && isArray(target)) {
- const newLength = Number(newValue)
- depsMap.forEach((dep, key) => {
- if (key === 'length' || key >= newLength) {
- deps.push(dep)
- }
- })
- } else {
- // schedule runs for SET | ADD | DELETE
- if (key !== void 0) {
- deps.push(depsMap.get(key))
- }
-
- // also run for iteration key on ADD | DELETE | Map.SET
- switch (type) {
- case TriggerOpTypes.ADD:
- if (!isArray(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- } else if (isIntegerKey(key)) {
- // new index added to array -> length changes
- deps.push(depsMap.get('length'))
- }
- break
- case TriggerOpTypes.DELETE:
- if (!isArray(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- }
- break
- case TriggerOpTypes.SET:
- if (isMap(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- }
- break
- }
- }
-
- const eventInfo = __DEV__
- ? { target, type, key, newValue, oldValue, oldTarget }
- : undefined
-
- if (deps.length === 1) {
- if (deps[0]) {
- if (__DEV__) {
- triggerEffects(deps[0], eventInfo)
- } else {
- triggerEffects(deps[0])
- }
- }
- } else {
- const effects: ReactiveEffect[] = []
- for (const dep of deps) {
- if (dep) {
- effects.push(...dep)
+ if (dep.get(effect) !== effect._trackId) {
+ dep.set(effect, effect._trackId)
+ const oldDep = effect.deps[effect._depsLength]
+ if (oldDep !== dep) {
+ if (oldDep) {
+ cleanupDepEffect(oldDep, effect)
}
+ effect.deps[effect._depsLength++] = dep
+ } else {
+ effect._depsLength++
}
if (__DEV__) {
- triggerEffects(createDep(effects), eventInfo)
- } else {
- triggerEffects(createDep(effects))
+ effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
}
}
}
-export function triggerEffects(
- dep: Dep | ReactiveEffect[],
- debuggerEventExtraInfo?: DebuggerEventExtraInfo
-) {
- // spread into array for stabilization
- const effects = isArray(dep) ? dep : [...dep]
- for (const effect of effects) {
- if (effect.computed) {
- triggerEffect(effect, debuggerEventExtraInfo)
- }
- }
- for (const effect of effects) {
- if (!effect.computed) {
- triggerEffect(effect, debuggerEventExtraInfo)
- }
- }
-}
+const queueEffectSchedulers: (() => void)[] = []
-function triggerEffect(
- effect: ReactiveEffect,
+export function triggerEffects(
+ dep: Dep,
+ dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
- if (effect !== activeEffect || effect.allowRecurse) {
- if (__DEV__ && effect.onTrigger) {
- effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
+ pauseScheduling()
+ for (const effect of dep.keys()) {
+ if (!effect.allowRecurse && effect._runnings) {
+ continue
}
- if (effect.scheduler) {
- effect.scheduler()
- } else {
- effect.run()
+ if (
+ effect._dirtyLevel < dirtyLevel &&
+ (!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
+ ) {
+ const lastDirtyLevel = effect._dirtyLevel
+ effect._dirtyLevel = dirtyLevel
+ if (
+ lastDirtyLevel === DirtyLevels.NotDirty &&
+ (!effect._queryings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
+ ) {
+ if (__DEV__) {
+ effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
+ }
+ effect.trigger()
+ if (effect.scheduler) {
+ queueEffectSchedulers.push(effect.scheduler)
+ }
+ }
}
}
-}
-
-export function getDepFromReactive(object: any, key: string | number | symbol) {
- return targetMap.get(object)?.get(key)
+ resetScheduling()
}
diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts
index ee4da5b1935..9497527e81e 100644
--- a/packages/reactivity/src/index.ts
+++ b/packages/reactivity/src/index.ts
@@ -31,7 +31,6 @@ export {
shallowReadonly,
markRaw,
toRaw,
- ReactiveFlags /* @remove */,
type Raw,
type DeepReadonly,
type ShallowReactive,
@@ -49,12 +48,11 @@ export { deferredComputed } from './deferredComputed'
export {
effect,
stop,
- trigger,
- track,
enableTracking,
pauseTracking,
resetTracking,
- ITERATE_KEY,
+ pauseScheduling,
+ resetScheduling,
ReactiveEffect,
type ReactiveEffectRunner,
type ReactiveEffectOptions,
@@ -63,6 +61,7 @@ export {
type DebuggerEvent,
type DebuggerEventExtraInfo
} from './effect'
+export { trigger, track, ITERATE_KEY } from './reactiveEffect'
export {
effectScope,
EffectScope,
@@ -71,5 +70,6 @@ export {
} from './effectScope'
export {
TrackOpTypes /* @remove */,
- TriggerOpTypes /* @remove */
-} from './operations'
+ TriggerOpTypes /* @remove */,
+ ReactiveFlags /* @remove */
+} from './constants'
diff --git a/packages/reactivity/src/operations.ts b/packages/reactivity/src/operations.ts
deleted file mode 100644
index 1b96e982571..00000000000
--- a/packages/reactivity/src/operations.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// using literal strings instead of numbers so that it's easier to inspect
-// debugger events
-
-export const enum TrackOpTypes {
- GET = 'get',
- HAS = 'has',
- ITERATE = 'iterate'
-}
-
-export const enum TriggerOpTypes {
- SET = 'set',
- ADD = 'add',
- DELETE = 'delete',
- CLEAR = 'clear'
-}
diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts
index 1881955cf1c..2904c69abe2 100644
--- a/packages/reactivity/src/reactive.ts
+++ b/packages/reactivity/src/reactive.ts
@@ -12,14 +12,7 @@ import {
shallowReadonlyCollectionHandlers
} from './collectionHandlers'
import type { UnwrapRefSimple, Ref, RawSymbol } from './ref'
-
-export const enum ReactiveFlags {
- SKIP = '__v_skip',
- IS_REACTIVE = '__v_isReactive',
- IS_READONLY = '__v_isReadonly',
- IS_SHALLOW = '__v_isShallow',
- RAW = '__v_raw'
-}
+import { ReactiveFlags } from './constants'
export interface Target {
[ReactiveFlags.SKIP]?: boolean
diff --git a/packages/reactivity/src/reactiveEffect.ts b/packages/reactivity/src/reactiveEffect.ts
new file mode 100644
index 00000000000..d3474db3da1
--- /dev/null
+++ b/packages/reactivity/src/reactiveEffect.ts
@@ -0,0 +1,150 @@
+import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
+import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
+import { createDep, Dep } from './dep'
+import {
+ activeEffect,
+ pauseScheduling,
+ resetScheduling,
+ shouldTrack,
+ trackEffect,
+ triggerEffects
+} from './effect'
+
+// The main WeakMap that stores {target -> key -> dep} connections.
+// Conceptually, it's easier to think of a dependency as a Dep class
+// which maintains a Set of subscribers, but we simply store them as
+// raw Sets to reduce memory overhead.
+type KeyToDepMap = Map
+const targetMap = new WeakMap