diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
index cb14f56afdb..c87a37ba5d2 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
@@ -47,6 +47,27 @@ export function render(_ctx) {
}"
`;
+exports[`compiler: v-for > key only binding pattern 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("
", true)
+
+export function render(_ctx) {
+ const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+ const n2 = t0()
+ const x2 = _child(n2)
+ let _row, _row_id
+ {
+ _row = _for_item0.value
+ _row_id = _row.id
+
+ }
+ _setText(x2, _toDisplayString(_row_id + _row_id))
+ return n2
+ }, (row) => (row.id))
+ return n0
+}"
+`;
+
exports[`compiler: v-for > multi effect 1`] = `
"import { setProp as _setProp, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("", true)
@@ -115,6 +136,75 @@ export function render(_ctx) {
}"
`;
+exports[`compiler: v-for > selector pattern 1`] = `
+"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("
", true)
+
+export function render(_ctx) {
+ const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+ const n2 = t0()
+ const x2 = _child(n2)
+ _selector0_0(() => {
+ _setText(x2, _toDisplayString(_ctx.selected === _for_item0.value.id ? 'danger' : ''))
+ })
+ return n2
+ }, (row) => (row.id))
+ const _selector0_0 = n0.useSelector(() => _ctx.selected)
+ return n0
+}"
+`;
+
+exports[`compiler: v-for > selector pattern 2`] = `
+"import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("
", true)
+
+export function render(_ctx) {
+ const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+ const n2 = t0()
+ _selector0_0(() => {
+ _setClass(n2, _ctx.selected === _for_item0.value.id ? 'danger' : '')
+ })
+ return n2
+ }, (row) => (row.id))
+ const _selector0_0 = n0.useSelector(() => _ctx.selected)
+ return n0
+}"
+`;
+
+exports[`compiler: v-for > selector pattern 3`] = `
+"import { setClass as _setClass, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("
", true)
+
+export function render(_ctx) {
+ const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+ const n2 = t0()
+ _renderEffect(() => {
+ const _row = _for_item0.value
+ _setClass(n2, _row.label === _row.id ? 'danger' : '')
+ })
+ return n2
+ }, (row) => (row.id))
+ return n0
+}"
+`;
+
+exports[`compiler: v-for > selector pattern 4`] = `
+"import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("
", true)
+
+export function render(_ctx) {
+ const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+ const n2 = t0()
+ _selector0_0(() => {
+ _setClass(n2, { danger: _for_item0.value.id === _ctx.selected })
+ })
+ return n2
+ }, (row) => (row.id))
+ const _selector0_0 = n0.useSelector(() => _ctx.selected)
+ return n0
+}"
+`;
+
exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = `
"import { getDefaultValue as _getDefaultValue, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("
", true)
diff --git a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
index 0008df7f4c7..91b8526b329 100644
--- a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
@@ -67,6 +67,73 @@ describe('compiler: v-for', () => {
).lengthOf(1)
})
+ test('key only binding pattern', () => {
+ expect(
+ compileWithVFor(
+ `
+
+ {{ row.id + row.id }}
+
+ `,
+ ).code,
+ ).matchSnapshot()
+ })
+
+ test('selector pattern', () => {
+ expect(
+ compileWithVFor(
+ `
+
+ {{ selected === row.id ? 'danger' : '' }}
+
+ `,
+ ).code,
+ ).matchSnapshot()
+
+ expect(
+ compileWithVFor(
+ `
+
+ `,
+ ).code,
+ ).matchSnapshot()
+
+ // Should not be optimized because row.label is not from parent scope
+ expect(
+ compileWithVFor(
+ `
+
+ `,
+ ).code,
+ ).matchSnapshot()
+
+ expect(
+ compileWithVFor(
+ `
+
+ `,
+ ).code,
+ ).matchSnapshot()
+ })
+
test('multi effect', () => {
const { code } = compileWithVFor(
``,
diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts
index b161b8f45d1..66b57c58378 100644
--- a/packages/compiler-vapor/src/generators/block.ts
+++ b/packages/compiler-vapor/src/generators/block.ts
@@ -19,14 +19,13 @@ export function genBlock(
context: CodegenContext,
args: CodeFragment[] = [],
root?: boolean,
- customReturns?: (returns: CodeFragment[]) => CodeFragment[],
): CodeFragment[] {
return [
'(',
...args,
') => {',
INDENT_START,
- ...genBlockContent(oper, context, root, customReturns),
+ ...genBlockContent(oper, context, root),
INDENT_END,
NEWLINE,
'}',
@@ -37,7 +36,7 @@ export function genBlockContent(
block: BlockIRNode,
context: CodegenContext,
root?: boolean,
- customReturns?: (returns: CodeFragment[]) => CodeFragment[],
+ genEffectsExtraFrag?: () => CodeFragment[],
): CodeFragment[] {
const [frag, push] = buildCodeFragment()
const { dynamic, effect, operation, returns } = block
@@ -56,7 +55,7 @@ export function genBlockContent(
}
push(...genOperations(operation, context))
- push(...genEffects(effect, context))
+ push(...genEffects(effect, context, genEffectsExtraFrag))
push(NEWLINE, `return `)
@@ -65,7 +64,7 @@ export function genBlockContent(
returnNodes.length > 1
? genMulti(DELIMITERS_ARRAY, ...returnNodes)
: [returnNodes[0] || 'null']
- push(...(customReturns ? customReturns(returnsCode) : returnsCode))
+ push(...returnsCode)
resetBlock()
return frag
diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts
index eedaeeb380a..ba8b9941562 100644
--- a/packages/compiler-vapor/src/generators/expression.ts
+++ b/packages/compiler-vapor/src/generators/expression.ts
@@ -230,6 +230,7 @@ function canPrefix(name: string) {
type DeclarationResult = {
ids: Record
frag: CodeFragment[]
+ varNames: string[]
}
type DeclarationValue = {
name: string
@@ -243,6 +244,7 @@ type DeclarationValue = {
export function processExpressions(
context: CodegenContext,
expressions: SimpleExpressionNode[],
+ shouldDeclareConst: boolean,
): DeclarationResult {
// analyze variables
const { seenVariable, variableToExpMap, expToVariableMap, seenIdentifier } =
@@ -266,7 +268,11 @@ export function processExpressions(
varDeclarations,
)
- return genDeclarations([...varDeclarations, ...expDeclarations], context)
+ return genDeclarations(
+ [...varDeclarations, ...expDeclarations],
+ context,
+ shouldDeclareConst,
+ )
}
function analyzeExpressions(expressions: SimpleExpressionNode[]) {
@@ -507,15 +513,21 @@ function processRepeatedExpressions(
function genDeclarations(
declarations: DeclarationValue[],
context: CodegenContext,
+ shouldDeclareConst: boolean,
): DeclarationResult {
const [frag, push] = buildCodeFragment()
const ids: Record = Object.create(null)
+ const varNames = new Set()
// process identifiers first as expressions may rely on them
declarations.forEach(({ name, isIdentifier, value }) => {
if (isIdentifier) {
const varName = (ids[name] = `_${name}`)
- push(`const ${varName} = `, ...genExpression(value, context), NEWLINE)
+ varNames.add(varName)
+ if (shouldDeclareConst) {
+ push(`const `)
+ }
+ push(`${varName} = `, ...genExpression(value, context), NEWLINE)
}
})
@@ -523,15 +535,19 @@ function genDeclarations(
declarations.forEach(({ name, isIdentifier, value }) => {
if (!isIdentifier) {
const varName = (ids[name] = `_${name}`)
+ varNames.add(varName)
+ if (shouldDeclareConst) {
+ push(`const `)
+ }
push(
- `const ${varName} = `,
+ `${varName} = `,
...context.withId(() => genExpression(value, context), ids),
NEWLINE,
)
}
})
- return { ids, frag }
+ return { ids, frag, varNames: [...varNames] }
}
function escapeRegExp(string: string) {
diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts
index fbb72c61d47..40f002a8536 100644
--- a/packages/compiler-vapor/src/generators/for.ts
+++ b/packages/compiler-vapor/src/generators/for.ts
@@ -1,16 +1,32 @@
import {
type SimpleExpressionNode,
createSimpleExpression,
+ isStaticNode,
walkIdentifiers,
} from '@vue/compiler-dom'
-import { genBlock } from './block'
+import { genBlockContent } from './block'
import { genExpression } from './expression'
import type { CodegenContext } from '../generate'
-import type { ForIRNode } from '../ir'
-import { type CodeFragment, NEWLINE, genCall, genMulti } from './utils'
-import type { Identifier } from '@babel/types'
+import type { BlockIRNode, ForIRNode, IREffect } from '../ir'
+import {
+ type CodeFragment,
+ INDENT_END,
+ INDENT_START,
+ NEWLINE,
+ genCall,
+ genMulti,
+} from './utils'
+import {
+ type Expression,
+ type Identifier,
+ type Node,
+ isNodesEquivalent,
+} from '@babel/types'
import { parseExpression } from '@babel/parser'
import { VaporVForFlags } from '../../../shared/src/vaporFlags'
+import { walk } from 'estree-walker'
+import { genOperation } from './operation'
+import { extend, isGloballyAllowed } from '@vue/shared'
export function genFor(
oper: ForIRNode,
@@ -78,7 +94,62 @@ export function genFor(
idMap[indexVar] = null
}
- const blockFn = context.withId(() => genBlock(render, context, args), idMap)
+ const { selectorPatterns, keyOnlyBindingPatterns } = matchPatterns(
+ render,
+ keyProp,
+ idMap,
+ )
+ const patternFrag: CodeFragment[] = []
+
+ for (let i = 0; i < selectorPatterns.length; i++) {
+ const { selector } = selectorPatterns[i]
+ const selectorName = `_selector${id}_${i}`
+ patternFrag.push(
+ NEWLINE,
+ `const ${selectorName} = `,
+ ...genCall(`n${id}.useSelector`, [
+ `() => `,
+ ...genExpression(selector, context),
+ ]),
+ )
+ }
+
+ const blockFn = context.withId(() => {
+ const frag: CodeFragment[] = []
+ frag.push('(', ...args, ') => {', INDENT_START)
+ if (selectorPatterns.length || keyOnlyBindingPatterns.length) {
+ frag.push(
+ ...genBlockContent(render, context, false, () => {
+ const patternFrag: CodeFragment[] = []
+
+ for (let i = 0; i < selectorPatterns.length; i++) {
+ const { effect } = selectorPatterns[i]
+ patternFrag.push(
+ NEWLINE,
+ `_selector${id}_${i}(() => {`,
+ INDENT_START,
+ )
+ for (const oper of effect.operations) {
+ patternFrag.push(...genOperation(oper, context))
+ }
+ patternFrag.push(INDENT_END, NEWLINE, `})`)
+ }
+
+ for (const { effect } of keyOnlyBindingPatterns) {
+ for (const oper of effect.operations) {
+ patternFrag.push(...genOperation(oper, context))
+ }
+ }
+
+ return patternFrag
+ }),
+ )
+ } else {
+ frag.push(...genBlockContent(render, context))
+ }
+ frag.push(INDENT_END, NEWLINE, '}')
+ return frag
+ }, idMap)
exitScope()
let flags = 0
@@ -103,6 +174,7 @@ export function genFor(
flags ? String(flags) : undefined,
// todo: hydrationNode
),
+ ...patternFrag,
]
// construct a id -> accessor path map.
@@ -234,3 +306,223 @@ export function genFor(
return idMap
}
}
+
+function matchPatterns(
+ render: BlockIRNode,
+ keyProp: SimpleExpressionNode | undefined,
+ idMap: Record,
+) {
+ const selectorPatterns: NonNullable<
+ ReturnType
+ >[] = []
+ const keyOnlyBindingPatterns: NonNullable<
+ ReturnType
+ >[] = []
+
+ render.effect = render.effect.filter(effect => {
+ if (keyProp !== undefined) {
+ const selector = matchSelectorPattern(effect, keyProp.ast, idMap)
+ if (selector) {
+ selectorPatterns.push(selector)
+ return false
+ }
+ const keyOnly = matchKeyOnlyBindingPattern(effect, keyProp.ast)
+ if (keyOnly) {
+ keyOnlyBindingPatterns.push(keyOnly)
+ return false
+ }
+ }
+
+ return true
+ })
+
+ return {
+ keyOnlyBindingPatterns,
+ selectorPatterns,
+ }
+}
+
+function matchKeyOnlyBindingPattern(
+ effect: IREffect,
+ keyAst: any,
+):
+ | {
+ effect: IREffect
+ }
+ | undefined {
+ // TODO: expressions can be multiple?
+ if (effect.expressions.length === 1) {
+ const ast = effect.expressions[0].ast
+ if (typeof ast === 'object' && ast !== null) {
+ if (isKeyOnlyBinding(ast, keyAst)) {
+ return { effect }
+ }
+ }
+ }
+}
+
+function matchSelectorPattern(
+ effect: IREffect,
+ keyAst: any,
+ idMap: Record,
+):
+ | {
+ effect: IREffect
+ selector: SimpleExpressionNode
+ }
+ | undefined {
+ // TODO: expressions can be multiple?
+ if (effect.expressions.length === 1) {
+ const ast = effect.expressions[0].ast
+ if (typeof ast === 'object' && ast) {
+ const matcheds: [key: Expression, selector: Expression][] = []
+
+ walk(ast, {
+ enter(node) {
+ if (
+ typeof node === 'object' &&
+ node &&
+ node.type === 'BinaryExpression' &&
+ node.operator === '===' &&
+ node.left.type !== 'PrivateName'
+ ) {
+ const { left, right } = node
+ for (const [a, b] of [
+ [left, right],
+ [right, left],
+ ]) {
+ const aIsKey = isKeyOnlyBinding(a, keyAst)
+ const bIsKey = isKeyOnlyBinding(b, keyAst)
+ const bVars = analyzeVariableScopes(b, idMap)
+ if (aIsKey && !bIsKey && !bVars.locals.length) {
+ matcheds.push([a, b])
+ }
+ }
+ }
+ },
+ })
+
+ if (matcheds.length === 1) {
+ const [key, selector] = matcheds[0]
+ const content = effect.expressions[0].content
+
+ let hasExtraId = false
+ const parentStackMap = new Map()
+ const parentStack: Node[] = []
+ walkIdentifiers(
+ ast,
+ id => {
+ if (id.start !== key.start && id.start !== selector.start) {
+ hasExtraId = true
+ }
+ parentStackMap.set(id, parentStack.slice())
+ },
+ false,
+ parentStack,
+ )
+
+ if (!hasExtraId) {
+ const name = content.slice(selector.start! - 1, selector.end! - 1)
+ return {
+ effect,
+ // @ts-expect-error
+ selector: {
+ content: name,
+ ast: extend({}, selector, {
+ start: 1,
+ end: name.length + 1,
+ }),
+ loc: selector.loc as any,
+ isStatic: false,
+ },
+ }
+ }
+ }
+ }
+
+ const content = effect.expressions[0].content
+ if (
+ typeof ast === 'object' &&
+ ast &&
+ ast.type === 'ConditionalExpression' &&
+ ast.test.type === 'BinaryExpression' &&
+ ast.test.operator === '===' &&
+ ast.test.left.type !== 'PrivateName' &&
+ isStaticNode(ast.consequent) &&
+ isStaticNode(ast.alternate)
+ ) {
+ const left = ast.test.left
+ const right = ast.test.right
+ for (const [a, b] of [
+ [left, right],
+ [right, left],
+ ]) {
+ const aIsKey = isKeyOnlyBinding(a, keyAst)
+ const bIsKey = isKeyOnlyBinding(b, keyAst)
+ const bVars = analyzeVariableScopes(b, idMap)
+ if (aIsKey && !bIsKey && !bVars.locals.length) {
+ return {
+ effect,
+ // @ts-expect-error
+ selector: {
+ content: content.slice(b.start! - 1, b.end! - 1),
+ ast: b,
+ loc: b.loc as any,
+ isStatic: false,
+ },
+ }
+ }
+ }
+ }
+ }
+}
+
+function analyzeVariableScopes(
+ ast: Node,
+ idMap: Record,
+) {
+ let globals: string[] = []
+ let locals: string[] = []
+
+ const ids: Identifier[] = []
+ const parentStackMap = new Map()
+ const parentStack: Node[] = []
+ walkIdentifiers(
+ ast,
+ id => {
+ ids.push(id)
+ parentStackMap.set(id, parentStack.slice())
+ },
+ false,
+ parentStack,
+ )
+
+ for (const id of ids) {
+ if (isGloballyAllowed(id.name)) {
+ continue
+ }
+ if (idMap[id.name]) {
+ locals.push(id.name)
+ } else {
+ globals.push(id.name)
+ }
+ }
+
+ return { globals, locals }
+}
+
+function isKeyOnlyBinding(expr: Node, keyAst: any) {
+ let only = true
+ walk(expr, {
+ enter(node) {
+ if (isNodesEquivalent(node, keyAst)) {
+ this.skip()
+ return
+ }
+ if (node.type === 'Identifier') {
+ only = false
+ }
+ },
+ })
+ return only
+}
diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts
index 4247bc6feca..b5f99f8ed07 100644
--- a/packages/compiler-vapor/src/generators/operation.ts
+++ b/packages/compiler-vapor/src/generators/operation.ts
@@ -98,17 +98,20 @@ export function genOperation(
export function genEffects(
effects: IREffect[],
context: CodegenContext,
+ genExtraFrag?: () => CodeFragment[],
): CodeFragment[] {
const {
helper,
block: { expressions },
} = context
const [frag, push, unshift] = buildCodeFragment()
+ const shouldDeclareConst = genExtraFrag === undefined
let operationsCount = 0
- const { ids, frag: declarationFrags } = processExpressions(
- context,
- expressions,
- )
+ const {
+ ids,
+ frag: declarationFrags,
+ varNames,
+ } = processExpressions(context, expressions, shouldDeclareConst)
push(...declarationFrags)
for (let i = 0; i < effects.length; i++) {
const effect = effects[i]
@@ -125,6 +128,9 @@ export function genEffects(
if (newLineCount > 1 || operationsCount > 1 || declarationFrags.length > 0) {
unshift(`{`, INDENT_START, NEWLINE)
push(INDENT_END, NEWLINE, '}')
+ if (!effects.length) {
+ unshift(NEWLINE)
+ }
}
if (effects.length) {
@@ -132,6 +138,14 @@ export function genEffects(
push(`)`)
}
+ if (!shouldDeclareConst && varNames.length) {
+ unshift(NEWLINE, `let `, varNames.join(', '))
+ }
+
+ if (genExtraFrag) {
+ push(...context.withId(genExtraFrag, ids))
+ }
+
return frag
}
diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts
index db2984cc1ef..62b3237c429 100644
--- a/packages/reactivity/__tests__/computed.spec.ts
+++ b/packages/reactivity/__tests__/computed.spec.ts
@@ -467,12 +467,8 @@ describe('reactivity/computed', () => {
const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
c2.value
- expect(
- c1.flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed),
- ).toBe(0)
- expect(
- c2.flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed),
- ).toBe(0)
+ expect(c1.flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)).toBe(0)
+ expect(c2.flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)).toBe(0)
})
it('should chained computeds dirtyLevel update with first computed effect', () => {
diff --git a/packages/reactivity/__tests__/effectScope.spec.ts b/packages/reactivity/__tests__/effectScope.spec.ts
index 84310b985f2..6e42ef94ecb 100644
--- a/packages/reactivity/__tests__/effectScope.spec.ts
+++ b/packages/reactivity/__tests__/effectScope.spec.ts
@@ -2,6 +2,7 @@ import { nextTick, watch, watchEffect } from '@vue/runtime-core'
import {
type ComputedRef,
EffectScope,
+ ReactiveEffect,
computed,
effect,
effectScope,
@@ -9,6 +10,7 @@ import {
onScopeDispose,
reactive,
ref,
+ setCurrentScope,
} from '../src'
describe('reactivity/effect/scope', () => {
@@ -20,7 +22,7 @@ describe('reactivity/effect/scope', () => {
it('should accept zero argument', () => {
const scope = effectScope()
- expect(scope.effects.length).toBe(0)
+ expect(getEffectsCount(scope)).toBe(0)
})
it('should return run value', () => {
@@ -29,7 +31,8 @@ describe('reactivity/effect/scope', () => {
it('should work w/ active property', () => {
const scope = effectScope()
- scope.run(() => 1)
+ const src = computed(() => 1)
+ scope.run(() => src.value)
expect(scope.active).toBe(true)
scope.stop()
expect(scope.active).toBe(false)
@@ -47,7 +50,7 @@ describe('reactivity/effect/scope', () => {
expect(dummy).toBe(7)
})
- expect(scope.effects.length).toBe(1)
+ expect(getEffectsCount(scope)).toBe(1)
})
it('stop', () => {
@@ -60,7 +63,7 @@ describe('reactivity/effect/scope', () => {
effect(() => (doubled = counter.num * 2))
})
- expect(scope.effects.length).toBe(2)
+ expect(getEffectsCount(scope)).toBe(2)
expect(dummy).toBe(0)
counter.num = 7
@@ -87,9 +90,8 @@ describe('reactivity/effect/scope', () => {
})
})
- expect(scope.effects.length).toBe(1)
- expect(scope.scopes!.length).toBe(1)
- expect(scope.scopes![0]).toBeInstanceOf(EffectScope)
+ expect(getEffectsCount(scope)).toBe(1)
+ expect(scope.deps?.nextDep?.dep).toBeInstanceOf(EffectScope)
expect(dummy).toBe(0)
counter.num = 7
@@ -117,7 +119,7 @@ describe('reactivity/effect/scope', () => {
})
})
- expect(scope.effects.length).toBe(1)
+ expect(getEffectsCount(scope)).toBe(1)
expect(dummy).toBe(0)
counter.num = 7
@@ -142,13 +144,13 @@ describe('reactivity/effect/scope', () => {
effect(() => (dummy = counter.num))
})
- expect(scope.effects.length).toBe(1)
+ expect(getEffectsCount(scope)).toBe(1)
scope.run(() => {
effect(() => (doubled = counter.num * 2))
})
- expect(scope.effects.length).toBe(2)
+ expect(getEffectsCount(scope)).toBe(2)
counter.num = 7
expect(dummy).toBe(7)
@@ -166,21 +168,21 @@ describe('reactivity/effect/scope', () => {
effect(() => (dummy = counter.num))
})
- expect(scope.effects.length).toBe(1)
+ expect(getEffectsCount(scope)).toBe(1)
scope.stop()
+ expect(getEffectsCount(scope)).toBe(0)
+
scope.run(() => {
effect(() => (doubled = counter.num * 2))
})
- expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
-
- expect(scope.effects.length).toBe(0)
+ expect(getEffectsCount(scope)).toBe(1)
counter.num = 7
expect(dummy).toBe(0)
- expect(doubled).toBe(undefined)
+ expect(doubled).toBe(14)
})
it('should fire onScopeDispose hook', () => {
@@ -224,9 +226,9 @@ describe('reactivity/effect/scope', () => {
it('should dereference child scope from parent scope after stopping child scope (no memleaks)', () => {
const parent = effectScope()
const child = parent.run(() => effectScope())!
- expect(parent.scopes!.includes(child)).toBe(true)
+ expect(parent.deps?.dep).toBe(child)
child.stop()
- expect(parent.scopes!.includes(child)).toBe(false)
+ expect(parent.deps).toBeUndefined()
})
it('test with higher level APIs', async () => {
@@ -290,21 +292,7 @@ describe('reactivity/effect/scope', () => {
parentScope.run(() => {
const childScope = effectScope(true)
- childScope.on()
- childScope.off()
- expect(getCurrentScope()).toBe(parentScope)
- })
- })
-
- it('calling on() and off() multiple times inside an active scope should not break currentScope', () => {
- const parentScope = effectScope()
- parentScope.run(() => {
- const childScope = effectScope(true)
- childScope.on()
- childScope.on()
- childScope.off()
- childScope.off()
- childScope.off()
+ setCurrentScope(setCurrentScope(childScope))
expect(getCurrentScope()).toBe(parentScope)
})
})
@@ -372,7 +360,17 @@ describe('reactivity/effect/scope', () => {
expect(watcherCalls).toBe(3)
expect(cleanupCalls).toBe(1)
- expect(scope.effects.length).toBe(0)
- expect(scope.cleanups.length).toBe(0)
+ expect(getEffectsCount(scope)).toBe(0)
+ expect(scope.cleanups).toBe(0)
})
})
+
+function getEffectsCount(scope: EffectScope): number {
+ let n = 0
+ for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+ if (dep.dep instanceof ReactiveEffect) {
+ n++
+ }
+ }
+ return n
+}
diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts
index 70670d81ec2..6e6488cea87 100644
--- a/packages/reactivity/src/computed.ts
+++ b/packages/reactivity/src/computed.ts
@@ -14,11 +14,11 @@ import {
type Link,
type Subscriber,
SubscriberFlags,
+ checkDirty,
endTracking,
link,
processComputedUpdate,
startTracking,
- updateDirtyFlag,
} from './system'
import { warn } from './warning'
@@ -66,7 +66,7 @@ export class ComputedRefImpl implements Dependency, Subscriber {
// Subscriber
deps: Link | undefined = undefined
depsTail: Link | undefined = undefined
- flags: SubscriberFlags = SubscriberFlags.Computed | SubscriberFlags.Dirty
+ flags: SubscriberFlags = SubscriberFlags.Dirty
/**
* @internal
@@ -93,13 +93,17 @@ export class ComputedRefImpl implements Dependency, Subscriber {
*/
get _dirty(): boolean {
const flags = this.flags
- if (
- flags & SubscriberFlags.Dirty ||
- (flags & SubscriberFlags.PendingComputed &&
- updateDirtyFlag(this, this.flags))
- ) {
+ if (flags & SubscriberFlags.Dirty) {
return true
}
+ if (flags & SubscriberFlags.Pending) {
+ if (checkDirty(this.deps!)) {
+ this.flags = flags | SubscriberFlags.Dirty
+ return true
+ } else {
+ this.flags = flags & ~SubscriberFlags.Pending
+ }
+ }
return false
}
/**
@@ -110,7 +114,7 @@ export class ComputedRefImpl implements Dependency, Subscriber {
if (v) {
this.flags |= SubscriberFlags.Dirty
} else {
- this.flags &= ~(SubscriberFlags.Dirty | SubscriberFlags.PendingComputed)
+ this.flags &= ~(SubscriberFlags.Dirty | SubscriberFlags.Pending)
}
}
@@ -134,7 +138,7 @@ export class ComputedRefImpl implements Dependency, Subscriber {
get value(): T {
const flags = this.flags
- if (flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed)) {
+ if (flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) {
processComputedUpdate(this, flags)
}
if (activeSub !== undefined) {
diff --git a/packages/reactivity/src/debug.ts b/packages/reactivity/src/debug.ts
index 5503dc8a11b..c1f35b62ad8 100644
--- a/packages/reactivity/src/debug.ts
+++ b/packages/reactivity/src/debug.ts
@@ -69,8 +69,11 @@ function setupFlagsHandler(target: Subscriber): void {
},
set(value) {
if (
- !((target as any)._flags & SubscriberFlags.Propagated) &&
- !!(value & SubscriberFlags.Propagated)
+ !(
+ (target as any)._flags &
+ (SubscriberFlags.Dirty | SubscriberFlags.Pending)
+ ) &&
+ !!(value & (SubscriberFlags.Dirty | SubscriberFlags.Pending))
) {
onTrigger(this)
}
diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts
index a77c4bf2b18..c26d455807b 100644
--- a/packages/reactivity/src/effect.ts
+++ b/packages/reactivity/src/effect.ts
@@ -3,12 +3,15 @@ import type { TrackOpTypes, TriggerOpTypes } from './constants'
import { setupOnTrigger } from './debug'
import { activeEffectScope } from './effectScope'
import {
+ type Dependency,
type Link,
type Subscriber,
SubscriberFlags,
+ checkDirty,
endTracking,
+ link,
startTracking,
- updateDirtyFlag,
+ unlink,
} from './system'
import { warn } from './warning'
@@ -49,32 +52,40 @@ export enum EffectFlags {
ALLOW_RECURSE = 1 << 7,
PAUSED = 1 << 8,
NOTIFIED = 1 << 9,
- STOP = 1 << 10,
}
-export class ReactiveEffect implements ReactiveEffectOptions {
+export class ReactiveEffect
+ implements ReactiveEffectOptions, Dependency, Subscriber
+{
// Subscriber
deps: Link | undefined = undefined
depsTail: Link | undefined = undefined
- flags: number = SubscriberFlags.Effect
+ flags: number = SubscriberFlags.Dirty
+ cleanups: number = 0
- /**
- * @internal
- */
- cleanup?: () => void = undefined
+ // Dependency
+ subs: Link | undefined = undefined
+ subsTail: Link | undefined = undefined
- onStop?: () => void
+ // dev only
onTrack?: (event: DebuggerEvent) => void
+ // dev only
onTrigger?: (event: DebuggerEvent) => void
- constructor(public fn: () => T) {
- if (activeEffectScope && activeEffectScope.active) {
- activeEffectScope.effects.push(this)
+ // @ts-expect-error
+ fn(): T {}
+
+ constructor(fn?: () => T) {
+ if (fn !== undefined) {
+ this.fn = fn
+ }
+ if (activeEffectScope) {
+ link(this, activeEffectScope)
}
}
get active(): boolean {
- return !(this.flags & EffectFlags.STOP)
+ return this.deps !== undefined
}
pause(): void {
@@ -84,12 +95,12 @@ export class ReactiveEffect implements ReactiveEffectOptions {
}
resume(): void {
- const flags = this.flags
+ let flags = this.flags
if (flags & EffectFlags.PAUSED) {
- this.flags &= ~EffectFlags.PAUSED
+ this.flags = flags &= ~EffectFlags.PAUSED
}
if (flags & EffectFlags.NOTIFIED) {
- this.flags &= ~EffectFlags.NOTIFIED
+ this.flags = flags &= ~EffectFlags.NOTIFIED
this.notify()
}
}
@@ -99,7 +110,7 @@ export class ReactiveEffect implements ReactiveEffectOptions {
if (!(flags & EffectFlags.PAUSED)) {
this.scheduler()
} else {
- this.flags |= EffectFlags.NOTIFIED
+ this.flags = flags | EffectFlags.NOTIFIED
}
}
@@ -110,13 +121,10 @@ export class ReactiveEffect implements ReactiveEffectOptions {
}
run(): T {
- // TODO cleanupEffect
-
- if (!this.active) {
- // stopped during cleanup
- return this.fn()
+ const cleanups = this.cleanups
+ if (cleanups) {
+ cleanup(this, cleanups)
}
- cleanupEffect(this)
const prevSub = activeSub
setActiveSub(this)
startTracking(this)
@@ -132,34 +140,43 @@ export class ReactiveEffect implements ReactiveEffectOptions {
}
setActiveSub(prevSub)
endTracking(this)
+ const flags = this.flags
if (
- this.flags & SubscriberFlags.Recursed &&
- this.flags & EffectFlags.ALLOW_RECURSE
+ (flags & (SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE)) ===
+ (SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE)
) {
- this.flags &= ~SubscriberFlags.Recursed
+ this.flags = flags & ~SubscriberFlags.Recursed
this.notify()
}
}
}
stop(): void {
- if (this.active) {
- startTracking(this)
- endTracking(this)
- cleanupEffect(this)
- this.onStop && this.onStop()
- this.flags |= EffectFlags.STOP
+ const sub = this.subs
+ const cleanups = this.cleanups
+ if (sub !== undefined) {
+ unlink(sub)
+ }
+ startTracking(this)
+ endTracking(this)
+ if (cleanups) {
+ cleanup(this, cleanups)
}
}
get dirty(): boolean {
const flags = this.flags
- if (
- flags & SubscriberFlags.Dirty ||
- (flags & SubscriberFlags.PendingComputed && updateDirtyFlag(this, flags))
- ) {
+ if (flags & SubscriberFlags.Dirty) {
return true
}
+ if (flags & SubscriberFlags.Pending) {
+ if (checkDirty(this.deps!)) {
+ this.flags = flags | SubscriberFlags.Dirty
+ return true
+ } else {
+ this.flags = flags & ~SubscriberFlags.Pending
+ }
+ }
return false
}
}
@@ -183,6 +200,15 @@ export function effect(
const e = new ReactiveEffect(fn)
if (options) {
+ const onStop = options.onStop
+ if (onStop !== undefined) {
+ options.onStop = undefined
+ const stop = e.stop.bind(e)
+ e.stop = () => {
+ stop()
+ onStop()
+ }
+ }
extend(e, options)
}
try {
@@ -254,6 +280,32 @@ export function resetTracking(): void {
}
}
+const cleanupCbs = new WeakMap()
+
+export function onCleanup(
+ sub: Subscriber & { cleanups: number },
+ cb: () => void,
+): void {
+ const cbs = cleanupCbs.get(sub)
+ if (cbs === undefined) {
+ cleanupCbs.set(sub, [cb])
+ sub.cleanups = 1
+ } else {
+ cbs[sub.cleanups!++] = cb
+ }
+}
+
+export function cleanup(
+ sub: Subscriber & { cleanups: number },
+ length: number,
+): void {
+ const cbs = cleanupCbs.get(sub)!
+ for (let i = 0; i < length; ++i) {
+ cbs[i]()
+ }
+ sub.cleanups = 0
+}
+
/**
* Registers a cleanup function for the current active effect.
* The cleanup function is called right before the next effect run, or when the
@@ -267,8 +319,9 @@ export function resetTracking(): void {
* an active effect.
*/
export function onEffectCleanup(fn: () => void, failSilently = false): void {
- if (activeSub instanceof ReactiveEffect) {
- activeSub.cleanup = fn
+ const e = activeSub
+ if (e instanceof ReactiveEffect) {
+ onCleanup(e, () => cleanupEffect(fn))
} else if (__DEV__ && !failSilently) {
warn(
`onEffectCleanup() was called when there was no active effect` +
@@ -277,18 +330,14 @@ export function onEffectCleanup(fn: () => void, failSilently = false): void {
}
}
-function cleanupEffect(e: ReactiveEffect) {
- const { cleanup } = e
- e.cleanup = undefined
- if (cleanup !== undefined) {
- // run cleanup without active effect
- const prevSub = activeSub
- activeSub = undefined
- try {
- cleanup()
- } finally {
- activeSub = prevSub
- }
+function cleanupEffect(fn: () => void) {
+ // run cleanup without active effect
+ const prevSub = activeSub
+ activeSub = undefined
+ try {
+ fn()
+ } finally {
+ activeSub = prevSub
}
}
diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts
index 00fa403b02e..2c109c2084f 100644
--- a/packages/reactivity/src/effectScope.ts
+++ b/packages/reactivity/src/effectScope.ts
@@ -1,76 +1,49 @@
-import { EffectFlags, type ReactiveEffect } from './effect'
+import { EffectFlags, cleanup, onCleanup } from './effect'
import {
+ type Dependency,
type Link,
type Subscriber,
endTracking,
+ link,
startTracking,
+ unlink,
} from './system'
import { warn } from './warning'
export let activeEffectScope: EffectScope | undefined
-export class EffectScope implements Subscriber {
- // Subscriber: In order to collect orphans computeds
+export class EffectScope implements Subscriber, Dependency {
+ // Subscriber
deps: Link | undefined = undefined
depsTail: Link | undefined = undefined
flags: number = 0
+ cleanups: number = 0
- /**
- * @internal track `on` calls, allow `on` call multiple times
- */
- private _on = 0
- /**
- * @internal
- */
- effects: ReactiveEffect[] = []
- /**
- * @internal
- */
- cleanups: (() => void)[] = []
+ // Dependency
+ subs: Link | undefined = undefined
+ subsTail: Link | undefined = undefined
- /**
- * only assigned by undetached scope
- * @internal
- */
- parent: EffectScope | undefined
- /**
- * record undetached scopes
- * @internal
- */
- scopes: EffectScope[] | undefined
- /**
- * track a child scope's index in its parent's scopes array for optimized
- * removal
- * @internal
- */
- private index: number | undefined
-
- constructor(
- public detached = false,
- parent: EffectScope | undefined = activeEffectScope,
- ) {
- this.parent = parent
- if (!detached && parent) {
- this.index = (parent.scopes || (parent.scopes = [])).push(this) - 1
+ constructor(detached = false) {
+ if (!detached && activeEffectScope) {
+ link(this, activeEffectScope)
}
}
get active(): boolean {
- return !(this.flags & EffectFlags.STOP)
+ return this.deps !== undefined
}
+ notify(): void {}
+
pause(): void {
if (!(this.flags & EffectFlags.PAUSED)) {
this.flags |= EffectFlags.PAUSED
- let i, l
- if (this.scopes) {
- for (i = 0, l = this.scopes.length; i < l; i++) {
- this.scopes[i].pause()
+ for (let link = this.deps; link !== undefined; link = link.nextDep) {
+ const dep = link.dep
+ if ('pause' in dep) {
+ dep.pause()
}
}
- for (i = 0, l = this.effects.length; i < l; i++) {
- this.effects[i].pause()
- }
}
}
@@ -78,90 +51,38 @@ export class EffectScope implements Subscriber {
* Resumes the effect scope, including all child scopes and effects.
*/
resume(): void {
- if (this.flags & EffectFlags.PAUSED) {
- this.flags &= ~EffectFlags.PAUSED
- let i, l
- if (this.scopes) {
- for (i = 0, l = this.scopes.length; i < l; i++) {
- this.scopes[i].resume()
+ const flags = this.flags
+ if (flags & EffectFlags.PAUSED) {
+ this.flags = flags & ~EffectFlags.PAUSED
+ for (let link = this.deps; link !== undefined; link = link.nextDep) {
+ const dep = link.dep
+ if ('resume' in dep) {
+ dep.resume()
}
}
- for (i = 0, l = this.effects.length; i < l; i++) {
- this.effects[i].resume()
- }
}
}
run(fn: () => T): T | undefined {
- if (this.active) {
- const prevEffectScope = activeEffectScope
- try {
- activeEffectScope = this
- return fn()
- } finally {
- activeEffectScope = prevEffectScope
- }
- } else if (__DEV__) {
- warn(`cannot run an inactive effect scope.`)
- }
- }
-
- prevScope: EffectScope | undefined
- /**
- * This should only be called on non-detached scopes
- * @internal
- */
- on(): void {
- if (++this._on === 1) {
- this.prevScope = activeEffectScope
+ const prevEffectScope = activeEffectScope
+ try {
activeEffectScope = this
+ return fn()
+ } finally {
+ activeEffectScope = prevEffectScope
}
}
- /**
- * This should only be called on non-detached scopes
- * @internal
- */
- off(): void {
- if (this._on > 0 && --this._on === 0) {
- activeEffectScope = this.prevScope
- this.prevScope = undefined
+ stop(): void {
+ const sub = this.subs
+ const cleanups = this.cleanups
+ if (sub !== undefined) {
+ unlink(sub)
}
- }
-
- stop(fromParent?: boolean): void {
- if (this.active) {
- this.flags |= EffectFlags.STOP
- startTracking(this)
- endTracking(this)
- let i, l
- for (i = 0, l = this.effects.length; i < l; i++) {
- this.effects[i].stop()
- }
- this.effects.length = 0
-
- for (i = 0, l = this.cleanups.length; i < l; i++) {
- this.cleanups[i]()
- }
- this.cleanups.length = 0
-
- if (this.scopes) {
- for (i = 0, l = this.scopes.length; i < l; i++) {
- this.scopes[i].stop(true)
- }
- this.scopes.length = 0
- }
-
- // nested scope, dereference from parent to avoid memory leaks
- if (!this.detached && this.parent && !fromParent) {
- // optimized O(1) removal
- const last = this.parent.scopes!.pop()
- if (last && last !== this) {
- this.parent.scopes![this.index!] = last
- last.index = this.index!
- }
- }
- this.parent = undefined
+ startTracking(this)
+ endTracking(this)
+ if (cleanups) {
+ cleanup(this, cleanups)
}
}
}
@@ -188,6 +109,14 @@ export function getCurrentScope(): EffectScope | undefined {
return activeEffectScope
}
+export function setCurrentScope(
+ scope: EffectScope | undefined,
+): EffectScope | undefined {
+ const prevScope = activeEffectScope
+ activeEffectScope = scope
+ return prevScope
+}
+
/**
* Registers a dispose callback on the current active effect scope. The
* callback will be invoked when the associated effect scope is stopped.
@@ -196,8 +125,8 @@ export function getCurrentScope(): EffectScope | undefined {
* @see {@link https://vuejs.org/api/reactivity-advanced.html#onscopedispose}
*/
export function onScopeDispose(fn: () => void, failSilently = false): void {
- if (activeEffectScope) {
- activeEffectScope.cleanups.push(fn)
+ if (activeEffectScope !== undefined) {
+ onCleanup(activeEffectScope, fn)
} else if (__DEV__ && !failSilently) {
warn(
`onScopeDispose() is called when there is no active effect scope` +
diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts
index f0445e87da0..509152663da 100644
--- a/packages/reactivity/src/index.ts
+++ b/packages/reactivity/src/index.ts
@@ -76,6 +76,10 @@ export {
effectScope,
EffectScope,
getCurrentScope,
+ /**
+ * @internal
+ */
+ setCurrentScope,
onScopeDispose,
} from './effectScope'
export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts
index 5239f34bf3f..b8b1c1e32fc 100644
--- a/packages/reactivity/src/ref.ts
+++ b/packages/reactivity/src/ref.ts
@@ -144,19 +144,22 @@ class RefImpl implements Dependency {
if (hasChanged(newValue, oldValue)) {
this._rawValue = newValue
this._value =
- this._wrap && !useDirectValue ? this._wrap(newValue) : newValue
- if (__DEV__) {
- triggerEventInfos.push({
- target: this,
- type: TriggerOpTypes.SET,
- key: 'value',
- newValue,
- oldValue,
- })
- }
- triggerRef(this as unknown as Ref)
- if (__DEV__) {
- triggerEventInfos.pop()
+ !useDirectValue && this._wrap ? this._wrap(newValue) : newValue
+ const subs = this.subs
+ if (subs !== undefined) {
+ if (__DEV__) {
+ triggerEventInfos.push({
+ target: this,
+ type: TriggerOpTypes.SET,
+ key: 'value',
+ newValue,
+ oldValue,
+ })
+ }
+ propagate(subs)
+ if (__DEV__) {
+ triggerEventInfos.pop()
+ }
}
}
}
diff --git a/packages/reactivity/src/system.ts b/packages/reactivity/src/system.ts
index b3699f727c8..6e6ecfd6467 100644
--- a/packages/reactivity/src/system.ts
+++ b/packages/reactivity/src/system.ts
@@ -2,6 +2,7 @@
// Ported from https://github.com/stackblitz/alien-signals/blob/v1.0.13/src/system.ts
import type { ComputedRefImpl as Computed } from './computed.js'
import type { ReactiveEffect as Effect } from './effect.js'
+import { EffectScope } from './effectScope.js'
export interface Dependency {
subs: Link | undefined
@@ -15,21 +16,19 @@ export interface Subscriber {
}
export interface Link {
- dep: Dependency | Computed
- sub: Subscriber | Computed | Effect
+ dep: Dependency | Computed | Effect | EffectScope
+ sub: Subscriber | Computed | Effect | EffectScope
prevSub: Link | undefined
nextSub: Link | undefined
+ prevDep: Link | undefined
nextDep: Link | undefined
}
export const enum SubscriberFlags {
- Computed = 1 << 0,
- Effect = 1 << 1,
Tracking = 1 << 2,
Recursed = 1 << 4,
Dirty = 1 << 5,
- PendingComputed = 1 << 6,
- Propagated = Dirty | PendingComputed,
+ Pending = 1 << 6,
}
interface OneWayLink {
@@ -37,7 +36,7 @@ interface OneWayLink {
linked: OneWayLink | undefined
}
-const notifyBuffer: (Effect | undefined)[] = []
+const notifyBuffer: (Effect | EffectScope | undefined)[] = []
let batchDepth = 0
let notifyIndex = 0
@@ -53,12 +52,12 @@ export function endBatch(): void {
}
}
-export function link(dep: Dependency, sub: Subscriber): Link | undefined {
- const currentDep = sub.depsTail
- if (currentDep !== undefined && currentDep.dep === dep) {
+export function link(dep: Dependency, sub: Subscriber): void {
+ const prevDep = sub.depsTail
+ if (prevDep !== undefined && prevDep.dep === dep) {
return
}
- const nextDep = currentDep !== undefined ? currentDep.nextDep : sub.deps
+ const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps
if (nextDep !== undefined && nextDep.dep === dep) {
sub.depsTail = nextDep
return
@@ -71,7 +70,75 @@ export function link(dep: Dependency, sub: Subscriber): Link | undefined {
) {
return
}
- return linkNewDep(dep, sub, nextDep, currentDep)
+ const newLink: Link = {
+ dep,
+ sub,
+ prevDep,
+ nextDep,
+ prevSub: undefined,
+ nextSub: undefined,
+ }
+ if (prevDep === undefined) {
+ sub.deps = newLink
+ } else {
+ prevDep.nextDep = newLink
+ }
+ if (dep.subs === undefined) {
+ dep.subs = newLink
+ } else {
+ const oldTail = dep.subsTail!
+ newLink.prevSub = oldTail
+ oldTail.nextSub = newLink
+ }
+ if (nextDep !== undefined) {
+ nextDep.prevDep = newLink
+ }
+ sub.depsTail = newLink
+ dep.subsTail = newLink
+}
+
+export function unlink(
+ link: Link,
+ sub: Subscriber = link.sub,
+): Link | undefined {
+ const dep = link.dep
+ const prevDep = link.prevDep
+ const nextDep = link.nextDep
+ const nextSub = link.nextSub
+ const prevSub = link.prevSub
+ if (nextSub !== undefined) {
+ nextSub.prevSub = prevSub
+ } else {
+ dep.subsTail = prevSub
+ }
+ if (prevSub !== undefined) {
+ prevSub.nextSub = nextSub
+ } else {
+ dep.subs = nextSub
+ }
+ if (nextDep !== undefined) {
+ nextDep.prevDep = prevDep
+ } else {
+ sub.depsTail = prevDep
+ }
+ if (prevDep !== undefined) {
+ prevDep.nextDep = nextDep
+ } else {
+ sub.deps = nextDep
+ }
+ if (dep.subs === undefined && 'deps' in dep) {
+ let toRemove = dep.deps
+ while (toRemove !== undefined) {
+ toRemove = unlink(toRemove, dep)
+ }
+ const depFlags = dep.flags
+ if ('stop' in dep) {
+ dep.stop()
+ } else if (!(depFlags & SubscriberFlags.Dirty)) {
+ dep.flags = depFlags | SubscriberFlags.Dirty
+ }
+ }
+ return nextDep
}
export function propagate(current: Link): void {
@@ -91,7 +158,8 @@ export function propagate(current: Link): void {
subFlags &
(SubscriberFlags.Tracking |
SubscriberFlags.Recursed |
- SubscriberFlags.Propagated)
+ SubscriberFlags.Dirty |
+ SubscriberFlags.Pending)
)
) {
sub.flags = subFlags | targetFlag
@@ -103,33 +171,34 @@ export function propagate(current: Link): void {
sub.flags = (subFlags & ~SubscriberFlags.Recursed) | targetFlag
shouldNotify = true
} else if (
- !(subFlags & SubscriberFlags.Propagated) &&
+ !(subFlags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) &&
isValidLink(current, sub)
) {
sub.flags = subFlags | SubscriberFlags.Recursed | targetFlag
- shouldNotify = (sub as Dependency).subs !== undefined
+ shouldNotify = !('notify' in sub)
}
if (shouldNotify) {
- const subSubs = (sub as Dependency).subs
- if (subSubs !== undefined) {
- current = subSubs
- if (subSubs.nextSub !== undefined) {
- branchs = { target: next, linked: branchs }
- ++branchDepth
- next = current.nextSub
+ if ('notify' in sub) {
+ notifyBuffer[notifyBufferLength++] = sub
+ } else {
+ const subSubs = (sub as Dependency).subs
+ if (subSubs !== undefined) {
+ current = subSubs
+ if (subSubs.nextSub !== undefined) {
+ branchs = { target: next, linked: branchs }
+ ++branchDepth
+ next = current.nextSub
+ }
+ targetFlag = SubscriberFlags.Pending
+ continue
}
- targetFlag = SubscriberFlags.PendingComputed
- continue
- }
- if (subFlags & SubscriberFlags.Effect) {
- notifyBuffer[notifyBufferLength++] = sub as Effect
}
} else if (!(subFlags & (SubscriberFlags.Tracking | targetFlag))) {
sub.flags = subFlags | targetFlag
} else if (
!(subFlags & targetFlag) &&
- subFlags & SubscriberFlags.Propagated &&
+ subFlags & (SubscriberFlags.Dirty | SubscriberFlags.Pending) &&
isValidLink(current, sub)
) {
sub.flags = subFlags | targetFlag
@@ -137,9 +206,7 @@ export function propagate(current: Link): void {
if ((current = next!) !== undefined) {
next = current.nextSub
- targetFlag = branchDepth
- ? SubscriberFlags.PendingComputed
- : SubscriberFlags.Dirty
+ targetFlag = branchDepth ? SubscriberFlags.Pending : SubscriberFlags.Dirty
continue
}
@@ -149,7 +216,7 @@ export function propagate(current: Link): void {
if (current !== undefined) {
next = current.nextSub
targetFlag = branchDepth
- ? SubscriberFlags.PendingComputed
+ ? SubscriberFlags.Pending
: SubscriberFlags.Dirty
continue top
}
@@ -166,38 +233,24 @@ export function propagate(current: Link): void {
export function startTracking(sub: Subscriber): void {
sub.depsTail = undefined
sub.flags =
- (sub.flags & ~(SubscriberFlags.Recursed | SubscriberFlags.Propagated)) |
+ (sub.flags &
+ ~(
+ SubscriberFlags.Recursed |
+ SubscriberFlags.Dirty |
+ SubscriberFlags.Pending
+ )) |
SubscriberFlags.Tracking
}
export function endTracking(sub: Subscriber): void {
const depsTail = sub.depsTail
- if (depsTail !== undefined) {
- const nextDep = depsTail.nextDep
- if (nextDep !== undefined) {
- clearTracking(nextDep)
- depsTail.nextDep = undefined
- }
- } else if (sub.deps !== undefined) {
- clearTracking(sub.deps)
- sub.deps = undefined
+ let toRemove = depsTail !== undefined ? depsTail.nextDep : sub.deps
+ while (toRemove !== undefined) {
+ toRemove = unlink(toRemove, sub)
}
sub.flags &= ~SubscriberFlags.Tracking
}
-export function updateDirtyFlag(
- sub: Subscriber,
- flags: SubscriberFlags,
-): boolean {
- if (checkDirty(sub.deps!)) {
- sub.flags = flags | SubscriberFlags.Dirty
- return true
- } else {
- sub.flags = flags & ~SubscriberFlags.PendingComputed
- return false
- }
-}
-
export function processComputedUpdate(
computed: Computed,
flags: SubscriberFlags,
@@ -210,7 +263,7 @@ export function processComputedUpdate(
}
}
} else {
- computed.flags = flags & ~SubscriberFlags.PendingComputed
+ computed.flags = flags & ~SubscriberFlags.Pending
}
}
@@ -224,41 +277,7 @@ export function processEffectNotifications(): void {
notifyBufferLength = 0
}
-function linkNewDep(
- dep: Dependency,
- sub: Subscriber,
- nextDep: Link | undefined,
- depsTail: Link | undefined,
-): Link {
- const newLink: Link = {
- dep,
- sub,
- nextDep,
- prevSub: undefined,
- nextSub: undefined,
- }
-
- if (depsTail === undefined) {
- sub.deps = newLink
- } else {
- depsTail.nextDep = newLink
- }
-
- if (dep.subs === undefined) {
- dep.subs = newLink
- } else {
- const oldTail = dep.subsTail!
- newLink.prevSub = oldTail
- oldTail.nextSub = newLink
- }
-
- sub.depsTail = newLink
- dep.subsTail = newLink
-
- return newLink
-}
-
-function checkDirty(current: Link): boolean {
+export function checkDirty(current: Link): boolean {
let prevLinks: OneWayLink | undefined
let checkDepth = 0
let dirty: boolean
@@ -269,12 +288,9 @@ function checkDirty(current: Link): boolean {
if (current.sub.flags & SubscriberFlags.Dirty) {
dirty = true
- } else if ('flags' in dep) {
+ } else if ('update' in dep) {
const depFlags = dep.flags
- if (
- (depFlags & (SubscriberFlags.Computed | SubscriberFlags.Dirty)) ===
- (SubscriberFlags.Computed | SubscriberFlags.Dirty)
- ) {
+ if (depFlags & SubscriberFlags.Dirty) {
if ((dep as Computed).update()) {
const subs = dep.subs!
if (subs.nextSub !== undefined) {
@@ -282,11 +298,7 @@ function checkDirty(current: Link): boolean {
}
dirty = true
}
- } else if (
- (depFlags &
- (SubscriberFlags.Computed | SubscriberFlags.PendingComputed)) ===
- (SubscriberFlags.Computed | SubscriberFlags.PendingComputed)
- ) {
+ } else if (depFlags & SubscriberFlags.Pending) {
if (current.nextSub !== undefined || current.prevSub !== undefined) {
prevLinks = { target: current, linked: prevLinks }
}
@@ -317,7 +329,7 @@ function checkDirty(current: Link): boolean {
continue
}
} else {
- sub.flags &= ~SubscriberFlags.PendingComputed
+ sub.flags &= ~SubscriberFlags.Pending
}
if (firstSub.nextSub !== undefined) {
current = prevLinks!.target
@@ -341,8 +353,8 @@ function shallowPropagate(link: Link): void {
const sub = link.sub
const subFlags = sub.flags
if (
- (subFlags & (SubscriberFlags.PendingComputed | SubscriberFlags.Dirty)) ===
- SubscriberFlags.PendingComputed
+ (subFlags & (SubscriberFlags.Pending | SubscriberFlags.Dirty)) ===
+ SubscriberFlags.Pending
) {
sub.flags = subFlags | SubscriberFlags.Dirty
}
@@ -366,40 +378,3 @@ function isValidLink(checkLink: Link, sub: Subscriber): boolean {
}
return false
}
-
-function clearTracking(link: Link): void {
- do {
- const dep = link.dep
- const nextDep = link.nextDep
- const nextSub = link.nextSub
- const prevSub = link.prevSub
-
- if (nextSub !== undefined) {
- nextSub.prevSub = prevSub
- } else {
- dep.subsTail = prevSub
- }
-
- if (prevSub !== undefined) {
- prevSub.nextSub = nextSub
- } else {
- dep.subs = nextSub
- }
-
- if (dep.subs === undefined && 'deps' in dep) {
- const depFlags = dep.flags
- if (!(depFlags & SubscriberFlags.Dirty)) {
- dep.flags = depFlags | SubscriberFlags.Dirty
- }
- const depDeps = dep.deps
- if (depDeps !== undefined) {
- link = depDeps
- dep.depsTail!.nextDep = nextDep
- dep.deps = undefined
- dep.depsTail = undefined
- continue
- }
- }
- link = nextDep!
- } while (link !== undefined)
-}
diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts
index 094bf226ca8..bac8e1ba40d 100644
--- a/packages/reactivity/src/watch.ts
+++ b/packages/reactivity/src/watch.ts
@@ -8,18 +8,17 @@ import {
isObject,
isPlainObject,
isSet,
- remove,
} from '@vue/shared'
import type { ComputedRef } from './computed'
import { ReactiveFlags } from './constants'
import {
type DebuggerOptions,
- type EffectScheduler,
ReactiveEffect,
+ cleanup,
+ onCleanup,
pauseTracking,
resetTracking,
} from './effect'
-import { getCurrentScope } from './effectScope'
import { isReactive, isShallow } from './reactive'
import { type Ref, isRef } from './ref'
import { warn } from './warning'
@@ -78,8 +77,7 @@ const INITIAL_WATCHER_VALUE = {}
export type WatchScheduler = (job: () => void, isFirstRun: boolean) => void
-const cleanupMap: WeakMap void)[]> = new WeakMap()
-let activeWatcher: ReactiveEffect | undefined = undefined
+let activeWatcher: WatcherEffect | undefined = undefined
/**
* Returns the current active effect if there is one.
@@ -102,12 +100,15 @@ export function getCurrentWatcher(): ReactiveEffect | undefined {
export function onWatcherCleanup(
cleanupFn: () => void,
failSilently = false,
- owner: ReactiveEffect | undefined = activeWatcher,
+ owner: WatcherEffect | undefined = activeWatcher,
): void {
if (owner) {
- let cleanups = cleanupMap.get(owner)
- if (!cleanups) cleanupMap.set(owner, (cleanups = []))
- cleanups.push(cleanupFn)
+ const { call } = owner.options
+ if (call) {
+ onCleanup(owner, () => call(cleanupFn, WatchErrorCodes.WATCH_CLEANUP))
+ } else {
+ onCleanup(owner, cleanupFn)
+ }
} else if (__DEV__ && !failSilently) {
warn(
`onWatcherCleanup() was called when there was no active watcher` +
@@ -116,212 +117,212 @@ export function onWatcherCleanup(
}
}
-export function watch(
- source: WatchSource | WatchSource[] | WatchEffect | object,
- cb?: WatchCallback | null,
- options: WatchOptions = EMPTY_OBJ,
-): WatchHandle {
- const { immediate, deep, once, scheduler, augmentJob, call } = options
-
- const warnInvalidSource = (s: unknown) => {
- ;(options.onWarn || warn)(
- `Invalid watch source: `,
- s,
- `A watch source can only be a getter/effect function, a ref, ` +
- `a reactive object, or an array of these types.`,
- )
- }
-
- const reactiveGetter = (source: object) => {
- // traverse will happen in wrapped getter below
- if (deep) return source
- // for `deep: false | 0` or shallow reactive, only traverse root-level properties
- if (isShallow(source) || deep === false || deep === 0)
- return traverse(source, 1)
- // for `deep: undefined` on a reactive object, deeply traverse all properties
- return traverse(source)
- }
-
- let effect: ReactiveEffect
- let getter: () => any
- let cleanup: (() => void) | undefined
- let boundCleanup: typeof onWatcherCleanup
- let forceTrigger = false
- let isMultiSource = false
-
- if (isRef(source)) {
- getter = () => source.value
- forceTrigger = isShallow(source)
- } else if (isReactive(source)) {
- getter = () => reactiveGetter(source)
- forceTrigger = true
- } else if (isArray(source)) {
- isMultiSource = true
- forceTrigger = source.some(s => isReactive(s) || isShallow(s))
- getter = () =>
- source.map(s => {
- if (isRef(s)) {
- return s.value
- } else if (isReactive(s)) {
- return reactiveGetter(s)
- } else if (isFunction(s)) {
- return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
- } else {
- __DEV__ && warnInvalidSource(s)
- }
- })
- } else if (isFunction(source)) {
- if (cb) {
- // getter with cb
- getter = call
- ? () => call(source, WatchErrorCodes.WATCH_GETTER)
- : (source as () => any)
- } else {
- // no cb -> simple effect
- getter = () => {
- if (cleanup) {
- pauseTracking()
+class WatcherEffect extends ReactiveEffect {
+ forceTrigger: boolean
+ isMultiSource: boolean
+ oldValue: any
+ boundCleanup: typeof onWatcherCleanup = fn =>
+ onWatcherCleanup(fn, false, this)
+
+ constructor(
+ source: WatchSource | WatchSource[] | WatchEffect | object,
+ public cb?: WatchCallback | null | undefined,
+ public options: WatchOptions = EMPTY_OBJ,
+ ) {
+ const { immediate, deep, once, scheduler, augmentJob, call, onWarn } =
+ options
+
+ let getter: () => any
+ let forceTrigger = false
+ let isMultiSource = false
+
+ if (isRef(source)) {
+ getter = () => source.value
+ forceTrigger = isShallow(source)
+ } else if (isReactive(source)) {
+ getter = () => reactiveGetter(source, deep)
+ forceTrigger = true
+ } else if (isArray(source)) {
+ isMultiSource = true
+ forceTrigger = source.some(s => isReactive(s) || isShallow(s))
+ getter = () =>
+ source.map(s => {
+ if (isRef(s)) {
+ return s.value
+ } else if (isReactive(s)) {
+ return reactiveGetter(s, deep)
+ } else if (isFunction(s)) {
+ return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
+ } else {
+ __DEV__ && warnInvalidSource(s, onWarn)
+ }
+ })
+ } else if (isFunction(source)) {
+ if (cb) {
+ // getter with cb
+ getter = call
+ ? () => call(source, WatchErrorCodes.WATCH_GETTER)
+ : (source as () => any)
+ } else {
+ // no cb -> simple effect
+ getter = () => {
+ if (this.cleanups) {
+ pauseTracking()
+ try {
+ cleanup(this, this.cleanups)
+ } finally {
+ resetTracking()
+ }
+ }
+ const currentEffect = activeWatcher
+ activeWatcher = this
try {
- cleanup()
+ return call
+ ? call(source, WatchErrorCodes.WATCH_CALLBACK, [
+ this.boundCleanup,
+ ])
+ : source(this.boundCleanup)
} finally {
- resetTracking()
+ activeWatcher = currentEffect
}
}
- const currentEffect = activeWatcher
- activeWatcher = effect
- try {
- return call
- ? call(source, WatchErrorCodes.WATCH_CALLBACK, [boundCleanup])
- : source(boundCleanup)
- } finally {
- activeWatcher = currentEffect
- }
}
+ } else {
+ getter = NOOP
+ __DEV__ && warnInvalidSource(source, onWarn)
}
- } else {
- getter = NOOP
- __DEV__ && warnInvalidSource(source)
- }
- if (cb && deep) {
- const baseGetter = getter
- const depth = deep === true ? Infinity : deep
- getter = () => traverse(baseGetter(), depth)
- }
+ if (cb && deep) {
+ const baseGetter = getter
+ const depth = deep === true ? Infinity : deep
+ getter = () => traverse(baseGetter(), depth)
+ }
- const scope = getCurrentScope()
- const watchHandle: WatchHandle = () => {
- effect.stop()
- if (scope && scope.active) {
- remove(scope.effects, effect)
+ super(getter)
+ this.forceTrigger = forceTrigger
+ this.isMultiSource = isMultiSource
+
+ if (once && cb) {
+ const _cb = cb
+ cb = (...args) => {
+ _cb(...args)
+ this.stop()
+ }
}
- }
- if (once && cb) {
- const _cb = cb
- cb = (...args) => {
- _cb(...args)
- watchHandle()
+ this.cb = cb
+
+ this.oldValue = isMultiSource
+ ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
+ : INITIAL_WATCHER_VALUE
+
+ const job = this.scheduler.bind(this)
+
+ if (augmentJob) {
+ augmentJob(job)
+ }
+
+ if (scheduler) {
+ this.scheduler = () => scheduler(job, false)
+ }
+
+ if (__DEV__) {
+ this.onTrack = options.onTrack
+ this.onTrigger = options.onTrigger
}
- }
- let oldValue: any = isMultiSource
- ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
- : INITIAL_WATCHER_VALUE
+ // initial run
+ if (cb) {
+ if (immediate) {
+ job()
+ } else {
+ this.oldValue = this.run()
+ }
+ } else if (scheduler) {
+ scheduler(job, true)
+ } else {
+ this.run()
+ }
+ }
- const job = (immediateFirstRun?: boolean) => {
- if (!effect.active || (!immediateFirstRun && !effect.dirty)) {
+ scheduler(): void {
+ if (!this.dirty) {
return
}
- if (cb) {
+ if (this.cb) {
// watch(source, cb)
- const newValue = effect.run()
+ const newValue = this.run()
+ const { deep, call } = this.options
if (
deep ||
- forceTrigger ||
- (isMultiSource
- ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
- : hasChanged(newValue, oldValue))
+ this.forceTrigger ||
+ (this.isMultiSource
+ ? (newValue as any[]).some((v, i) => hasChanged(v, this.oldValue[i]))
+ : hasChanged(newValue, this.oldValue))
) {
// cleanup before running cb again
- if (cleanup) {
- cleanup()
+ if (this.cleanups) {
+ cleanup(this, this.cleanups)
}
const currentWatcher = activeWatcher
- activeWatcher = effect
+ activeWatcher = this
try {
const args = [
newValue,
// pass undefined as the old value when it's changed for the first time
- oldValue === INITIAL_WATCHER_VALUE
+ this.oldValue === INITIAL_WATCHER_VALUE
? undefined
- : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
+ : this.isMultiSource && this.oldValue[0] === INITIAL_WATCHER_VALUE
? []
- : oldValue,
- boundCleanup,
+ : this.oldValue,
+ this.boundCleanup,
]
call
- ? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
+ ? call(this.cb, WatchErrorCodes.WATCH_CALLBACK, args)
: // @ts-expect-error
- cb!(...args)
- oldValue = newValue
+ this.cb(...args)
+ this.oldValue = newValue
} finally {
activeWatcher = currentWatcher
}
}
} else {
// watchEffect
- effect.run()
+ this.run()
}
}
+}
- if (augmentJob) {
- augmentJob(job)
- }
-
- effect = new ReactiveEffect(getter)
-
- effect.scheduler = scheduler
- ? () => scheduler(job, false)
- : (job as EffectScheduler)
-
- boundCleanup = fn => onWatcherCleanup(fn, false, effect)
-
- cleanup = effect.onStop = () => {
- const cleanups = cleanupMap.get(effect)
- if (cleanups) {
- if (call) {
- call(cleanups, WatchErrorCodes.WATCH_CLEANUP)
- } else {
- for (const cleanup of cleanups) cleanup()
- }
- cleanupMap.delete(effect)
- }
- }
-
- if (__DEV__) {
- effect.onTrack = options.onTrack
- effect.onTrigger = options.onTrigger
- }
-
- // initial run
- if (cb) {
- if (immediate) {
- job(true)
- } else {
- oldValue = effect.run()
- }
- } else if (scheduler) {
- scheduler(job.bind(null, true), true)
- } else {
- effect.run()
- }
+function reactiveGetter(source: object, deep: WatchOptions['deep']): unknown {
+ // traverse will happen in wrapped getter below
+ if (deep) return source
+ // for `deep: false | 0` or shallow reactive, only traverse root-level properties
+ if (isShallow(source) || deep === false || deep === 0)
+ return traverse(source, 1)
+ // for `deep: undefined` on a reactive object, deeply traverse all properties
+ return traverse(source)
+}
- watchHandle.pause = effect.pause.bind(effect)
- watchHandle.resume = effect.resume.bind(effect)
- watchHandle.stop = watchHandle
+function warnInvalidSource(s: object, onWarn: WatchOptions['onWarn']): void {
+ ;(onWarn || warn)(
+ `Invalid watch source: `,
+ s,
+ `A watch source can only be a getter/effect function, a ref, ` +
+ `a reactive object, or an array of these types.`,
+ )
+}
- return watchHandle
+export function watch(
+ source: WatchSource | WatchSource[] | WatchEffect | object,
+ cb?: WatchCallback | null,
+ options: WatchOptions = EMPTY_OBJ,
+): WatchHandle {
+ const effect = new WatcherEffect(source, cb, options)
+ const stop = effect.stop.bind(effect) as WatchHandle
+ stop.pause = effect.pause.bind(effect)
+ stop.resume = effect.resume.bind(effect)
+ stop.stop = stop
+ return stop
}
export function traverse(
diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts
index 39032a63699..932a762da90 100644
--- a/packages/runtime-core/__tests__/apiWatch.spec.ts
+++ b/packages/runtime-core/__tests__/apiWatch.spec.ts
@@ -25,7 +25,9 @@ import {
} from '@vue/runtime-test'
import {
type DebuggerEvent,
+ type EffectScope,
ITERATE_KEY,
+ ReactiveEffect,
type Ref,
type ShallowRef,
TrackOpTypes,
@@ -1332,16 +1334,15 @@ describe('api: watch', () => {
render(h(Comp), nodeOps.createElement('div'))
expect(instance!).toBeDefined()
- expect(instance!.scope.effects).toBeInstanceOf(Array)
// includes the component's own render effect AND the watcher effect
- expect(instance!.scope.effects.length).toBe(2)
+ expect(getEffectsCount(instance!.scope)).toBe(2)
_show!.value = false
await nextTick()
await nextTick()
- expect(instance!.scope.effects.length).toBe(0)
+ expect(getEffectsCount(instance!.scope)).toBe(0)
})
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
@@ -1489,7 +1490,7 @@ describe('api: watch', () => {
createApp(Comp).mount(root)
// should not record watcher in detached scope and only the instance's
// own update effect
- expect(instance!.scope.effects.length).toBe(1)
+ expect(getEffectsCount(instance!.scope)).toBe(1)
})
test('watchEffect should keep running if created in a detached scope', async () => {
@@ -1796,9 +1797,9 @@ describe('api: watch', () => {
}
const root = nodeOps.createElement('div')
createApp(Comp).mount(root)
- expect(instance!.scope.effects.length).toBe(2)
+ expect(getEffectsCount(instance!.scope)).toBe(2)
unwatch!()
- expect(instance!.scope.effects.length).toBe(1)
+ expect(getEffectsCount(instance!.scope)).toBe(1)
const scope = effectScope()
scope.run(() => {
@@ -1806,14 +1807,14 @@ describe('api: watch', () => {
console.log(num.value)
})
})
- expect(scope.effects.length).toBe(1)
+ expect(getEffectsCount(scope)).toBe(1)
unwatch!()
- expect(scope.effects.length).toBe(0)
+ expect(getEffectsCount(scope)).toBe(0)
scope.run(() => {
watch(num, () => {}, { once: true, immediate: true })
})
- expect(scope.effects.length).toBe(0)
+ expect(getEffectsCount(scope)).toBe(0)
})
// simplified case of VueUse syncRef
@@ -2011,3 +2012,13 @@ describe('api: watch', () => {
expect(onCleanup).toBeCalledTimes(0)
})
})
+
+function getEffectsCount(scope: EffectScope): number {
+ let n = 0
+ for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+ if (dep.dep instanceof ReactiveEffect) {
+ n++
+ }
+ }
+ return n
+}
diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts
index f2de08a4032..39a316fcbb3 100644
--- a/packages/runtime-core/__tests__/scheduler.spec.ts
+++ b/packages/runtime-core/__tests__/scheduler.spec.ts
@@ -125,9 +125,8 @@ describe('scheduler', () => {
calls.push('cb1')
queueJob(job1)
}
- cb1.flags! |= SchedulerJobFlags.PRE
- queueJob(cb1)
+ queueJob(cb1, true)
await nextTick()
expect(calls).toEqual(['cb1', 'job1'])
})
@@ -143,24 +142,21 @@ describe('scheduler', () => {
calls.push('cb1')
queueJob(job1)
// cb2 should execute before the job
- queueJob(cb2)
- queueJob(cb3)
+ queueJob(cb2, true)
+ queueJob(cb3, true)
}
- cb1.flags! |= SchedulerJobFlags.PRE
const cb2: SchedulerJob = () => {
calls.push('cb2')
}
- cb2.flags! |= SchedulerJobFlags.PRE
cb2.id = 1
const cb3: SchedulerJob = () => {
calls.push('cb3')
}
- cb3.flags! |= SchedulerJobFlags.PRE
cb3.id = 1
- queueJob(cb1)
+ queueJob(cb1, true)
await nextTick()
expect(calls).toEqual(['cb1', 'cb2', 'cb3', 'job1'])
})
@@ -171,24 +167,20 @@ describe('scheduler', () => {
calls.push('job1')
}
job1.id = 1
- job1.flags! |= SchedulerJobFlags.PRE
const job2: SchedulerJob = () => {
calls.push('job2')
queueJob(job5)
- queueJob(job6)
+ queueJob(job6, true)
}
job2.id = 2
- job2.flags! |= SchedulerJobFlags.PRE
const job3: SchedulerJob = () => {
calls.push('job3')
}
job3.id = 2
- job3.flags! |= SchedulerJobFlags.PRE
const job4: SchedulerJob = () => {
calls.push('job4')
}
job4.id = 3
- job4.flags! |= SchedulerJobFlags.PRE
const job5: SchedulerJob = () => {
calls.push('job5')
}
@@ -197,14 +189,13 @@ describe('scheduler', () => {
calls.push('job6')
}
job6.id = 2
- job6.flags! |= SchedulerJobFlags.PRE
// We need several jobs to test this properly, otherwise
// findInsertionIndex can yield the correct index by chance
- queueJob(job4)
- queueJob(job2)
- queueJob(job3)
- queueJob(job1)
+ queueJob(job4, true)
+ queueJob(job2, true)
+ queueJob(job3, true)
+ queueJob(job1, true)
await nextTick()
expect(calls).toEqual(['job1', 'job2', 'job3', 'job6', 'job5', 'job4'])
@@ -217,8 +208,8 @@ describe('scheduler', () => {
// when updating the props of a child component. This is handled
// directly inside `updateComponentPreRender` to avoid non atomic
// cb triggers (#1763)
- queueJob(cb1)
- queueJob(cb2)
+ queueJob(cb1, true)
+ queueJob(cb2, true)
flushPreFlushCbs()
calls.push('job1')
}
@@ -227,11 +218,9 @@ describe('scheduler', () => {
// a cb triggers its parent job, which should be skipped
queueJob(job1)
}
- cb1.flags! |= SchedulerJobFlags.PRE
const cb2: SchedulerJob = () => {
calls.push('cb2')
}
- cb2.flags! |= SchedulerJobFlags.PRE
queueJob(job1)
await nextTick()
@@ -242,29 +231,25 @@ describe('scheduler', () => {
const calls: string[] = []
const job1: SchedulerJob = () => {
calls.push('job1')
- queueJob(job3)
- queueJob(job4)
+ queueJob(job3, true)
+ queueJob(job4, true)
}
// job1 has no id
- job1.flags! |= SchedulerJobFlags.PRE
const job2: SchedulerJob = () => {
calls.push('job2')
}
job2.id = 1
- job2.flags! |= SchedulerJobFlags.PRE
const job3: SchedulerJob = () => {
calls.push('job3')
}
// job3 has no id
- job3.flags! |= SchedulerJobFlags.PRE
const job4: SchedulerJob = () => {
calls.push('job4')
}
// job4 has no id
- job4.flags! |= SchedulerJobFlags.PRE
- queueJob(job1)
- queueJob(job2)
+ queueJob(job1, true)
+ queueJob(job2, true)
await nextTick()
expect(calls).toEqual(['job1', 'job3', 'job4', 'job2'])
})
@@ -273,9 +258,8 @@ describe('scheduler', () => {
it('queue preFlushCb inside postFlushCb', async () => {
const spy = vi.fn()
const cb: SchedulerJob = () => spy()
- cb.flags! |= SchedulerJobFlags.PRE
queuePostFlushCb(() => {
- queueJob(cb)
+ queueJob(cb, true)
})
await nextTick()
expect(spy).toHaveBeenCalled()
@@ -476,16 +460,14 @@ describe('scheduler', () => {
job3.id = 1
const job4: SchedulerJob = () => calls.push('job4')
job4.id = 2
- job4.flags! |= SchedulerJobFlags.PRE
const job5: SchedulerJob = () => calls.push('job5')
// job5 has no id
- job5.flags! |= SchedulerJobFlags.PRE
queueJob(job1)
queueJob(job2)
queueJob(job3)
- queueJob(job4)
- queueJob(job5)
+ queueJob(job4, true)
+ queueJob(job5, true)
await nextTick()
expect(calls).toEqual(['job5', 'job3', 'job4', 'job2', 'job1'])
})
@@ -685,40 +667,38 @@ describe('scheduler', () => {
let recurse = true
const job1: SchedulerJob = vi.fn(() => {
- queueJob(job3)
- queueJob(job3)
+ queueJob(job3, true)
+ queueJob(job3, true)
flushPreFlushCbs()
})
job1.id = 1
- job1.flags = SchedulerJobFlags.PRE
const job2: SchedulerJob = vi.fn(() => {
if (recurse) {
// job2 does not allow recurse, so this shouldn't do anything
- queueJob(job2)
+ queueJob(job2, true)
// job3 is already queued, so this shouldn't do anything
- queueJob(job3)
+ queueJob(job3, true)
recurse = false
}
})
job2.id = 2
- job2.flags = SchedulerJobFlags.PRE
const job3: SchedulerJob = vi.fn(() => {
if (recurse) {
- queueJob(job2)
- queueJob(job3)
+ queueJob(job2, true)
+ queueJob(job3, true)
// The jobs are already queued, so these should have no effect
- queueJob(job2)
- queueJob(job3)
+ queueJob(job2, true)
+ queueJob(job3, true)
}
})
job3.id = 3
- job3.flags = SchedulerJobFlags.ALLOW_RECURSE | SchedulerJobFlags.PRE
+ job3.flags = SchedulerJobFlags.ALLOW_RECURSE
- queueJob(job1)
+ queueJob(job1, true)
await nextTick()
@@ -775,8 +755,7 @@ describe('scheduler', () => {
spy()
flushPreFlushCbs()
}
- job.flags! |= SchedulerJobFlags.PRE
- queueJob(job)
+ queueJob(job, true)
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
})
@@ -789,17 +768,15 @@ describe('scheduler', () => {
calls.push('job1')
}
job1.id = 1
- job1.flags! |= SchedulerJobFlags.PRE
const job2: SchedulerJob = () => {
calls.push('job2')
}
job2.id = 2
- job2.flags! |= SchedulerJobFlags.PRE
queuePostFlushCb(() => {
- queueJob(job2)
- queueJob(job1)
+ queueJob(job2, true)
+ queueJob(job1, true)
// e.g. nested app.mount() call
flushPreFlushCbs()
diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts
index 07e7fc67fef..2769c2b3b78 100644
--- a/packages/runtime-core/src/apiAsyncComponent.ts
+++ b/packages/runtime-core/src/apiAsyncComponent.ts
@@ -214,7 +214,7 @@ export function defineAsyncComponent<
) {
// parent is keep-alive, force update so the loaded component's
// name is taken into account
- ;(instance.parent as ComponentInternalInstance).update()
+ ;(instance.parent as ComponentInternalInstance).effect.run()
}
})
.catch(err => {
diff --git a/packages/runtime-core/src/apiLifecycle.ts b/packages/runtime-core/src/apiLifecycle.ts
index 93af3a2b01c..09efe090a1b 100644
--- a/packages/runtime-core/src/apiLifecycle.ts
+++ b/packages/runtime-core/src/apiLifecycle.ts
@@ -37,11 +37,11 @@ export function injectHook(
// Set currentInstance during hook invocation.
// This assumes the hook does not synchronously trigger other hooks, which
// can only be false when the user does something really funky.
- const reset = setCurrentInstance(target)
+ const prev = setCurrentInstance(target)
try {
return callWithAsyncErrorHandling(hook, target, type, args)
} finally {
- reset()
+ setCurrentInstance(...prev)
resetTracking()
}
})
diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts
index 6a5532ad555..45b1d28f807 100644
--- a/packages/runtime-core/src/apiSetupHelpers.ts
+++ b/packages/runtime-core/src/apiSetupHelpers.ts
@@ -14,7 +14,6 @@ import {
createSetupContext,
getCurrentGenericInstance,
setCurrentInstance,
- unsetCurrentInstance,
} from './component'
import type { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
import type {
@@ -511,7 +510,7 @@ export function withAsyncContext(getAwaitable: () => any): [any, () => void] {
)
}
let awaitable = getAwaitable()
- unsetCurrentInstance()
+ setCurrentInstance(null, undefined)
if (isPromise(awaitable)) {
awaitable = awaitable.catch(e => {
setCurrentInstance(ctx)
diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts
index 8f6168cdf29..ec2a5a2482c 100644
--- a/packages/runtime-core/src/apiWatch.ts
+++ b/packages/runtime-core/src/apiWatch.ts
@@ -203,7 +203,7 @@ function doWatch(
if (isFirstRun) {
job()
} else {
- queueJob(job)
+ queueJob(job, true)
}
}
}
@@ -214,12 +214,9 @@ function doWatch(
if (cb) {
job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
}
- if (isPre) {
- job.flags! |= SchedulerJobFlags.PRE
- if (instance) {
- job.id = instance.uid
- ;(job as SchedulerJob).i = instance
- }
+ if (isPre && instance) {
+ job.id = instance.uid
+ ;(job as SchedulerJob).i = instance
}
}
@@ -256,9 +253,9 @@ export function instanceWatch(
cb = value.handler as Function
options = value
}
- const reset = setCurrentInstance(this)
+ const prev = setCurrentInstance(this)
const res = doWatch(getter, cb.bind(publicThis), options)
- reset()
+ setCurrentInstance(...prev)
return res
}
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index f6ff8803c87..43c0877bd2b 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -97,7 +97,6 @@ import type { RendererElement } from './renderer'
import {
setCurrentInstance,
setInSSRSetupState,
- unsetCurrentInstance,
} from './componentCurrentInstance'
export * from './componentCurrentInstance'
@@ -554,10 +553,6 @@ export interface ComponentInternalInstance extends GenericComponentInstance {
* Render effect instance
*/
effect: ReactiveEffect
- /**
- * Force update render effect
- */
- update: () => void
/**
* Render effect job to be passed to scheduler (checks if dirty)
*/
@@ -718,7 +713,6 @@ export function createComponentInstance(
next: null,
subTree: null!, // will be set synchronously right after creation
effect: null!,
- update: null!, // will be set synchronously right after creation
job: null!,
scope: new EffectScope(true /* detached */),
render: null,
@@ -891,7 +885,7 @@ function setupStatefulComponent(
pauseTracking()
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
- const reset = setCurrentInstance(instance)
+ const prev = setCurrentInstance(instance)
const setupResult = callWithErrorHandling(
setup,
instance,
@@ -903,7 +897,7 @@ function setupStatefulComponent(
)
const isAsyncSetup = isPromise(setupResult)
resetTracking()
- reset()
+ setCurrentInstance(...prev)
if ((isAsyncSetup || instance.sp) && !isAsyncWrapper(instance)) {
// async setup / serverPrefetch, mark as async boundary for useId()
@@ -911,6 +905,9 @@ function setupStatefulComponent(
}
if (isAsyncSetup) {
+ const unsetCurrentInstance = (): void => {
+ setCurrentInstance(null, undefined)
+ }
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
@@ -1083,13 +1080,13 @@ export function finishComponentSetup(
// support for 2.x options
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
- const reset = setCurrentInstance(instance)
+ const prev = setCurrentInstance(instance)
pauseTracking()
try {
applyOptions(instance)
} finally {
resetTracking()
- reset()
+ setCurrentInstance(...prev)
}
}
diff --git a/packages/runtime-core/src/componentCurrentInstance.ts b/packages/runtime-core/src/componentCurrentInstance.ts
index c091b9c693d..bace000486c 100644
--- a/packages/runtime-core/src/componentCurrentInstance.ts
+++ b/packages/runtime-core/src/componentCurrentInstance.ts
@@ -4,6 +4,7 @@ import type {
GenericComponentInstance,
} from './component'
import { currentRenderingInstance } from './componentRenderContext'
+import { type EffectScope, setCurrentScope } from '@vue/reactivity'
/**
* @internal
@@ -25,7 +26,10 @@ export let isInSSRComponentSetup = false
export let setInSSRSetupState: (state: boolean) => void
-let internalSetCurrentInstance: (
+/**
+ * @internal
+ */
+export let simpleSetCurrentInstance: (
instance: GenericComponentInstance | null,
) => void
@@ -53,7 +57,7 @@ if (__SSR__) {
else setters[0](v)
}
}
- internalSetCurrentInstance = registerGlobalSetter(
+ simpleSetCurrentInstance = registerGlobalSetter(
`__VUE_INSTANCE_SETTERS__`,
v => (currentInstance = v),
)
@@ -66,7 +70,7 @@ if (__SSR__) {
v => (isInSSRComponentSetup = v),
)
} else {
- internalSetCurrentInstance = i => {
+ simpleSetCurrentInstance = i => {
currentInstance = i
}
setInSSRSetupState = v => {
@@ -74,34 +78,14 @@ if (__SSR__) {
}
}
-export const setCurrentInstance = (instance: GenericComponentInstance) => {
+export const setCurrentInstance = (
+ instance: GenericComponentInstance | null,
+ scope: EffectScope | undefined = instance !== null
+ ? instance.scope
+ : undefined,
+): [GenericComponentInstance | null, EffectScope | undefined] => {
const prev = currentInstance
- internalSetCurrentInstance(instance)
- instance.scope.on()
- return (): void => {
- instance.scope.off()
- internalSetCurrentInstance(prev)
- }
-}
-
-export const unsetCurrentInstance = (): void => {
- currentInstance && currentInstance.scope.off()
- internalSetCurrentInstance(null)
-}
-
-/**
- * Exposed for vapor only. Vapor never runs during SSR so we don't want to pay
- * for the extra overhead
- * @internal
- */
-export const simpleSetCurrentInstance = (
- i: GenericComponentInstance | null,
- unset?: GenericComponentInstance | null,
-): void => {
- currentInstance = i
- if (unset) {
- unset.scope.off()
- } else if (i) {
- i.scope.on()
- }
+ simpleSetCurrentInstance(instance)
+ const prevScope = setCurrentScope(scope)
+ return [prev, prevScope]
}
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
index d0fe97ff03d..bdbb66ccd0e 100644
--- a/packages/runtime-core/src/componentProps.ts
+++ b/packages/runtime-core/src/componentProps.ts
@@ -522,7 +522,7 @@ function baseResolveDefault(
key: string,
) {
let value
- const reset = setCurrentInstance(instance)
+ const prev = setCurrentInstance(instance)
const props = toRaw(instance.props)
value = factory.call(
__COMPAT__ && isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
@@ -530,7 +530,7 @@ function baseResolveDefault(
: null,
props,
)
- reset()
+ setCurrentInstance(...prev)
return value
}
diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts
index a43c99e2f45..3605e27c4f1 100644
--- a/packages/runtime-core/src/componentPublicInstance.ts
+++ b/packages/runtime-core/src/componentPublicInstance.ts
@@ -383,7 +383,7 @@ export const publicPropertiesMap: PublicPropertiesMap =
$forceUpdate: i =>
i.f ||
(i.f = () => {
- queueJob(i.update)
+ queueJob(() => i.effect.run())
}),
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP),
diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts
index 2b58bc3fc43..71977df20d1 100644
--- a/packages/runtime-core/src/components/BaseTransition.ts
+++ b/packages/runtime-core/src/components/BaseTransition.ts
@@ -224,7 +224,7 @@ const BaseTransitionImpl: ComponentOptions = {
// #6835
// it also needs to be updated when active is undefined
if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
- instance.update()
+ instance.effect.run()
}
delete leavingHooks.afterLeave
oldInnerChild = undefined
diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts
index ed5d8b081a0..6483e22416f 100644
--- a/packages/runtime-core/src/hmr.ts
+++ b/packages/runtime-core/src/hmr.ts
@@ -100,7 +100,7 @@ function rerender(id: string, newRender?: Function): void {
} else {
const i = instance as ComponentInternalInstance
i.renderCache = []
- i.update()
+ i.effect.run()
}
nextTick(() => {
isHmrUpdating = false
@@ -160,7 +160,7 @@ function reload(id: string, newComp: HMRComponent): void {
if (parent.vapor) {
parent.hmrRerender!()
} else {
- ;(parent as ComponentInternalInstance).update()
+ ;(parent as ComponentInternalInstance).effect.run()
}
nextTick(() => {
isHmrUpdating = false
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index e309554f2f6..617b4d4d8d8 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -543,6 +543,7 @@ export {
*/
export {
currentInstance,
+ setCurrentInstance,
simpleSetCurrentInstance,
} from './componentCurrentInstance'
/**
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 5a18d62a8e1..48f915402e5 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -59,6 +59,7 @@ import {
ReactiveEffect,
pauseTracking,
resetTracking,
+ setCurrentScope,
} from '@vue/reactivity'
import { updateProps } from './componentProps'
import { updateSlots } from './componentSlots'
@@ -1303,7 +1304,7 @@ function baseCreateRenderer(
// normal update
instance.next = n2
// instance.update is the reactive effect.
- instance.update()
+ instance.effect.run()
}
} else {
// no update needed. just copy over properties
@@ -1594,13 +1595,11 @@ function baseCreateRenderer(
}
// create reactive effect for rendering
- instance.scope.on()
+ const prevScope = setCurrentScope(instance.scope)
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
- instance.scope.off()
+ setCurrentScope(prevScope)
- const update = (instance.update = effect.run.bind(effect))
- const job: SchedulerJob = (instance.job = () =>
- effect.dirty && effect.run())
+ const job: SchedulerJob = (instance.job = effect.scheduler.bind(effect))
job.i = instance
job.id = instance.uid
effect.scheduler = () => queueJob(job)
@@ -1618,7 +1617,7 @@ function baseCreateRenderer(
: void 0
}
- update()
+ effect.run()
}
const updateComponentPreRender = (
diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts
index a75eba300f7..2b5b8e98ed8 100644
--- a/packages/runtime-core/src/scheduler.ts
+++ b/packages/runtime-core/src/scheduler.ts
@@ -1,10 +1,9 @@
import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
-import { NOOP, isArray } from '@vue/shared'
+import { isArray } from '@vue/shared'
import { type GenericComponentInstance, getComponentName } from './component'
export enum SchedulerJobFlags {
QUEUED = 1 << 0,
- PRE = 1 << 1,
/**
* Indicates whether the effect is allowed to recursively trigger itself
* when managed by the scheduler.
@@ -20,8 +19,8 @@ export enum SchedulerJobFlags {
* responsibility to perform recursive state mutation that eventually
* stabilizes (#1727).
*/
- ALLOW_RECURSE = 1 << 2,
- DISPOSED = 1 << 3,
+ ALLOW_RECURSE = 1 << 1,
+ DISPOSED = 1 << 2,
}
export interface SchedulerJob extends Function {
@@ -40,15 +39,21 @@ export interface SchedulerJob extends Function {
export type SchedulerJobs = SchedulerJob | SchedulerJob[]
-const queue: SchedulerJob[] = []
-let flushIndex = -1
+const queueMainJobs: (SchedulerJob | undefined)[] = []
+const queuePreJobs: (SchedulerJob | undefined)[] = []
+const queuePostJobs: (SchedulerJob | undefined)[] = []
-const pendingPostFlushCbs: SchedulerJob[] = []
-let activePostFlushCbs: SchedulerJob[] | null = null
+let mainFlushIndex = -1
+let preFlushIndex = -1
let postFlushIndex = 0
+let mainJobsLength = 0
+let preJobsLength = 0
+let postJobsLength = 0
+let flushingPreJob = false
+let activePostFlushCbs: SchedulerJob[] | null = null
+let currentFlushPromise: Promise | null = null
const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise
-let currentFlushPromise: Promise | null = null
const RECURSION_LIMIT = 100
type CountMap = Map
@@ -70,18 +75,15 @@ export function nextTick(
// A pre watcher will have the same id as its component's update job. The
// watcher should be inserted immediately before the update job. This allows
// watchers to be skipped if the component is unmounted by the parent update.
-function findInsertionIndex(id: number) {
- let start = flushIndex + 1
- let end = queue.length
+function findInsertionIndex(id: number, isPre: boolean) {
+ let start = (isPre ? preFlushIndex : mainFlushIndex) + 1
+ let end = isPre ? preJobsLength : mainJobsLength
+ const queue = isPre ? queuePreJobs : queueMainJobs
while (start < end) {
const middle = (start + end) >>> 1
- const middleJob = queue[middle]
- const middleJobId = getId(middleJob)
- if (
- middleJobId < id ||
- (middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
- ) {
+ const middleJob = queue[middle]!
+ if (middleJob.id! <= id) {
start = middle + 1
} else {
end = middle
@@ -94,22 +96,25 @@ function findInsertionIndex(id: number) {
/**
* @internal for runtime-vapor only
*/
-export function queueJob(job: SchedulerJob): void {
- if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
- const jobId = getId(job)
- const lastJob = queue[queue.length - 1]
+export function queueJob(job: SchedulerJob, isPre = false): void {
+ const flags = job.flags!
+ if (!(flags & SchedulerJobFlags.QUEUED)) {
+ if (job.id === undefined) {
+ job.id = isPre ? -1 : Infinity
+ }
+ const queueLength = isPre ? preJobsLength : mainJobsLength
+ const queue = isPre ? queuePreJobs : queueMainJobs
if (
- !lastJob ||
+ !queueLength ||
// fast path when the job id is larger than the tail
- (!(job.flags! & SchedulerJobFlags.PRE) && jobId >= getId(lastJob))
+ job.id >= queue[queueLength - 1]!.id!
) {
- queue.push(job)
+ queue[queueLength] = job
} else {
- queue.splice(findInsertionIndex(jobId), 0, job)
+ queue.splice(findInsertionIndex(job.id, isPre), 0, job)
}
-
- job.flags! |= SchedulerJobFlags.QUEUED
-
+ isPre ? preJobsLength++ : mainJobsLength++
+ job.flags! = flags | SchedulerJobFlags.QUEUED
queueFlush()
}
}
@@ -125,17 +130,25 @@ function queueFlush() {
export function queuePostFlushCb(cb: SchedulerJobs): void {
if (!isArray(cb)) {
+ if (cb.id === undefined) {
+ cb.id = Infinity
+ }
if (activePostFlushCbs && cb.id === -1) {
activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
} else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
- pendingPostFlushCbs.push(cb)
+ queuePostJobs[postJobsLength++] = cb
cb.flags! |= SchedulerJobFlags.QUEUED
}
} else {
// if cb is an array, it is a component lifecycle hook which can only be
// triggered by a job, which is already deduped in the main queue, so
// we can skip duplicate check here to improve perf
- pendingPostFlushCbs.push(...cb)
+ for (const job of cb) {
+ if (job.id === undefined) {
+ job.id = Infinity
+ }
+ queuePostJobs[postJobsLength++] = job
+ }
}
queueFlush()
}
@@ -143,22 +156,25 @@ export function queuePostFlushCb(cb: SchedulerJobs): void {
export function flushPreFlushCbs(
instance?: GenericComponentInstance,
seen?: CountMap,
- // skip the current job
- i: number = flushIndex + 1,
): void {
if (__DEV__) {
seen = seen || new Map()
}
- for (; i < queue.length; i++) {
- const cb = queue[i]
- if (cb && cb.flags! & SchedulerJobFlags.PRE) {
+ for (
+ let i = flushingPreJob ? preFlushIndex + 1 : preFlushIndex;
+ i < preJobsLength;
+ i++
+ ) {
+ const cb = queuePreJobs[i]
+ if (cb) {
if (instance && cb.id !== instance.uid) {
continue
}
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}
- queue.splice(i, 1)
+ queuePreJobs.splice(i, 1)
+ preJobsLength--
i--
if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
cb.flags! &= ~SchedulerJobFlags.QUEUED
@@ -172,19 +188,24 @@ export function flushPreFlushCbs(
}
export function flushPostFlushCbs(seen?: CountMap): void {
- if (pendingPostFlushCbs.length) {
- const deduped = [...new Set(pendingPostFlushCbs)].sort(
- (a, b) => getId(a) - getId(b),
- )
- pendingPostFlushCbs.length = 0
+ if (postJobsLength) {
+ const deduped = new Set()
+ for (let i = 0; i < postJobsLength; i++) {
+ const job = queuePostJobs[i]!
+ queuePostJobs[i] = undefined
+ deduped.add(job)
+ }
+ postJobsLength = 0
+
+ const sorted = [...deduped].sort((a, b) => a.id! - b.id!)
// #1947 already has active queue, nested flushPostFlushCbs call
if (activePostFlushCbs) {
- activePostFlushCbs.push(...deduped)
+ activePostFlushCbs.push(...sorted)
return
}
- activePostFlushCbs = deduped
+ activePostFlushCbs = sorted
if (__DEV__) {
seen = seen || new Map()
}
@@ -227,28 +248,49 @@ export function flushOnAppMount(): void {
}
}
-const getId = (job: SchedulerJob): number =>
- job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
-
function flushJobs(seen?: CountMap) {
if (__DEV__) {
seen = seen || new Map()
}
- // conditional usage of checkRecursiveUpdate must be determined out of
- // try ... catch block since Rollup by default de-optimizes treeshaking
- // inside try-catch. This can leave all warning code unshaked. Although
- // they would get eventually shaken by a minifier like terser, some minifiers
- // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
- const check = __DEV__
- ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
- : NOOP
-
try {
- for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
- const job = queue[flushIndex]
- if (job && !(job.flags! & SchedulerJobFlags.DISPOSED)) {
- if (__DEV__ && check(job)) {
+ preFlushIndex = 0
+ mainFlushIndex = 0
+
+ while (preFlushIndex < preJobsLength || mainFlushIndex < mainJobsLength) {
+ let job: SchedulerJob
+ if (preFlushIndex < preJobsLength) {
+ if (mainFlushIndex < mainJobsLength) {
+ const preJob = queuePreJobs[preFlushIndex]!
+ const mainJob = queueMainJobs[mainFlushIndex]!
+ if (preJob.id! <= mainJob.id!) {
+ job = preJob
+ flushingPreJob = true
+ } else {
+ job = mainJob
+ flushingPreJob = false
+ }
+ } else {
+ job = queuePreJobs[preFlushIndex]!
+ flushingPreJob = true
+ }
+ } else {
+ job = queueMainJobs[mainFlushIndex]!
+ flushingPreJob = false
+ }
+
+ if (!(job.flags! & SchedulerJobFlags.DISPOSED)) {
+ // conditional usage of checkRecursiveUpdate must be determined out of
+ // try ... catch block since Rollup by default de-optimizes treeshaking
+ // inside try-catch. This can leave all warning code unshaked. Although
+ // they would get eventually shaken by a minifier like terser, some minifiers
+ // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
+ if (__DEV__ && checkRecursiveUpdates(seen!, job)) {
+ if (flushingPreJob) {
+ queuePreJobs[preFlushIndex++] = undefined
+ } else {
+ queueMainJobs[mainFlushIndex++] = undefined
+ }
continue
}
if (job.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
@@ -263,24 +305,41 @@ function flushJobs(seen?: CountMap) {
job.flags! &= ~SchedulerJobFlags.QUEUED
}
}
+
+ if (flushingPreJob) {
+ queuePreJobs[preFlushIndex++] = undefined
+ } else {
+ queueMainJobs[mainFlushIndex++] = undefined
+ }
}
} finally {
// If there was an error we still need to clear the QUEUED flags
- for (; flushIndex < queue.length; flushIndex++) {
- const job = queue[flushIndex]
+ while (preFlushIndex < preJobsLength) {
+ const job = queuePreJobs[preFlushIndex]
+ queuePreJobs[preFlushIndex++] = undefined
+ if (job) {
+ job.flags! &= ~SchedulerJobFlags.QUEUED
+ }
+ }
+ while (mainFlushIndex < mainJobsLength) {
+ const job = queueMainJobs[mainFlushIndex]
+ queueMainJobs[mainFlushIndex++] = undefined
if (job) {
job.flags! &= ~SchedulerJobFlags.QUEUED
}
}
- flushIndex = -1
- queue.length = 0
+ preFlushIndex = -1
+ mainFlushIndex = -1
+ preJobsLength = 0
+ mainJobsLength = 0
+ flushingPreJob = false
flushPostFlushCbs(seen)
currentFlushPromise = null
// If new jobs have been added to either queue, keep flushing
- if (queue.length || pendingPostFlushCbs.length) {
+ if (preJobsLength || mainJobsLength || postJobsLength) {
flushJobs(seen)
}
}
diff --git a/packages/runtime-vapor/__tests__/apiWatch.spec.ts b/packages/runtime-vapor/__tests__/apiWatch.spec.ts
index 068791b8ad2..290c509552c 100644
--- a/packages/runtime-vapor/__tests__/apiWatch.spec.ts
+++ b/packages/runtime-vapor/__tests__/apiWatch.spec.ts
@@ -1,4 +1,6 @@
import {
+ type EffectScope,
+ ReactiveEffect,
currentInstance,
effectScope,
nextTick,
@@ -298,7 +300,7 @@ describe('apiWatch', () => {
define(Comp).render()
// should not record watcher in detached scope
// the 1 is the props validation effect
- expect(instance!.scope.effects.length).toBe(1)
+ expect(getEffectsCount(instance!.scope)).toBe(1)
})
test('watchEffect should keep running if created in a detached scope', async () => {
@@ -336,3 +338,13 @@ describe('apiWatch', () => {
expect(countW).toBe(2)
})
})
+
+function getEffectsCount(scope: EffectScope): number {
+ let n = 0
+ for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+ if (dep.dep instanceof ReactiveEffect) {
+ n++
+ }
+ }
+ return n
+}
diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts
index 5fdff8eafe4..07699ba0fc4 100644
--- a/packages/runtime-vapor/__tests__/component.spec.ts
+++ b/packages/runtime-vapor/__tests__/component.spec.ts
@@ -1,4 +1,6 @@
import {
+ type EffectScope,
+ ReactiveEffect,
type Ref,
inject,
nextTick,
@@ -280,12 +282,12 @@ describe('component', () => {
const i = instance as VaporComponentInstance
// watchEffect + renderEffect + props validation effect
- expect(i.scope.effects.length).toBe(3)
+ expect(getEffectsCount(i.scope)).toBe(3)
expect(host.innerHTML).toBe('0
')
app.unmount()
expect(host.innerHTML).toBe('')
- expect(i.scope.effects.length).toBe(0)
+ expect(getEffectsCount(i.scope)).toBe(0)
})
test('should mount component only with template in production mode', () => {
@@ -328,3 +330,13 @@ describe('component', () => {
).toHaveBeenWarned()
})
})
+
+function getEffectsCount(scope: EffectScope): number {
+ let n = 0
+ for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+ if (dep.dep instanceof ReactiveEffect) {
+ n++
+ }
+ }
+ return n
+}
diff --git a/packages/runtime-vapor/__tests__/dom/prop.spec.ts b/packages/runtime-vapor/__tests__/dom/prop.spec.ts
index e879b7103e5..9d07b413541 100644
--- a/packages/runtime-vapor/__tests__/dom/prop.spec.ts
+++ b/packages/runtime-vapor/__tests__/dom/prop.spec.ts
@@ -12,20 +12,13 @@ import {
} from '../../src/dom/prop'
import { setStyle } from '../../src/dom/prop'
import { VaporComponentInstance } from '../../src/component'
-import {
- currentInstance,
- ref,
- simpleSetCurrentInstance,
-} from '@vue/runtime-dom'
+import { ref, setCurrentInstance } from '@vue/runtime-dom'
let removeComponentInstance = NOOP
beforeEach(() => {
const instance = new VaporComponentInstance({}, {}, null)
- const prev = currentInstance
- simpleSetCurrentInstance(instance)
- removeComponentInstance = () => {
- simpleSetCurrentInstance(prev)
- }
+ const prev = setCurrentInstance(instance)
+ removeComponentInstance = () => setCurrentInstance(...prev)
})
afterEach(() => {
removeComponentInstance()
diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts
index 7ba6023b1e9..02120002607 100644
--- a/packages/runtime-vapor/__tests__/for.spec.ts
+++ b/packages/runtime-vapor/__tests__/for.spec.ts
@@ -94,7 +94,7 @@ describe('createFor', () => {
})
return span
},
- item => item.name,
+ item => item,
)
return n1
}).render()
diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts
index 0cd8317532f..9df529f995e 100644
--- a/packages/runtime-vapor/src/apiCreateFor.ts
+++ b/packages/runtime-vapor/src/apiCreateFor.ts
@@ -8,8 +8,9 @@ import {
shallowReadArray,
shallowRef,
toReactive,
+ watch,
} from '@vue/reactivity'
-import { getSequence, isArray, isObject, isString } from '@vue/shared'
+import { isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode } from './dom/node'
import {
type Block,
@@ -78,12 +79,18 @@ export const createFor = (
let oldBlocks: ForBlock[] = []
let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null
+ // useSelector only
+ let currentKey: any
// TODO handle this in hydration
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
const frag = new VaporFragment(oldBlocks)
const instance = currentInstance!
- const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE
- const isComponent = flags & VaporVForFlags.IS_COMPONENT
+ const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
+ const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT)
+ const selectors: {
+ deregister: (key: any) => void
+ cleanup: () => void
+ }[] = []
if (__DEV__ && !instance) {
warn('createFor() can only be used inside setup()')
@@ -111,9 +118,12 @@ export const createFor = (
}
} else if (!newLength) {
// fast path for clearing all
+ for (const selector of selectors) {
+ selector.cleanup()
+ }
const doRemove = !canUseFastRemove
for (let i = 0; i < oldLength; i++) {
- unmount(oldBlocks[i], doRemove)
+ unmount(oldBlocks[i], doRemove, false)
}
if (canUseFastRemove) {
parent!.textContent = ''
@@ -132,150 +142,135 @@ export const createFor = (
unmount(oldBlocks[i])
}
} else {
- let i = 0
- let e1 = oldLength - 1 // prev ending index
- let e2 = newLength - 1 // next ending index
-
- // 1. sync from start
- // (a b) c
- // (a b) d e
- while (i <= e1 && i <= e2) {
- if (tryPatchIndex(source, i)) {
- i++
- } else {
- break
+ const commonLength = Math.min(oldLength, newLength)
+ const oldKeyToIndexMap = new Map()
+ const pendingNews: [
+ index: number,
+ item: ReturnType,
+ key: any,
+ ][] = []
+
+ let defaultAnchor: Node = parentAnchor
+ let right = 0
+ let left = 0
+
+ while (right < commonLength) {
+ const index = newLength - right - 1
+ const item = getItem(source, index)
+ const key = getKey.apply(null, item)
+ const block = oldBlocks[oldLength - right - 1]
+ if (block.key === key) {
+ update(block, ...item)
+ newBlocks[index] = block
+ right++
+ continue
+ }
+ if (right !== 0) {
+ defaultAnchor = normalizeAnchor(newBlocks[index + 1].nodes)
}
+ break
}
- // 2. sync from end
- // a (b c)
- // d e (b c)
- while (i <= e1 && i <= e2) {
- if (tryPatchIndex(source, i)) {
- e1--
- e2--
+ while (left < commonLength - right) {
+ const item = getItem(source, left)
+ const key = getKey.apply(null, item)
+ const oldBlock = oldBlocks[left]
+ const oldKey = oldBlock.key
+ if (oldKey === key) {
+ update((newBlocks[left] = oldBlock), item[0])
} else {
- break
+ pendingNews.push([left, item, key])
+ oldKeyToIndexMap.set(oldKey, left)
}
+ left++
}
- // 3. common sequence + mount
- // (a b)
- // (a b) c
- // i = 2, e1 = 1, e2 = 2
- // (a b)
- // c (a b)
- // i = 0, e1 = -1, e2 = 0
- if (i > e1) {
- if (i <= e2) {
- const nextPos = e2 + 1
- const anchor =
- nextPos < newLength
- ? normalizeAnchor(newBlocks[nextPos].nodes)
- : parentAnchor
- while (i <= e2) {
- mount(source, i, anchor)
- i++
- }
- }
+ for (let i = left; i < oldLength - right; i++) {
+ oldKeyToIndexMap.set(oldBlocks[i].key, i)
}
- // 4. common sequence + unmount
- // (a b) c
- // (a b)
- // i = 2, e1 = 2, e2 = 1
- // a (b c)
- // (b c)
- // i = 0, e1 = 0, e2 = -1
- else if (i > e2) {
- while (i <= e1) {
- unmount(oldBlocks[i])
- i++
- }
+ const prepareLength = Math.min(newLength - right, commonLength)
+ for (let i = left; i < prepareLength; i++) {
+ const item = getItem(source, i)
+ const key = getKey.apply(null, item)
+ pendingNews.push([i, item, key])
}
- // 5. unknown sequence
- // [i ... e1 + 1]: a b [c d e] f g
- // [i ... e2 + 1]: a b [e d c h] f g
- // i = 2, e1 = 4, e2 = 5
- else {
- const s1 = i // prev starting index
- const s2 = i // next starting index
-
- // 5.1 build key:index map for newChildren
- const keyToNewIndexMap = new Map()
- for (i = s2; i <= e2; i++) {
- keyToNewIndexMap.set(getKey(...getItem(source, i)), i)
+ const pendingMounts: [
+ index: number,
+ item: ReturnType,
+ key: any,
+ anchorIndex: number,
+ ][] = []
+ const moveOrMount = (
+ index: number,
+ item: ReturnType,
+ key: any,
+ anchorIndex: number,
+ ) => {
+ const oldIndex = oldKeyToIndexMap.get(key)
+ if (oldIndex !== undefined) {
+ const block = (newBlocks[index] = oldBlocks[oldIndex])
+ update(block, ...item)
+ insert(
+ block,
+ parent!,
+ anchorIndex === -1
+ ? defaultAnchor
+ : normalizeAnchor(newBlocks[anchorIndex].nodes),
+ )
+ oldKeyToIndexMap.delete(key)
+ } else {
+ pendingMounts.push([index, item, key, anchorIndex])
}
+ }
- // 5.2 loop through old children left to be patched and try to patch
- // matching nodes & remove nodes that are no longer present
- let j
- let patched = 0
- const toBePatched = e2 - s2 + 1
- let moved = false
- // used to track whether any node has moved
- let maxNewIndexSoFar = 0
- // works as Map
- // Note that oldIndex is offset by +1
- // and oldIndex = 0 is a special value indicating the new node has
- // no corresponding old node.
- // used for determining longest stable subsequence
- const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
-
- for (i = s1; i <= e1; i++) {
- const prevBlock = oldBlocks[i]
- if (patched >= toBePatched) {
- // all new children have been patched so this can only be a removal
- unmount(prevBlock)
- } else {
- const newIndex = keyToNewIndexMap.get(prevBlock.key)
- if (newIndex == null) {
- unmount(prevBlock)
- } else {
- newIndexToOldIndexMap[newIndex - s2] = i + 1
- if (newIndex >= maxNewIndexSoFar) {
- maxNewIndexSoFar = newIndex
- } else {
- moved = true
- }
- update(
- (newBlocks[newIndex] = prevBlock),
- ...getItem(source, newIndex),
- )
- patched++
- }
- }
- }
+ for (let i = pendingNews.length - 1; i >= 0; i--) {
+ const [index, item, key] = pendingNews[i]
+ moveOrMount(
+ index,
+ item,
+ key,
+ index < prepareLength - 1 ? index + 1 : -1,
+ )
+ }
+
+ for (let i = prepareLength; i < newLength - right; i++) {
+ const item = getItem(source, i)
+ const key = getKey.apply(null, item)
+ moveOrMount(i, item, key, -1)
+ }
- // 5.3 move and mount
- // generate longest stable subsequence only when nodes have moved
- const increasingNewIndexSequence = moved
- ? getSequence(newIndexToOldIndexMap)
- : []
- j = increasingNewIndexSequence.length - 1
- // looping backwards so that we can use last patched node as anchor
- for (i = toBePatched - 1; i >= 0; i--) {
- const nextIndex = s2 + i
- const anchor =
- nextIndex + 1 < newLength
- ? normalizeAnchor(newBlocks[nextIndex + 1].nodes)
- : parentAnchor
- if (newIndexToOldIndexMap[i] === 0) {
- // mount new
- mount(source, nextIndex, anchor)
- } else if (moved) {
- // move if:
- // There is no stable subsequence (e.g. a reverse)
- // OR current node is not among the stable sequence
- if (j < 0 || i !== increasingNewIndexSequence[j]) {
- insert(newBlocks[nextIndex].nodes, parent!, anchor)
- } else {
- j--
- }
- }
+ const shouldUseFastRemove = pendingMounts.length === newLength
+
+ for (const i of oldKeyToIndexMap.values()) {
+ unmount(
+ oldBlocks[i],
+ !(shouldUseFastRemove && canUseFastRemove),
+ !shouldUseFastRemove,
+ )
+ }
+ if (shouldUseFastRemove) {
+ for (const selector of selectors) {
+ selector.cleanup()
+ }
+ if (canUseFastRemove) {
+ parent!.textContent = ''
+ parent!.appendChild(parentAnchor)
}
}
+
+ for (const [index, item, key, anchorIndex] of pendingMounts) {
+ mount(
+ source,
+ index,
+ item,
+ key,
+ anchorIndex === -1
+ ? defaultAnchor
+ : normalizeAnchor(newBlocks[anchorIndex].nodes),
+ )
+ }
}
}
@@ -293,14 +288,16 @@ export const createFor = (
const mount = (
source: ResolvedSource,
idx: number,
+ [item, key, index] = getItem(source, idx),
+ key2 = getKey && getKey(item, key, index),
anchor: Node | undefined = parentAnchor,
): ForBlock => {
- const [item, key, index] = getItem(source, idx)
const itemRef = shallowRef(item)
// avoid creating refs if the render fn doesn't need it
const keyRef = needKey ? shallowRef(key) : undefined
const indexRef = needIndex ? shallowRef(index) : undefined
+ currentKey = key2
let nodes: Block
let scope: EffectScope | undefined
if (isComponent) {
@@ -319,7 +316,7 @@ export const createFor = (
itemRef,
keyRef,
indexRef,
- getKey && getKey(item, key, index),
+ key2,
))
if (parent) insert(block.nodes, parent, anchor)
@@ -327,15 +324,6 @@ export const createFor = (
return block
}
- const tryPatchIndex = (source: any, idx: number) => {
- const block = oldBlocks[idx]
- const [item, key, index] = getItem(source, idx)
- if (block.key === getKey!(item, key, index)) {
- update((newBlocks[idx] = block), item)
- return true
- }
- }
-
const update = (
{ itemRef, keyRef, indexRef }: ForBlock,
newItem: any,
@@ -353,9 +341,18 @@ export const createFor = (
}
}
- const unmount = ({ nodes, scope }: ForBlock, doRemove = true) => {
- scope && scope.stop()
- doRemove && removeBlock(nodes, parent!)
+ const unmount = (block: ForBlock, doRemove = true, doDeregister = true) => {
+ if (!isComponent) {
+ block.scope!.stop()
+ }
+ if (doRemove) {
+ removeBlock(block.nodes, parent!)
+ }
+ if (doDeregister) {
+ for (const selector of selectors) {
+ selector.deregister(block.key)
+ }
+ }
}
if (flags & VaporVForFlags.ONCE) {
@@ -368,7 +365,61 @@ export const createFor = (
insert(frag, _insertionParent, _insertionAnchor)
}
+ // @ts-expect-error
+ frag.useSelector = useSelector
+
return frag
+
+ function useSelector(
+ getActiveKey: () => any,
+ ): (key: any, cb: () => void) => void {
+ let operMap = new Map void)[]>()
+ let activeKey = getActiveKey()
+ let activeOpers: (() => void)[] | undefined
+
+ watch(getActiveKey, newValue => {
+ if (activeOpers !== undefined) {
+ for (const oper of activeOpers) {
+ oper()
+ }
+ }
+ activeOpers = operMap.get(newValue)
+ if (activeOpers !== undefined) {
+ for (const oper of activeOpers) {
+ oper()
+ }
+ }
+ })
+
+ selectors.push({ deregister, cleanup })
+ return register
+
+ function cleanup() {
+ operMap = new Map()
+ activeOpers = undefined
+ }
+
+ function register(oper: () => void) {
+ oper()
+ let opers = operMap.get(currentKey)
+ if (opers !== undefined) {
+ opers.push(oper)
+ } else {
+ opers = [oper]
+ operMap.set(currentKey, opers)
+ if (currentKey === activeKey) {
+ activeOpers = opers
+ }
+ }
+ }
+
+ function deregister(key: any) {
+ operMap.delete(key)
+ if (key === activeKey) {
+ activeOpers = undefined
+ }
+ }
+ }
}
export function createForSlots(
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index 548babebf8b..83113e356f6 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -20,7 +20,7 @@ import {
pushWarningContext,
queuePostFlushCb,
registerHMR,
- simpleSetCurrentInstance,
+ setCurrentInstance,
startMeasure,
unregisterHMR,
warn,
@@ -191,8 +191,7 @@ export function createComponent(
instance.emitsOptions = normalizeEmitsOptions(component)
}
- const prev = currentInstance
- simpleSetCurrentInstance(instance)
+ const prev = setCurrentInstance(instance)
pauseTracking()
if (__DEV__) {
@@ -260,7 +259,7 @@ export function createComponent(
}
resetTracking()
- simpleSetCurrentInstance(prev, instance)
+ setCurrentInstance(...prev)
if (__DEV__) {
popWarningContext()
diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts
index a5e9daad229..7aa11694172 100644
--- a/packages/runtime-vapor/src/componentProps.ts
+++ b/packages/runtime-vapor/src/componentProps.ts
@@ -12,12 +12,11 @@ import type { VaporComponent, VaporComponentInstance } from './component'
import {
type NormalizedPropsOptions,
baseNormalizePropsOptions,
- currentInstance,
isEmitListener,
popWarningContext,
pushWarningContext,
resolvePropValue,
- simpleSetCurrentInstance,
+ setCurrentInstance,
validateProps,
warn,
} from '@vue/runtime-dom'
@@ -257,10 +256,9 @@ function resolveDefault(
factory: (props: Record) => unknown,
instance: VaporComponentInstance,
) {
- const prev = currentInstance
- simpleSetCurrentInstance(instance)
+ const prev = setCurrentInstance(instance)
const res = factory.call(null, instance.props)
- simpleSetCurrentInstance(prev, instance)
+ setCurrentInstance(...prev)
return res
}
diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts
index 741f385861d..c96c1afa130 100644
--- a/packages/runtime-vapor/src/hmr.ts
+++ b/packages/runtime-vapor/src/hmr.ts
@@ -1,8 +1,7 @@
import {
- currentInstance,
popWarningContext,
pushWarningContext,
- simpleSetCurrentInstance,
+ setCurrentInstance,
} from '@vue/runtime-dom'
import { insert, normalizeBlock, remove } from './block'
import {
@@ -19,12 +18,11 @@ export function hmrRerender(instance: VaporComponentInstance): void {
const parent = normalized[0].parentNode!
const anchor = normalized[normalized.length - 1].nextSibling
remove(instance.block, parent)
- const prev = currentInstance
- simpleSetCurrentInstance(instance)
+ const prev = setCurrentInstance(instance)
pushWarningContext(instance)
devRender(instance)
popWarningContext()
- simpleSetCurrentInstance(prev, instance)
+ setCurrentInstance(...prev)
insert(instance.block, parent, anchor)
}
@@ -36,14 +34,13 @@ export function hmrReload(
const parent = normalized[0].parentNode!
const anchor = normalized[normalized.length - 1].nextSibling
unmountComponent(instance, parent)
- const prev = currentInstance
- simpleSetCurrentInstance(instance.parent)
+ const prev = setCurrentInstance(instance.parent)
const newInstance = createComponent(
newComp,
instance.rawProps,
instance.rawSlots,
instance.isSingleRoot,
)
- simpleSetCurrentInstance(prev, instance.parent)
+ setCurrentInstance(...prev)
mountComponent(newInstance, parent, anchor)
}
diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts
index a9fa9b33562..3cc2c7b6334 100644
--- a/packages/runtime-vapor/src/renderEffect.ts
+++ b/packages/runtime-vapor/src/renderEffect.ts
@@ -1,70 +1,102 @@
-import { ReactiveEffect, getCurrentScope } from '@vue/reactivity'
+import { EffectFlags, type EffectScope, ReactiveEffect } from '@vue/reactivity'
import {
type SchedulerJob,
currentInstance,
queueJob,
queuePostFlushCb,
- simpleSetCurrentInstance,
+ setCurrentInstance,
startMeasure,
warn,
} from '@vue/runtime-dom'
import { type VaporComponentInstance, isVaporComponent } from './component'
import { invokeArrayFns } from '@vue/shared'
-export function renderEffect(fn: () => void, noLifecycle = false): void {
- const instance = currentInstance as VaporComponentInstance | null
- const scope = getCurrentScope()
- if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) {
- warn('renderEffect called without active EffectScope or Vapor instance.')
- }
+class RenderEffect extends ReactiveEffect {
+ i: VaporComponentInstance | null
+ job: SchedulerJob
+
+ constructor(public render: () => void) {
+ super()
+ const instance = currentInstance as VaporComponentInstance | null
+ if (__DEV__ && !__TEST__ && !this.subs && !isVaporComponent(instance)) {
+ warn('renderEffect called without active EffectScope or Vapor instance.')
+ }
+
+ const job: SchedulerJob = super.scheduler.bind(this)
- // renderEffect is always called after user has registered all hooks
- const hasUpdateHooks = instance && (instance.bu || instance.u)
- const renderEffectFn = noLifecycle
- ? fn
- : () => {
- if (__DEV__ && instance) {
- startMeasure(instance, `renderEffect`)
- }
- const prev = currentInstance
- simpleSetCurrentInstance(instance)
- if (scope) scope.on()
- if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) {
- instance.isUpdating = true
- instance.bu && invokeArrayFns(instance.bu)
- fn()
- queuePostFlushCb(() => {
- instance.isUpdating = false
- instance.u && invokeArrayFns(instance.u)
- })
- } else {
- fn()
- }
- if (scope) scope.off()
- simpleSetCurrentInstance(prev, instance)
- if (__DEV__ && instance) {
- startMeasure(instance, `renderEffect`)
- }
+ if (instance) {
+ if (__DEV__) {
+ this.onTrack = instance.rtc
+ ? e => invokeArrayFns(instance.rtc!, e)
+ : void 0
+ this.onTrigger = instance.rtg
+ ? e => invokeArrayFns(instance.rtg!, e)
+ : void 0
}
+ job.i = instance
+ job.id = instance.uid
+ }
- const effect = new ReactiveEffect(renderEffectFn)
- const job: SchedulerJob = () => effect.dirty && effect.run()
+ this.job = job
+ this.i = instance
- if (instance) {
- if (__DEV__) {
- effect.onTrack = instance.rtc
- ? e => invokeArrayFns(instance.rtc!, e)
- : void 0
- effect.onTrigger = instance.rtg
- ? e => invokeArrayFns(instance.rtg!, e)
- : void 0
+ // TODO recurse handling
+ }
+
+ fn(): void {
+ const instance = this.i
+ const scope = this.subs ? (this.subs.sub as EffectScope) : undefined
+ // renderEffect is always called after user has registered all hooks
+ const hasUpdateHooks = instance && (instance.bu || instance.u)
+ if (__DEV__ && instance) {
+ startMeasure(instance, `renderEffect`)
+ }
+ const prev = setCurrentInstance(instance, scope)
+ if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) {
+ instance.isUpdating = true
+ instance.bu && invokeArrayFns(instance.bu)
+ this.render()
+ if (instance.u) {
+ queuePostFlushCb(() => {
+ instance.isUpdating = false
+ invokeArrayFns(instance.u!)
+ })
+ } else {
+ instance.isUpdating = false
+ }
+ } else {
+ this.render()
+ }
+ setCurrentInstance(...prev)
+ if (__DEV__ && instance) {
+ startMeasure(instance, `renderEffect`)
}
- job.i = instance
- job.id = instance.uid
}
- effect.scheduler = () => queueJob(job)
- effect.run()
+ notify(): void {
+ const flags = this.flags
+ if (!(flags & EffectFlags.PAUSED)) {
+ queueJob(this.job)
+ } else {
+ this.flags = flags | EffectFlags.NOTIFIED
+ }
+ }
+}
+
+class RenderEffect_NoLifecycle extends RenderEffect {
+ constructor(render: () => void) {
+ super(render)
+ }
- // TODO recurse handling
+ fn() {
+ this.render()
+ }
+}
+
+export function renderEffect(fn: () => void, noLifecycle = false): void {
+ if (noLifecycle) {
+ new RenderEffect_NoLifecycle(fn).run()
+ } else {
+ new RenderEffect(fn).run()
+ }
}
diff --git a/rollup.config.js b/rollup.config.js
index 7f2ecb8c864..3db827e0447 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -218,7 +218,8 @@ function createConfig(format, output, plugins = []) {
}
},
treeshake: {
- moduleSideEffects: false,
+ // should be true to awllow @babel/types side effects
+ moduleSideEffects: true,
},
}