From 4972e178af2afea195683e8a72d32072b709a8a8 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 2 Apr 2025 19:21:58 +0800 Subject: [PATCH 01/30] refactor(reactivity): associate effects and effect scopes based on doubly linked list (deps, subs) --- .../reactivity/__tests__/computed.spec.ts | 8 +- .../reactivity/__tests__/effectScope.spec.ts | 37 ++- packages/reactivity/src/computed.ts | 21 +- packages/reactivity/src/effect.ts | 37 ++- packages/reactivity/src/effectScope.ts | 114 +++----- packages/reactivity/src/system.ts | 245 ++++++++---------- packages/reactivity/src/watch.ts | 6 - .../runtime-core/__tests__/apiWatch.spec.ts | 28 +- .../runtime-vapor/__tests__/apiWatch.spec.ts | 13 +- .../runtime-vapor/__tests__/component.spec.ts | 15 +- 10 files changed, 254 insertions(+), 270 deletions(-) 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..fce49fdd03d 100644 --- a/packages/reactivity/__tests__/effectScope.spec.ts +++ b/packages/reactivity/__tests__/effectScope.spec.ts @@ -20,7 +20,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', () => { @@ -47,7 +47,7 @@ describe('reactivity/effect/scope', () => { expect(dummy).toBe(7) }) - expect(scope.effects.length).toBe(1) + expect(getEffectsCount(scope)).toBe(1) }) it('stop', () => { @@ -60,7 +60,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 +87,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(2) + expect(scope.deps?.nextDep?.dep).toBeInstanceOf(EffectScope) expect(dummy).toBe(0) counter.num = 7 @@ -117,7 +116,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 +141,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,7 +165,7 @@ describe('reactivity/effect/scope', () => { effect(() => (dummy = counter.num)) }) - expect(scope.effects.length).toBe(1) + expect(getEffectsCount(scope)).toBe(1) scope.stop() @@ -176,7 +175,7 @@ describe('reactivity/effect/scope', () => { expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned() - expect(scope.effects.length).toBe(0) + expect(getEffectsCount(scope)).toBe(0) counter.num = 7 expect(dummy).toBe(0) @@ -224,9 +223,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 () => { @@ -372,7 +371,17 @@ describe('reactivity/effect/scope', () => { expect(watcherCalls).toBe(3) expect(cleanupCalls).toBe(1) - expect(scope.effects.length).toBe(0) + expect(getEffectsCount(scope)).toBe(0) expect(scope.cleanups.length).toBe(0) }) }) + +function getEffectsCount(scope: EffectScope): number { + let n = 0 + for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) { + if ('notify' in dep.dep) { + n++ + } + } + return n +} diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 70670d81ec2..dcaa5b797a1 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,12 +93,13 @@ export class ComputedRefImpl implements Dependency, Subscriber { */ get _dirty(): boolean { const flags = this.flags - if ( - flags & SubscriberFlags.Dirty || - (flags & SubscriberFlags.PendingComputed && - updateDirtyFlag(this, this.flags)) - ) { - return true + if (flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) { + if (flags & SubscriberFlags.Dirty || checkDirty(this.deps!)) { + this.flags = flags | SubscriberFlags.Dirty + return true + } else { + this.flags = flags & ~SubscriberFlags.Pending + } } return false } @@ -110,7 +111,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 +135,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/effect.ts b/packages/reactivity/src/effect.ts index a77c4bf2b18..6b17804e64d 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -6,9 +6,11 @@ import { type Link, type Subscriber, SubscriberFlags, + checkDirty, endTracking, + link, startTracking, - updateDirtyFlag, + unlink, } from './system' import { warn } from './warning' @@ -56,7 +58,11 @@ export class ReactiveEffect implements ReactiveEffectOptions { // Subscriber deps: Link | undefined = undefined depsTail: Link | undefined = undefined - flags: number = SubscriberFlags.Effect + flags: number = 0 + + // Dependency + subs: Link | undefined = undefined + subsTail: Link | undefined = undefined /** * @internal @@ -69,7 +75,7 @@ export class ReactiveEffect implements ReactiveEffectOptions { constructor(public fn: () => T) { if (activeEffectScope && activeEffectScope.active) { - activeEffectScope.effects.push(this) + link(this, activeEffectScope) } } @@ -99,7 +105,7 @@ export class ReactiveEffect implements ReactiveEffectOptions { if (!(flags & EffectFlags.PAUSED)) { this.scheduler() } else { - this.flags |= EffectFlags.NOTIFIED + this.flags = flags | EffectFlags.NOTIFIED } } @@ -132,11 +138,12 @@ 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 && + flags & EffectFlags.ALLOW_RECURSE ) { - this.flags &= ~SubscriberFlags.Recursed + this.flags = flags & ~SubscriberFlags.Recursed this.notify() } } @@ -149,16 +156,22 @@ export class ReactiveEffect implements ReactiveEffectOptions { cleanupEffect(this) this.onStop && this.onStop() this.flags |= EffectFlags.STOP + + if (this.subs !== undefined) { + unlink(this.subs) + } } } get dirty(): boolean { const flags = this.flags - if ( - flags & SubscriberFlags.Dirty || - (flags & SubscriberFlags.PendingComputed && updateDirtyFlag(this, flags)) - ) { - return true + if (flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) { + if (flags & SubscriberFlags.Dirty || checkDirty(this.deps!)) { + this.flags = flags | SubscriberFlags.Dirty + return true + } else { + this.flags = flags & ~SubscriberFlags.Pending + } } return false } diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index 00fa403b02e..1c561ab6e48 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -1,57 +1,37 @@ -import { EffectFlags, type ReactiveEffect } from './effect' +import { EffectFlags } from './effect' import { + type Dependency, type Link, type Subscriber, - endTracking, - startTracking, + link, + 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 + // Dependency + subs: Link | undefined = undefined + subsTail: Link | undefined = undefined + /** * @internal track `on` calls, allow `on` call multiple times */ private _on = 0 - /** - * @internal - */ - effects: ReactiveEffect[] = [] /** * @internal */ cleanups: (() => void)[] = [] - /** - * 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) } } @@ -59,18 +39,17 @@ export class EffectScope implements Subscriber { return !(this.flags & EffectFlags.STOP) } + 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 ('notify' in dep) { + dep.pause() } } - for (i = 0, l = this.effects.length; i < l; i++) { - this.effects[i].pause() - } } } @@ -78,17 +57,15 @@ 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 ('notify' in dep) { + dep.resume() } } - for (i = 0, l = this.effects.length; i < l; i++) { - this.effects[i].resume() - } } } @@ -129,39 +106,32 @@ export class EffectScope implements Subscriber { } } - stop(fromParent?: boolean): void { + stop(): 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() + + let link = this.deps + while (link !== undefined) { + const next = link.nextDep + const dep = link.dep + if ('notify' in dep) { + dep.stop() + } + link = next + } + while (this.deps !== undefined) { + unlink(this.deps) } - this.effects.length = 0 + let i, l 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! - } + if (this.subs !== undefined) { + unlink(this.subs) } - this.parent = undefined } } } diff --git a/packages/reactivity/src/system.ts b/packages/reactivity/src/system.ts index b3699f727c8..468562832f5 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,20 @@ export interface Subscriber { } export interface Link { - dep: Dependency | Computed - sub: Subscriber | Computed | Effect + dep: Dependency | Computed | Effect + 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, + Propagated = Dirty | Pending, } interface OneWayLink { @@ -37,7 +37,7 @@ interface OneWayLink { linked: OneWayLink | undefined } -const notifyBuffer: (Effect | undefined)[] = [] +const notifyBuffer: (Effect | EffectScope | undefined)[] = [] let batchDepth = 0 let notifyIndex = 0 @@ -53,12 +53,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 +71,69 @@ 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): void { + const dep = link.dep + const sub = link.sub + 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) { + const depFlags = dep.flags + if (!(depFlags & SubscriberFlags.Dirty)) { + dep.flags = depFlags | SubscriberFlags.Dirty + } + while (dep.deps !== undefined) { + unlink(dep.deps) + } + } } export function propagate(current: Link): void { @@ -84,7 +146,7 @@ export function propagate(current: Link): void { const sub = current.sub const subFlags = sub.flags - let shouldNotify = false + let shouldNotify = 0 if ( !( @@ -95,35 +157,36 @@ export function propagate(current: Link): void { ) ) { sub.flags = subFlags | targetFlag - shouldNotify = true + shouldNotify = 1 } else if ( subFlags & SubscriberFlags.Recursed && !(subFlags & SubscriberFlags.Tracking) ) { sub.flags = (subFlags & ~SubscriberFlags.Recursed) | targetFlag - shouldNotify = true + shouldNotify = 1 } else if ( !(subFlags & SubscriberFlags.Propagated) && isValidLink(current, sub) ) { sub.flags = subFlags | SubscriberFlags.Recursed | targetFlag - shouldNotify = (sub as Dependency).subs !== undefined + shouldNotify = 2 } 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 (shouldNotify === 1 && '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 @@ -137,9 +200,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 +210,7 @@ export function propagate(current: Link): void { if (current !== undefined) { next = current.nextSub targetFlag = branchDepth - ? SubscriberFlags.PendingComputed + ? SubscriberFlags.Pending : SubscriberFlags.Dirty continue top } @@ -173,29 +234,15 @@ export function startTracking(sub: Subscriber): void { 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 + while (depsTail.nextDep !== undefined) { + unlink(depsTail.nextDep) } - } else if (sub.deps !== undefined) { - clearTracking(sub.deps) - sub.deps = undefined - } - sub.flags &= ~SubscriberFlags.Tracking -} - -export function updateDirtyFlag( - sub: Subscriber, - flags: SubscriberFlags, -): boolean { - if (checkDirty(sub.deps!)) { - sub.flags = flags | SubscriberFlags.Dirty - return true } else { - sub.flags = flags & ~SubscriberFlags.PendingComputed - return false + while (sub.deps !== undefined) { + unlink(sub.deps) + } } + sub.flags &= ~SubscriberFlags.Tracking } export function processComputedUpdate( @@ -210,7 +257,7 @@ export function processComputedUpdate( } } } else { - computed.flags = flags & ~SubscriberFlags.PendingComputed + computed.flags = flags & ~SubscriberFlags.Pending } } @@ -224,41 +271,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 +282,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 +292,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 +323,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 +347,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 +372,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..f1f8f26a23d 100644 --- a/packages/reactivity/src/watch.ts +++ b/packages/reactivity/src/watch.ts @@ -8,7 +8,6 @@ import { isObject, isPlainObject, isSet, - remove, } from '@vue/shared' import type { ComputedRef } from './computed' import { ReactiveFlags } from './constants' @@ -19,7 +18,6 @@ import { pauseTracking, resetTracking, } from './effect' -import { getCurrentScope } from './effectScope' import { isReactive, isShallow } from './reactive' import { type Ref, isRef } from './ref' import { warn } from './warning' @@ -209,12 +207,8 @@ export function watch( getter = () => traverse(baseGetter(), depth) } - const scope = getCurrentScope() const watchHandle: WatchHandle = () => { effect.stop() - if (scope && scope.active) { - remove(scope.effects, effect) - } } if (once && cb) { diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 39032a63699..8912524f4b2 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -25,6 +25,7 @@ import { } from '@vue/runtime-test' import { type DebuggerEvent, + type EffectScope, ITERATE_KEY, type Ref, type ShallowRef, @@ -1332,16 +1333,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 +1489,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 +1796,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 +1806,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 +2011,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 ('notify' in dep.dep) { + n++ + } + } + return n +} diff --git a/packages/runtime-vapor/__tests__/apiWatch.spec.ts b/packages/runtime-vapor/__tests__/apiWatch.spec.ts index 068791b8ad2..76fe78b7963 100644 --- a/packages/runtime-vapor/__tests__/apiWatch.spec.ts +++ b/packages/runtime-vapor/__tests__/apiWatch.spec.ts @@ -1,4 +1,5 @@ import { + type EffectScope, currentInstance, effectScope, nextTick, @@ -298,7 +299,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 +337,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 ('notify' in dep.dep) { + n++ + } + } + return n +} diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts index 5fdff8eafe4..374aaf6cf43 100644 --- a/packages/runtime-vapor/__tests__/component.spec.ts +++ b/packages/runtime-vapor/__tests__/component.spec.ts @@ -1,4 +1,5 @@ import { + type EffectScope, type Ref, inject, nextTick, @@ -280,12 +281,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 +329,13 @@ describe('component', () => { ).toHaveBeenWarned() }) }) + +function getEffectsCount(scope: EffectScope): number { + let n = 0 + for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) { + if ('notify' in dep.dep) { + n++ + } + } + return n +} From 1b0d8813cfc0871f765d8ecd97bec4b4523b6371 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 5 Apr 2025 23:43:29 +0800 Subject: [PATCH 02/30] refactor(runtime-core): decouple pre type scheduler job processing --- .../runtime-core/__tests__/scheduler.spec.ts | 85 +++----- packages/runtime-core/src/apiWatch.ts | 11 +- packages/runtime-core/src/scheduler.ts | 182 ++++++++++++------ 3 files changed, 156 insertions(+), 122 deletions(-) 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/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 8f6168cdf29..c7a43418ab2 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 } } diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index a75eba300f7..1f51eabc09c 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,19 +96,23 @@ function findInsertionIndex(id: number) { /** * @internal for runtime-vapor only */ -export function queueJob(job: SchedulerJob): void { +export function queueJob(job: SchedulerJob, isPre = false): void { if (!(job.flags! & SchedulerJobFlags.QUEUED)) { - const jobId = getId(job) - const lastJob = queue[queue.length - 1] + 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) } + isPre ? preJobsLength++ : mainJobsLength++ job.flags! |= SchedulerJobFlags.QUEUED @@ -125,17 +131,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 +157,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 +189,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 +249,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 +306,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) } } From 7a907d5dc0c5380eda4b6eea4f22bcffc24cf6ec Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sun, 6 Apr 2025 10:45:11 +0800 Subject: [PATCH 03/30] refactor(runtime-vapor): rewrite renderEffect as a class --- packages/reactivity/__tests__/effect.spec.ts | 4 +- packages/reactivity/src/effect.ts | 14 +- packages/runtime-vapor/src/renderEffect.ts | 136 ++++++++++++------- 3 files changed, 99 insertions(+), 55 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 20f0244a7bc..3b8c81cb72b 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -624,7 +624,7 @@ describe('reactivity/effect', () => { const runner = effect(() => {}) const otherRunner = effect(runner) expect(runner).not.toBe(otherRunner) - expect(runner.effect.fn).toBe(otherRunner.effect.fn) + expect(runner.effect.callback).toBe(otherRunner.effect.callback) }) it('should wrap if the passed function is a fake effect', () => { @@ -632,7 +632,7 @@ describe('reactivity/effect', () => { fakeRunner.effect = {} const runner = effect(fakeRunner) expect(fakeRunner).not.toBe(runner) - expect(runner.effect.fn).toBe(fakeRunner) + expect(runner.effect.callback).toBe(fakeRunner) }) it('should not run multiple times for a single mutation', () => { diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 6b17804e64d..ef87177c0c3 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -73,7 +73,13 @@ export class ReactiveEffect implements ReactiveEffectOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void - constructor(public fn: () => T) { + // @ts-expect-error + callback(): T {} + + constructor(fn?: () => T) { + if (fn !== undefined) { + this.callback = fn + } if (activeEffectScope && activeEffectScope.active) { link(this, activeEffectScope) } @@ -120,7 +126,7 @@ export class ReactiveEffect implements ReactiveEffectOptions { if (!this.active) { // stopped during cleanup - return this.fn() + return this.callback() } cleanupEffect(this) const prevSub = activeSub @@ -128,7 +134,7 @@ export class ReactiveEffect implements ReactiveEffectOptions { startTracking(this) try { - return this.fn() + return this.callback() } finally { if (__DEV__ && activeSub !== this) { warn( @@ -191,7 +197,7 @@ export function effect( options?: ReactiveEffectOptions, ): ReactiveEffectRunner { if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) { - fn = (fn as ReactiveEffectRunner).effect.fn + fn = (fn as ReactiveEffectRunner).effect.callback } const e = new ReactiveEffect(fn) diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index a9fa9b33562..54417dc58ff 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -1,4 +1,8 @@ -import { ReactiveEffect, getCurrentScope } from '@vue/reactivity' +import { + type EffectScope, + ReactiveEffect, + getCurrentScope, +} from '@vue/reactivity' import { type SchedulerJob, currentInstance, @@ -11,60 +15,94 @@ import { 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 + scope: EffectScope | undefined + baseJob: SchedulerJob + postJob: SchedulerJob + + constructor(public render: () => void) { + super() + const instance = currentInstance as VaporComponentInstance | null + const scope = getCurrentScope() + if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) { + warn('renderEffect called without active EffectScope or Vapor instance.') + } + + this.baseJob = () => { + if (this.dirty) { + this.run() + } + } + this.postJob = () => { + instance!.isUpdating = false + instance!.u && invokeArrayFns(instance!.u) + } - // 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 } + this.baseJob.i = instance + this.baseJob.id = instance.uid + } + + this.i = instance + this.scope = scope - const effect = new ReactiveEffect(renderEffectFn) - const job: SchedulerJob = () => effect.dirty && effect.run() + // TODO recurse handling + } - 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 + callback() { + const instance = this.i! + const scope = this.scope + // renderEffect is always called after user has registered all hooks + const hasUpdateHooks = instance && (instance.bu || instance.u) + if (__DEV__ && instance) { + startMeasure(instance, `renderEffect`) } - job.i = instance - job.id = instance.uid + const prev = currentInstance + simpleSetCurrentInstance(instance) + if (scope) scope.on() + if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) { + instance.isUpdating = true + instance.bu && invokeArrayFns(instance.bu) + this.render() + queuePostFlushCb(this.postJob) + } else { + this.render() + } + if (scope) scope.off() + simpleSetCurrentInstance(prev, instance) + if (__DEV__ && instance) { + startMeasure(instance, `renderEffect`) + } + } + + scheduler() { + queueJob(this.baseJob) } +} - effect.scheduler = () => queueJob(job) - effect.run() +class RenderEffect_NoLifecycle extends RenderEffect { + constructor(render: () => void) { + super(render) + } - // TODO recurse handling + callback() { + this.render() + } +} + +export function renderEffect(fn: () => void, noLifecycle = false): void { + if (noLifecycle) { + new RenderEffect_NoLifecycle(fn).run() + } else { + new RenderEffect(fn).run() + } } From 8178e9997c3059e568c350957beece8acc6befe3 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sun, 6 Apr 2025 16:06:43 +0800 Subject: [PATCH 04/30] feat(benchmark): add fast-bench script --- packages-private/benchmark/fast-bench.js | 244 +++++++++++++++++++++++ packages-private/benchmark/package.json | 9 +- packages/runtime-vapor/src/index.ts | 14 +- pnpm-lock.yaml | 52 ++--- 4 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 packages-private/benchmark/fast-bench.js diff --git a/packages-private/benchmark/fast-bench.js b/packages-private/benchmark/fast-bench.js new file mode 100644 index 00000000000..18bc3267916 --- /dev/null +++ b/packages-private/benchmark/fast-bench.js @@ -0,0 +1,244 @@ +const DEV = !!process.env.VSCODE_INSPECTOR_OPTIONS +const repeat = arr => arr.flatMap(v => [v, v, v]) + +await polyfill() +await build() +await bench() + +async function polyfill() { + const { JSDOM } = await import('jsdom') + + const dom = new JSDOM('') + globalThis.document = dom.window.document + globalThis.Node = dom.window.Node +} + +async function build() { + const { compile } = await import( + '../../packages/compiler-vapor/dist/compiler-vapor.esm-browser.js' + ) + + const code = compile( + ` + + + + + + + + + +
{{ row.id }} + {{ row.label.value }} + + + + +
+`, + { + mode: 'module', + prefixIdentifiers: true, + }, + ) + .code.replace( + ` from 'vue'`, + ` from '../../../packages/vue/dist/vue.runtime-with-vapor.esm-browser${!DEV ? '.prod' : ''}.js'`, + ) + .replaceAll(`_delegateEvents(`, `// _delegateEvents(`) + + const { writeFileSync, existsSync, mkdirSync } = await import('node:fs') + const { dirname, resolve } = await import('node:path') + const { fileURLToPath } = await import('node:url') + + const __filename = fileURLToPath(import.meta.url) + const __dirname = dirname(__filename) + const outPath = resolve(__dirname, 'dist', 'component.js') + + if (!existsSync(dirname(outPath))) { + mkdirSync(dirname(outPath)) + } + writeFileSync(outPath, code) +} + +async function bench() { + let ID = 1 + + const { render } = await import('./dist/component.js') + + const { + shallowRef, + reactive, + VaporComponentInstance, + simpleSetCurrentInstance, + nextTick, + } = await import( + `../../packages/vue/dist/vue.runtime-with-vapor.esm-browser${!DEV ? '.prod' : ''}.js` + ) + + const ctx = reactive({ + selected: null, + rows: [], + }) + + simpleSetCurrentInstance(new VaporComponentInstance({}, {}, null)) + render(ctx) + + const { run, bench, boxplot } = await import('mitata') + + boxplot(() => { + for (const shouldRender of [false, true]) { + const subfix = shouldRender ? ' (render)' : '' + + // bench('append: $n' + subfix, async function* (state) { + // const n = state.get('n') + // ctx.rows = [] + // await nextTick() + // yield () => { + // ctx.rows = ctx.rows.concat(buildData(n)) + // if (shouldRender) { + // await nextTick() + // } + // } + // }).args('n', [1]) + + bench('select: $n' + subfix, async function* (state) { + const n = state.get('n') + ctx.rows = buildData(n) + await nextTick() + const ids = ctx.rows.map(d => d.id) + let i = 0 + yield shouldRender + ? async () => { + ctx.selected = ids[i++ % ids.length] + await nextTick() + } + : () => { + ctx.selected = ids[i++ % ids.length] + } + }).args('n', repeat([100])) + + bench('full-update: $n' + subfix, async function* (state) { + const n = state.get('n') + ctx.rows = buildData(n) + await nextTick() + yield shouldRender + ? async () => { + for (const row of ctx.rows) { + row.label += ' !!!' + } + await nextTick() + } + : () => { + for (const row of ctx.rows) { + row.label += ' !!!' + } + } + }).args('n', repeat([100])) + + bench('partial-update: $n' + subfix, async function* (state) { + const n = state.get('n') + ctx.rows = buildData(n) + await nextTick() + const toUpdate = [] + for (let i = 0; i < ctx.rows.length; i += 10) { + toUpdate.push(ctx.rows[i]) + } + yield shouldRender + ? async () => { + for (const row of toUpdate) { + row.label += ' !!!' + } + await nextTick() + } + : () => { + for (const row of toUpdate) { + row.label += ' !!!' + } + } + }).args('n', repeat([1000])) + } + }) + + run({ format: 'markdown' }) + + function _random(max) { + return Math.round(Math.random() * 1000) % max + } + + function buildData(count = 1000) { + const adjectives = [ + 'pretty', + 'large', + 'big', + 'small', + 'tall', + 'short', + 'long', + 'handsome', + 'plain', + 'quaint', + 'clean', + 'elegant', + 'easy', + 'angry', + 'crazy', + 'helpful', + 'mushy', + 'odd', + 'unsightly', + 'adorable', + 'important', + 'inexpensive', + 'cheap', + 'expensive', + 'fancy', + ] + const colours = [ + 'red', + 'yellow', + 'blue', + 'green', + 'pink', + 'brown', + 'purple', + 'brown', + 'white', + 'black', + 'orange', + ] + const nouns = [ + 'table', + 'chair', + 'house', + 'bbq', + 'desk', + 'car', + 'pony', + 'cookie', + 'sandwich', + 'burger', + 'pizza', + 'mouse', + 'keyboard', + ] + const data = [] + for (let i = 0; i < count; i++) + data.push({ + id: ID++, + label: shallowRef( + adjectives[_random(adjectives.length)] + + ' ' + + colours[_random(colours.length)] + + ' ' + + nouns[_random(nouns.length)], + ), + }) + return data + } +} diff --git a/packages-private/benchmark/package.json b/packages-private/benchmark/package.json index e6eb08e9539..52469cb5173 100644 --- a/packages-private/benchmark/package.json +++ b/packages-private/benchmark/package.json @@ -6,7 +6,9 @@ "type": "module", "scripts": { "dev": "pnpm start --noMinify --skipBench --vdom", - "start": "node index.js" + "start": "node index.js", + "prefast-bench": "cd ../.. && npm run build", + "fast-bench": "node fast-bench.js" }, "dependencies": { "@vitejs/plugin-vue": "catalog:", @@ -15,6 +17,9 @@ "vite": "catalog:" }, "devDependencies": { - "@types/connect": "^3.4.38" + "@types/connect": "^3.4.38", + "@types/jsdom": "latest", + "jsdom": "latest", + "mitata": "latest" } } diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 682532fa4d8..5e52b95aa7f 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -7,7 +7,14 @@ export type { VaporDirective } from './directives/custom' // compiler-use only export { insert, prepend, remove, isFragment, VaporFragment } from './block' export { setInsertionState } from './insertionState' -export { createComponent, createComponentWithFallback } from './component' +export { + createComponent, + createComponentWithFallback, + /** + * @internal + */ + VaporComponentInstance, +} from './component' export { renderEffect } from './renderEffect' export { createSlot } from './componentSlots' export { template } from './dom/template' @@ -42,3 +49,8 @@ export { applyDynamicModel, } from './directives/vModel' export { withVaporDirectives } from './directives/custom' + +/** + * @internal + */ +export { simpleSetCurrentInstance } from '@vue/runtime-core' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c72eaa1ab19..3a127bcd647 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,6 +201,15 @@ importers: '@types/connect': specifier: ^3.4.38 version: 3.4.38 + '@types/jsdom': + specifier: latest + version: 21.1.7 + jsdom: + specifier: latest + version: 26.0.0 + mitata: + specifier: latest + version: 1.0.34 packages-private/dts-built-test: dependencies: @@ -1059,35 +1068,30 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-glibc@2.4.1': resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.4.1': resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.4.1': resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.4.1': resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.4.1': resolution: {integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==} @@ -1220,67 +1224,56 @@ packages: resolution: {integrity: sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.37.0': resolution: {integrity: sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.37.0': resolution: {integrity: sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.37.0': resolution: {integrity: sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.37.0': resolution: {integrity: sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.37.0': resolution: {integrity: sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.37.0': resolution: {integrity: sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.37.0': resolution: {integrity: sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.37.0': resolution: {integrity: sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.37.0': resolution: {integrity: sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.37.0': resolution: {integrity: sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.37.0': resolution: {integrity: sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==} @@ -1320,28 +1313,24 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [glibc] '@swc/core-linux-arm64-musl@1.11.12': resolution: {integrity: sha512-3dlHowBgYBgi23ZBSvFHe/tD3PowEhxfVAy08NckWBeaG/e4dyrYMhAiccfuy6jkDYXEF1L2DtpRtxGImxoaPg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - libc: [musl] '@swc/core-linux-x64-gnu@1.11.12': resolution: {integrity: sha512-ToEWzLA5lXlYCbGNzMow6+uy4zhpXKQyFb3RHM8AYVb0n4pNPWvwF+8ybWDimeGBBaHJLgRQsUMuJ4NV6urSrA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [glibc] '@swc/core-linux-x64-musl@1.11.12': resolution: {integrity: sha512-N5xF+MDZr79e8gvVXX3YP1bMeaRL16Kst/R7bGUQvvCq1UGD86qMUtSr5KfCl0h5SNKP2YKtkN98HQLnGEikow==} engines: {node: '>=10'} cpu: [x64] os: [linux] - libc: [musl] '@swc/core-win32-arm64-msvc@1.11.12': resolution: {integrity: sha512-/PYiyYWSQRtMoOamMfhAfq0y3RWk9LpUZ49yetJn2XI85TRkL5u2DTLLNkTPvoTiCfo0eZOJF9t5b7Z6ly0iHQ==} @@ -1394,6 +1383,9 @@ packages: '@types/hash-sum@1.0.2': resolution: {integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw==} + '@types/jsdom@21.1.7': + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1412,6 +1404,9 @@ packages: '@types/serve-handler@6.1.4': resolution: {integrity: sha512-aXy58tNie0NkuSCY291xUxl0X+kGYy986l4kqW6Gi4kEXgr6Tx0fpSH7YwUSa5usPpG3s9DBeIR6hHcDtL2IvQ==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1492,25 +1487,21 @@ packages: resolution: {integrity: sha512-fp4Azi8kHz6TX8SFmKfyScZrMLfp++uRm2srpqRjsRZIIBzH74NtSkdEUHImR4G7f7XJ+sVZjCc6KDDK04YEpQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/rspack-resolver-binding-linux-arm64-musl@1.2.2': resolution: {integrity: sha512-gMiG3DCFioJxdGBzhlL86KcFgt9HGz0iDhw0YVYPsShItpN5pqIkNrI+L/Q/0gfDiGrfcE0X3VANSYIPmqEAlQ==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/rspack-resolver-binding-linux-x64-gnu@1.2.2': resolution: {integrity: sha512-n/4n2CxaUF9tcaJxEaZm+lqvaw2gflfWQ1R9I7WQgYkKEKbRKbpG/R3hopYdUmLSRI4xaW1Cy0Bz40eS2Yi4Sw==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/rspack-resolver-binding-linux-x64-musl@1.2.2': resolution: {integrity: sha512-cHyhAr6rlYYbon1L2Ag449YCj3p6XMfcYTP0AQX+KkQo025d1y/VFtPWvjMhuEsE2lLvtHm7GdJozj6BOMtzVg==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/rspack-resolver-binding-wasm32-wasi@1.2.2': resolution: {integrity: sha512-eogDKuICghDLGc32FtP+WniG38IB1RcGOGz0G3z8406dUdjJvxfHGuGs/dSlM9YEp/v0lEqhJ4mBu6X2nL9pog==} @@ -2898,6 +2889,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mitata@1.0.34: + resolution: {integrity: sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA==} + mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} @@ -4574,6 +4568,12 @@ snapshots: '@types/hash-sum@1.0.2': {} + '@types/jsdom@21.1.7': + dependencies: + '@types/node': 22.13.13 + '@types/tough-cookie': 4.0.5 + parse5: 7.2.1 + '@types/json-schema@7.0.15': {} '@types/node@22.13.13': @@ -4590,6 +4590,8 @@ snapshots: dependencies: '@types/node': 22.13.13 + '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': {} '@types/web-bluetooth@0.0.20': {} @@ -6199,6 +6201,8 @@ snapshots: minipass@7.1.2: {} + mitata@1.0.34: {} + mitt@3.0.1: {} monaco-editor@0.52.2: {} From 2c83f9a7625982c5d840299a6fbe362a6677b733 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 11 Apr 2025 14:18:46 +0800 Subject: [PATCH 05/30] feat(compiler-vapor, runtime-vapor): recognize selector pattern and key only binding pattern in v-for Co-Authored-By: edison --- .../__snapshots__/vFor.spec.ts.snap | 90 +++++ .../__tests__/transforms/vFor.spec.ts | 67 ++++ .../compiler-vapor/src/generators/block.ts | 9 +- .../src/generators/expression.ts | 24 +- packages/compiler-vapor/src/generators/for.ts | 309 +++++++++++++++++- .../src/generators/operation.ts | 22 +- packages/reactivity/src/effectScope.ts | 12 +- packages/runtime-vapor/src/apiCreateFor.ts | 73 +++++ packages/runtime-vapor/src/index.ts | 1 + packages/runtime-vapor/src/renderEffect.ts | 4 +- rollup.config.js | 1 + 11 files changed, 588 insertions(+), 24 deletions(-) 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..358c17dd623 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 { useSelectorPattern as _useSelectorPattern, 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 _selector0_0 = _useSelectorPattern(() => _ctx.selected, () => (_ctx.rows)) + const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { + const n2 = t0() + const x2 = _child(n2) + _selector0_0.register(_for_item0.value.id, () => { + _setText(x2, _toDisplayString(_ctx.selected === _for_item0.value.id ? 'danger' : '')) + }) + return n2 + }, (row) => (row.id)) + return n0 +}" +`; + +exports[`compiler: v-for > selector pattern 2`] = ` +"import { useSelectorPattern as _useSelectorPattern, setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const _selector0_0 = _useSelectorPattern(() => _ctx.selected, () => (_ctx.rows)) + const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { + const n2 = t0() + _selector0_0.register(_for_item0.value.id, () => { + _setClass(n2, _ctx.selected === _for_item0.value.id ? 'danger' : '') + }) + return n2 + }, (row) => (row.id)) + 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 { useSelectorPattern as _useSelectorPattern, setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const _selector0_0 = _useSelectorPattern(() => _ctx.selected, () => (_ctx.rows)) + const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { + const n2 = t0() + _selector0_0.register(_for_item0.value.id, () => { + _setClass(n2, { danger: _for_item0.value.id === _ctx.selected }) + }) + return n2 + }, (row) => (row.id)) + 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..c6af68dfd25 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,69 @@ 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(helper('useSelectorPattern'), [ + `() => `, + ...genExpression(selector, context), + `, `, + ...sourceExpr, + ]), + ) + } + + const blockFn = context.withId(() => { + const frag: CodeFragment[] = [] + frag.push('(', ...args, ') => {', INDENT_START) + if (selectorPatterns.length || keyOnlyBindingPatterns.length) { + const keyExpr = + render.expressions.find(expr => expr.content === keyProp!.content) ?? + keyProp! + 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}.register(`, + ...genExpression(keyExpr, context), + `, () => {`, + 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 @@ -93,6 +171,7 @@ export function genFor( } return [ + ...patternFrag, NEWLINE, `const n${id} = `, ...genCall( @@ -234,3 +313,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/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index 1c561ab6e48..d9838c97a70 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -28,6 +28,10 @@ export class EffectScope implements Subscriber, Dependency { * @internal */ cleanups: (() => void)[] = [] + /** + * @internal + */ + cleanupsLength = 0 constructor(detached = false) { if (!detached && activeEffectScope) { @@ -123,11 +127,11 @@ export class EffectScope implements Subscriber, Dependency { unlink(this.deps) } - let i, l - for (i = 0, l = this.cleanups.length; i < l; i++) { + const l = this.cleanupsLength + for (let i = 0; i < l; i++) { this.cleanups[i]() } - this.cleanups.length = 0 + this.cleanupsLength = 0 if (this.subs !== undefined) { unlink(this.subs) @@ -167,7 +171,7 @@ export function getCurrentScope(): EffectScope | undefined { */ export function onScopeDispose(fn: () => void, failSilently = false): void { if (activeEffectScope) { - activeEffectScope.cleanups.push(fn) + activeEffectScope.cleanups[activeEffectScope.cleanupsLength++] = fn } else if (__DEV__ && !failSilently) { warn( `onScopeDispose() is called when there is no active effect scope` + diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 0cd8317532f..cc91080c0de 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -3,11 +3,13 @@ import { type ShallowRef, isReactive, isShallow, + onScopeDispose, pauseTracking, resetTracking, shallowReadArray, shallowRef, toReactive, + watch, } from '@vue/reactivity' import { getSequence, isArray, isObject, isString } from '@vue/shared' import { createComment, createTextNode } from './dom/node' @@ -451,3 +453,74 @@ export function getRestElement(val: any, keys: string[]): any { export function getDefaultValue(val: any, defaultVal: any): any { return val === undefined ? defaultVal : val } + +export function useSelectorPattern( + getActiveKey: () => any, + source: () => any[], +): { + register: (key: any, cb: () => void) => void +} { + let mapVersion = 1 + let operMap = new Map void)[]>() + let activeOpers: (() => void)[] | undefined + + watch(source, newValue => { + if (Array.isArray(newValue) && !newValue.length) { + operMap = new Map() + activeOpers = undefined + mapVersion++ + } + }) + 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() + } + } + }) + + return { register } + + function register(key: any, oper: () => void) { + oper() + let opers = operMap.get(key) + if (opers !== undefined) { + opers.push(oper) + } else { + opers = [oper] + operMap.set(key, opers) + if (getActiveKey() === key) { + activeOpers = opers + } + } + const currentMapVersion = mapVersion + onScopeDispose(() => { + if (currentMapVersion === mapVersion) { + deregister(key, oper) + } + }) + } + + function deregister(key: any, oper: () => void) { + const opers = operMap.get(key) + if (opers !== undefined) { + if (opers.length === 1) { + operMap.delete(key) + if (activeOpers === opers) { + activeOpers = undefined + } + } else { + const index = opers.indexOf(oper) + if (index >= 0) { + opers.splice(index, 1) + } + } + } + } +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 5e52b95aa7f..55e68d1b417 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -37,6 +37,7 @@ export { createForSlots, getRestElement, getDefaultValue, + useSelectorPattern, } from './apiCreateFor' export { createTemplateRefSetter } from './apiTemplateRef' export { createDynamicComponent } from './apiCreateDynamicComponent' diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index 54417dc58ff..8c0ac00020c 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -58,7 +58,7 @@ class RenderEffect extends ReactiveEffect { // TODO recurse handling } - callback() { + callback(): void { const instance = this.i! const scope = this.scope // renderEffect is always called after user has registered all hooks @@ -84,7 +84,7 @@ class RenderEffect extends ReactiveEffect { } } - scheduler() { + scheduler(): void { queueJob(this.baseJob) } } diff --git a/rollup.config.js b/rollup.config.js index 7f2ecb8c864..1fa345f87fc 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -314,6 +314,7 @@ function createConfig(format, output, plugins = []) { const treeShakenDeps = [ 'source-map-js', '@babel/parser', + '@babel/types', 'estree-walker', 'entities/lib/decode.js', ] From 5f10bfea8eceb1173d44a362d7ca5d1978fe1362 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 11 Apr 2025 23:05:50 +0800 Subject: [PATCH 06/30] refactor(reactivity): execute effect stop in unlink --- packages/reactivity/src/effectScope.ts | 16 ++-------------- packages/reactivity/src/system.ts | 4 +++- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index d9838c97a70..c4cc998729b 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -50,7 +50,7 @@ export class EffectScope implements Subscriber, Dependency { this.flags |= EffectFlags.PAUSED for (let link = this.deps; link !== undefined; link = link.nextDep) { const dep = link.dep - if ('notify' in dep) { + if ('pause' in dep) { dep.pause() } } @@ -66,7 +66,7 @@ export class EffectScope implements Subscriber, Dependency { this.flags = flags & ~EffectFlags.PAUSED for (let link = this.deps; link !== undefined; link = link.nextDep) { const dep = link.dep - if ('notify' in dep) { + if ('resume' in dep) { dep.resume() } } @@ -113,26 +113,14 @@ export class EffectScope implements Subscriber, Dependency { stop(): void { if (this.active) { this.flags |= EffectFlags.STOP - - let link = this.deps - while (link !== undefined) { - const next = link.nextDep - const dep = link.dep - if ('notify' in dep) { - dep.stop() - } - link = next - } while (this.deps !== undefined) { unlink(this.deps) } - const l = this.cleanupsLength for (let i = 0; i < l; i++) { this.cleanups[i]() } this.cleanupsLength = 0 - if (this.subs !== undefined) { unlink(this.subs) } diff --git a/packages/reactivity/src/system.ts b/packages/reactivity/src/system.ts index 468562832f5..c64f2203d99 100644 --- a/packages/reactivity/src/system.ts +++ b/packages/reactivity/src/system.ts @@ -127,7 +127,9 @@ export function unlink(link: Link): void { } if (dep.subs === undefined && 'deps' in dep) { const depFlags = dep.flags - if (!(depFlags & SubscriberFlags.Dirty)) { + if ('stop' in dep) { + dep.stop() + } else if (!(depFlags & SubscriberFlags.Dirty)) { dep.flags = depFlags | SubscriberFlags.Dirty } while (dep.deps !== undefined) { From e4c548cf1f5afa33b7ef1affaaa914daeb6e53e3 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 14 Apr 2025 04:32:47 +0800 Subject: [PATCH 07/30] refactor(runtime-vapor): redo v-for updating --- packages/runtime-vapor/__tests__/for.spec.ts | 2 +- packages/runtime-vapor/src/apiCreateFor.ts | 215 +++++++------------ 2 files changed, 75 insertions(+), 142 deletions(-) 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 cc91080c0de..285a121b464 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -11,7 +11,7 @@ import { 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, @@ -134,149 +134,90 @@ 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 moveOrMount = ( + index: number, + item: ReturnType, + key: any, + anchor: Node, + ) => { + const oldIndex = oldKeyToIndexMap.get(key) + if (oldIndex !== undefined) { + const block = (newBlocks[index] = oldBlocks[oldIndex]) + update(block, ...item) + insert(block, parent!, anchor) + oldKeyToIndexMap.delete(key) + } else { + mount(source, index, item, key, anchor) } } - // 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) - } + for (let i = pendingNews.length - 1; i >= 0; i--) { + const [index, item, key] = pendingNews[i] + moveOrMount( + index, + item, + key, + index < commonLength - 1 + ? normalizeAnchor(newBlocks[index + 1].nodes) + : defaultAnchor, + ) + } - // 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 = left; i < newLength - right; i++) { + const item = getItem(source, i) + const key = getKey.apply(null, item) + moveOrMount(i, item, key, defaultAnchor) + } - // 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-- - } - } - } + for (const i of oldKeyToIndexMap.values()) { + unmount(oldBlocks[i]) } } } @@ -295,9 +236,10 @@ 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 @@ -321,7 +263,7 @@ export const createFor = ( itemRef, keyRef, indexRef, - getKey && getKey(item, key, index), + key2, )) if (parent) insert(block.nodes, parent, anchor) @@ -329,15 +271,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, From 63f4328eb4383bedc1f9cf0324bc02faa7272079 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 15 Apr 2025 18:57:49 +0800 Subject: [PATCH 08/30] refactor: minor code adjustments --- packages/reactivity/src/effect.ts | 10 +++--- packages/reactivity/src/ref.ts | 29 ++++++++------- packages/runtime-core/src/renderer.ts | 3 +- packages/runtime-core/src/scheduler.ts | 7 ++-- packages/runtime-vapor/src/renderEffect.ts | 41 ++++++++++------------ 5 files changed, 43 insertions(+), 47 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index ef87177c0c3..84b808c75d4 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -96,12 +96,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() } } @@ -297,9 +297,9 @@ export function onEffectCleanup(fn: () => void, failSilently = false): void { } function cleanupEffect(e: ReactiveEffect) { - const { cleanup } = e - e.cleanup = undefined + const cleanup = e.cleanup if (cleanup !== undefined) { + e.cleanup = undefined // run cleanup without active effect const prevSub = activeSub activeSub = undefined 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/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 5a18d62a8e1..ef2ae3011da 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1599,8 +1599,7 @@ function baseCreateRenderer( instance.scope.off() 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) diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 1f51eabc09c..2b5b8e98ed8 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -97,7 +97,8 @@ function findInsertionIndex(id: number, isPre: boolean) { * @internal for runtime-vapor only */ export function queueJob(job: SchedulerJob, isPre = false): void { - if (!(job.flags! & SchedulerJobFlags.QUEUED)) { + const flags = job.flags! + if (!(flags & SchedulerJobFlags.QUEUED)) { if (job.id === undefined) { job.id = isPre ? -1 : Infinity } @@ -113,9 +114,7 @@ export function queueJob(job: SchedulerJob, isPre = false): void { queue.splice(findInsertionIndex(job.id, isPre), 0, job) } isPre ? preJobsLength++ : mainJobsLength++ - - job.flags! |= SchedulerJobFlags.QUEUED - + job.flags! = flags | SchedulerJobFlags.QUEUED queueFlush() } } diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index 8c0ac00020c..7b60416f972 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -1,8 +1,4 @@ -import { - type EffectScope, - ReactiveEffect, - getCurrentScope, -} from '@vue/reactivity' +import { EffectFlags, type EffectScope, ReactiveEffect } from '@vue/reactivity' import { type SchedulerJob, currentInstance, @@ -17,24 +13,18 @@ import { invokeArrayFns } from '@vue/shared' class RenderEffect extends ReactiveEffect { i: VaporComponentInstance | null - scope: EffectScope | undefined - baseJob: SchedulerJob - postJob: SchedulerJob + job: SchedulerJob + updateJob: SchedulerJob constructor(public render: () => void) { super() const instance = currentInstance as VaporComponentInstance | null - const scope = getCurrentScope() - if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) { + if (__DEV__ && !__TEST__ && !this.subs && !isVaporComponent(instance)) { warn('renderEffect called without active EffectScope or Vapor instance.') } - this.baseJob = () => { - if (this.dirty) { - this.run() - } - } - this.postJob = () => { + const job: SchedulerJob = super.scheduler.bind(this) + this.updateJob = () => { instance!.isUpdating = false instance!.u && invokeArrayFns(instance!.u) } @@ -48,19 +38,19 @@ class RenderEffect extends ReactiveEffect { ? e => invokeArrayFns(instance.rtg!, e) : void 0 } - this.baseJob.i = instance - this.baseJob.id = instance.uid + job.i = instance + job.id = instance.uid } + this.job = job this.i = instance - this.scope = scope // TODO recurse handling } callback(): void { const instance = this.i! - const scope = this.scope + 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) { @@ -73,7 +63,7 @@ class RenderEffect extends ReactiveEffect { instance.isUpdating = true instance.bu && invokeArrayFns(instance.bu) this.render() - queuePostFlushCb(this.postJob) + queuePostFlushCb(this.updateJob) } else { this.render() } @@ -84,8 +74,13 @@ class RenderEffect extends ReactiveEffect { } } - scheduler(): void { - queueJob(this.baseJob) + notify(): void { + const flags = this.flags + if (!(flags & EffectFlags.PAUSED)) { + queueJob(this.job) + } else { + this.flags = flags | EffectFlags.NOTIFIED + } } } From 6744e6ff3afb76a9dd116de7a7999bc3ed08999f Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 15 Apr 2025 23:37:36 +0800 Subject: [PATCH 09/30] refactor: make effect scope enable/disable logic symmetric --- .../reactivity/__tests__/effectScope.spec.ts | 17 +------ packages/reactivity/src/effectScope.ts | 35 ++++---------- packages/reactivity/src/index.ts | 4 ++ packages/runtime-core/src/apiLifecycle.ts | 4 +- packages/runtime-core/src/apiSetupHelpers.ts | 3 +- packages/runtime-core/src/apiWatch.ts | 4 +- packages/runtime-core/src/component.ts | 12 +++-- .../src/componentCurrentInstance.ts | 48 +++++++------------ packages/runtime-core/src/componentProps.ts | 4 +- packages/runtime-core/src/index.ts | 1 + packages/runtime-core/src/renderer.ts | 5 +- .../runtime-vapor/__tests__/dom/prop.spec.ts | 13 ++--- packages/runtime-vapor/src/component.ts | 7 ++- packages/runtime-vapor/src/componentProps.ts | 8 ++-- packages/runtime-vapor/src/hmr.ts | 13 ++--- packages/runtime-vapor/src/renderEffect.ts | 11 ++--- 16 files changed, 66 insertions(+), 123 deletions(-) diff --git a/packages/reactivity/__tests__/effectScope.spec.ts b/packages/reactivity/__tests__/effectScope.spec.ts index fce49fdd03d..93ba9e89073 100644 --- a/packages/reactivity/__tests__/effectScope.spec.ts +++ b/packages/reactivity/__tests__/effectScope.spec.ts @@ -9,6 +9,7 @@ import { onScopeDispose, reactive, ref, + setCurrentScope, } from '../src' describe('reactivity/effect/scope', () => { @@ -289,21 +290,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) }) }) diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index c4cc998729b..0b7aca716af 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -20,10 +20,6 @@ export class EffectScope implements Subscriber, Dependency { subs: Link | undefined = undefined subsTail: Link | undefined = undefined - /** - * @internal track `on` calls, allow `on` call multiple times - */ - private _on = 0 /** * @internal */ @@ -87,29 +83,6 @@ export class EffectScope implements Subscriber, Dependency { } } - prevScope: EffectScope | undefined - /** - * This should only be called on non-detached scopes - * @internal - */ - on(): void { - if (++this._on === 1) { - this.prevScope = activeEffectScope - activeEffectScope = this - } - } - - /** - * 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 { if (this.active) { this.flags |= EffectFlags.STOP @@ -150,6 +123,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. 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/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 c7a43418ab2..ec2a5a2482c 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -253,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..6074ea42648 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' @@ -891,7 +890,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 +902,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 +910,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 +1085,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/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 ef2ae3011da..86b17c209c1 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' @@ -1594,9 +1595,9 @@ 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.scheduler.bind(effect)) 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/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 7b60416f972..9ad24f1e0c7 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -4,7 +4,7 @@ import { currentInstance, queueJob, queuePostFlushCb, - simpleSetCurrentInstance, + setCurrentInstance, startMeasure, warn, } from '@vue/runtime-dom' @@ -49,16 +49,14 @@ class RenderEffect extends ReactiveEffect { } callback(): void { - const instance = this.i! + 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 = currentInstance - simpleSetCurrentInstance(instance) - if (scope) scope.on() + const prev = setCurrentInstance(instance, scope) if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) { instance.isUpdating = true instance.bu && invokeArrayFns(instance.bu) @@ -67,8 +65,7 @@ class RenderEffect extends ReactiveEffect { } else { this.render() } - if (scope) scope.off() - simpleSetCurrentInstance(prev, instance) + setCurrentInstance(...prev) if (__DEV__ && instance) { startMeasure(instance, `renderEffect`) } From 679b779a788490c32417947d42bd9e11cce378d1 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 16 Apr 2025 02:44:23 +0800 Subject: [PATCH 10/30] refactor(runtime-vapor): move moveOrMount function --- packages/runtime-vapor/src/apiCreateFor.ts | 40 ++++++++++++---------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 285a121b464..ecd0d9fc2d8 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -181,26 +181,11 @@ export const createFor = ( oldKeyToIndexMap.set(oldBlocks[i].key, i) } - const moveOrMount = ( - index: number, - item: ReturnType, - key: any, - anchor: Node, - ) => { - const oldIndex = oldKeyToIndexMap.get(key) - if (oldIndex !== undefined) { - const block = (newBlocks[index] = oldBlocks[oldIndex]) - update(block, ...item) - insert(block, parent!, anchor) - oldKeyToIndexMap.delete(key) - } else { - mount(source, index, item, key, anchor) - } - } - for (let i = pendingNews.length - 1; i >= 0; i--) { const [index, item, key] = pendingNews[i] moveOrMount( + oldKeyToIndexMap, + source, index, item, key, @@ -213,7 +198,7 @@ export const createFor = ( for (let i = left; i < newLength - right; i++) { const item = getItem(source, i) const key = getKey.apply(null, item) - moveOrMount(i, item, key, defaultAnchor) + moveOrMount(oldKeyToIndexMap, source, i, item, key, defaultAnchor) } for (const i of oldKeyToIndexMap.values()) { @@ -271,6 +256,25 @@ export const createFor = ( return block } + const moveOrMount = ( + keyToOldIndexMap: Map, + source: ResolvedSource, + index: number, + item: ReturnType, + key: any, + anchor: Node, + ) => { + const oldIndex = keyToOldIndexMap.get(key) + if (oldIndex !== undefined) { + const block = (newBlocks[index] = oldBlocks[oldIndex]) + update(block, ...item) + insert(block, parent!, anchor) + keyToOldIndexMap.delete(key) + } else { + mount(source, index, item, key, anchor) + } + } + const update = ( { itemRef, keyRef, indexRef }: ForBlock, newItem: any, From 6a439c8358a9e7633dce94b461e8340e8ceca1e7 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 16 Apr 2025 04:18:28 +0800 Subject: [PATCH 11/30] perf(runtime-vapor): remove reliance on onScopeDispose in useSelector --- .../__snapshots__/vFor.spec.ts.snap | 18 +-- packages/compiler-vapor/src/generators/for.ts | 13 +- packages/runtime-vapor/src/apiCreateFor.ts | 149 +++++++++--------- packages/runtime-vapor/src/index.ts | 1 - 4 files changed, 87 insertions(+), 94 deletions(-) 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 358c17dd623..c87a37ba5d2 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -137,36 +137,36 @@ export function render(_ctx) { `; exports[`compiler: v-for > selector pattern 1`] = ` -"import { useSelectorPattern as _useSelectorPattern, child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue'; +"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 _selector0_0 = _useSelectorPattern(() => _ctx.selected, () => (_ctx.rows)) const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { const n2 = t0() const x2 = _child(n2) - _selector0_0.register(_for_item0.value.id, () => { + _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 { useSelectorPattern as _useSelectorPattern, setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; +"import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; const t0 = _template("", true) export function render(_ctx) { - const _selector0_0 = _useSelectorPattern(() => _ctx.selected, () => (_ctx.rows)) const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { const n2 = t0() - _selector0_0.register(_for_item0.value.id, () => { + _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 }" `; @@ -189,18 +189,18 @@ export function render(_ctx) { `; exports[`compiler: v-for > selector pattern 4`] = ` -"import { useSelectorPattern as _useSelectorPattern, setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; +"import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; const t0 = _template("", true) export function render(_ctx) { - const _selector0_0 = _useSelectorPattern(() => _ctx.selected, () => (_ctx.rows)) const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { const n2 = t0() - _selector0_0.register(_for_item0.value.id, () => { + _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 }" `; diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts index c6af68dfd25..40f002a8536 100644 --- a/packages/compiler-vapor/src/generators/for.ts +++ b/packages/compiler-vapor/src/generators/for.ts @@ -107,11 +107,9 @@ export function genFor( patternFrag.push( NEWLINE, `const ${selectorName} = `, - ...genCall(helper('useSelectorPattern'), [ + ...genCall(`n${id}.useSelector`, [ `() => `, ...genExpression(selector, context), - `, `, - ...sourceExpr, ]), ) } @@ -120,9 +118,6 @@ export function genFor( const frag: CodeFragment[] = [] frag.push('(', ...args, ') => {', INDENT_START) if (selectorPatterns.length || keyOnlyBindingPatterns.length) { - const keyExpr = - render.expressions.find(expr => expr.content === keyProp!.content) ?? - keyProp! frag.push( ...genBlockContent(render, context, false, () => { const patternFrag: CodeFragment[] = [] @@ -131,9 +126,7 @@ export function genFor( const { effect } = selectorPatterns[i] patternFrag.push( NEWLINE, - `_selector${id}_${i}.register(`, - ...genExpression(keyExpr, context), - `, () => {`, + `_selector${id}_${i}(() => {`, INDENT_START, ) for (const oper of effect.operations) { @@ -171,7 +164,6 @@ export function genFor( } return [ - ...patternFrag, NEWLINE, `const n${id} = `, ...genCall( @@ -182,6 +174,7 @@ export function genFor( flags ? String(flags) : undefined, // todo: hydrationNode ), + ...patternFrag, ] // construct a id -> accessor path map. diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index ecd0d9fc2d8..1cd2abfe098 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -3,7 +3,6 @@ import { type ShallowRef, isReactive, isShallow, - onScopeDispose, pauseTracking, resetTracking, shallowReadArray, @@ -80,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 selectors: { + deregister: (key: any) => void + cleanup: () => void + }[] = [] if (__DEV__ && !instance) { warn('createFor() can only be used inside setup()') @@ -113,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 = '' @@ -230,6 +238,7 @@ export const createFor = ( const keyRef = needKey ? shallowRef(key) : undefined const indexRef = needIndex ? shallowRef(index) : undefined + currentKey = key2 let nodes: Block let scope: EffectScope | undefined if (isComponent) { @@ -292,9 +301,18 @@ export const createFor = ( } } - const unmount = ({ nodes, scope }: ForBlock, doRemove = true) => { + const unmount = ( + { nodes, scope, key }: ForBlock, + doRemove = true, + doDeregister = true, + ) => { scope && scope.stop() doRemove && removeBlock(nodes, parent!) + if (doDeregister) { + for (const selector of selectors) { + selector.deregister(key) + } + } } if (flags & VaporVForFlags.ONCE) { @@ -307,7 +325,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( @@ -390,74 +462,3 @@ export function getRestElement(val: any, keys: string[]): any { export function getDefaultValue(val: any, defaultVal: any): any { return val === undefined ? defaultVal : val } - -export function useSelectorPattern( - getActiveKey: () => any, - source: () => any[], -): { - register: (key: any, cb: () => void) => void -} { - let mapVersion = 1 - let operMap = new Map void)[]>() - let activeOpers: (() => void)[] | undefined - - watch(source, newValue => { - if (Array.isArray(newValue) && !newValue.length) { - operMap = new Map() - activeOpers = undefined - mapVersion++ - } - }) - 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() - } - } - }) - - return { register } - - function register(key: any, oper: () => void) { - oper() - let opers = operMap.get(key) - if (opers !== undefined) { - opers.push(oper) - } else { - opers = [oper] - operMap.set(key, opers) - if (getActiveKey() === key) { - activeOpers = opers - } - } - const currentMapVersion = mapVersion - onScopeDispose(() => { - if (currentMapVersion === mapVersion) { - deregister(key, oper) - } - }) - } - - function deregister(key: any, oper: () => void) { - const opers = operMap.get(key) - if (opers !== undefined) { - if (opers.length === 1) { - operMap.delete(key) - if (activeOpers === opers) { - activeOpers = undefined - } - } else { - const index = opers.indexOf(oper) - if (index >= 0) { - opers.splice(index, 1) - } - } - } - } -} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 55e68d1b417..5e52b95aa7f 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -37,7 +37,6 @@ export { createForSlots, getRestElement, getDefaultValue, - useSelectorPattern, } from './apiCreateFor' export { createTemplateRefSetter } from './apiTemplateRef' export { createDynamicComponent } from './apiCreateDynamicComponent' From 621044a0a23ed20bb9ac057685890fe1ea9fe5a8 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 16 Apr 2025 21:22:16 +0800 Subject: [PATCH 12/30] fix(runtime-vapor): setup minimum valid length of pendingNews --- packages/runtime-vapor/src/apiCreateFor.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 1cd2abfe098..b8ebc1cc770 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -189,6 +189,13 @@ export const createFor = ( oldKeyToIndexMap.set(oldBlocks[i].key, 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]) + } + for (let i = pendingNews.length - 1; i >= 0; i--) { const [index, item, key] = pendingNews[i] moveOrMount( @@ -197,13 +204,13 @@ export const createFor = ( index, item, key, - index < commonLength - 1 + index < prepareLength - 1 ? normalizeAnchor(newBlocks[index + 1].nodes) : defaultAnchor, ) } - for (let i = left; i < newLength - right; i++) { + for (let i = prepareLength; i < newLength - right; i++) { const item = getItem(source, i) const key = getKey.apply(null, item) moveOrMount(oldKeyToIndexMap, source, i, item, key, defaultAnchor) From a3647c01519b4df215f30e9758c47d8f552a839f Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 16 Apr 2025 21:35:23 +0800 Subject: [PATCH 13/30] refactor(reactivity): rename "callback" to "fn" in ReactiveEffect --- packages/reactivity/__tests__/effect.spec.ts | 4 ++-- packages/reactivity/src/effect.ts | 10 +++++----- packages/runtime-vapor/src/renderEffect.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 3b8c81cb72b..20f0244a7bc 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -624,7 +624,7 @@ describe('reactivity/effect', () => { const runner = effect(() => {}) const otherRunner = effect(runner) expect(runner).not.toBe(otherRunner) - expect(runner.effect.callback).toBe(otherRunner.effect.callback) + expect(runner.effect.fn).toBe(otherRunner.effect.fn) }) it('should wrap if the passed function is a fake effect', () => { @@ -632,7 +632,7 @@ describe('reactivity/effect', () => { fakeRunner.effect = {} const runner = effect(fakeRunner) expect(fakeRunner).not.toBe(runner) - expect(runner.effect.callback).toBe(fakeRunner) + expect(runner.effect.fn).toBe(fakeRunner) }) it('should not run multiple times for a single mutation', () => { diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 84b808c75d4..5f1375dd024 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -74,11 +74,11 @@ export class ReactiveEffect implements ReactiveEffectOptions { onTrigger?: (event: DebuggerEvent) => void // @ts-expect-error - callback(): T {} + fn(): T {} constructor(fn?: () => T) { if (fn !== undefined) { - this.callback = fn + this.fn = fn } if (activeEffectScope && activeEffectScope.active) { link(this, activeEffectScope) @@ -126,7 +126,7 @@ export class ReactiveEffect implements ReactiveEffectOptions { if (!this.active) { // stopped during cleanup - return this.callback() + return this.fn() } cleanupEffect(this) const prevSub = activeSub @@ -134,7 +134,7 @@ export class ReactiveEffect implements ReactiveEffectOptions { startTracking(this) try { - return this.callback() + return this.fn() } finally { if (__DEV__ && activeSub !== this) { warn( @@ -197,7 +197,7 @@ export function effect( options?: ReactiveEffectOptions, ): ReactiveEffectRunner { if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) { - fn = (fn as ReactiveEffectRunner).effect.callback + fn = (fn as ReactiveEffectRunner).effect.fn } const e = new ReactiveEffect(fn) diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index 9ad24f1e0c7..6ea2a2dc17c 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -48,7 +48,7 @@ class RenderEffect extends ReactiveEffect { // TODO recurse handling } - callback(): void { + 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 @@ -86,7 +86,7 @@ class RenderEffect_NoLifecycle extends RenderEffect { super(render) } - callback() { + fn() { this.render() } } From 706f28375b2fbf5c10d2f22b1c944c0f45f2c57f Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 16 Apr 2025 22:34:02 +0800 Subject: [PATCH 14/30] refactor(reactivity): remove SubscriberFlags.Propagated --- packages/reactivity/src/debug.ts | 7 +++++-- packages/reactivity/src/system.ts | 15 ++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) 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/system.ts b/packages/reactivity/src/system.ts index c64f2203d99..a50c1a7861d 100644 --- a/packages/reactivity/src/system.ts +++ b/packages/reactivity/src/system.ts @@ -29,7 +29,6 @@ export const enum SubscriberFlags { Recursed = 1 << 4, Dirty = 1 << 5, Pending = 1 << 6, - Propagated = Dirty | Pending, } interface OneWayLink { @@ -155,7 +154,8 @@ export function propagate(current: Link): void { subFlags & (SubscriberFlags.Tracking | SubscriberFlags.Recursed | - SubscriberFlags.Propagated) + SubscriberFlags.Dirty | + SubscriberFlags.Pending) ) ) { sub.flags = subFlags | targetFlag @@ -167,7 +167,7 @@ export function propagate(current: Link): void { sub.flags = (subFlags & ~SubscriberFlags.Recursed) | targetFlag shouldNotify = 1 } else if ( - !(subFlags & SubscriberFlags.Propagated) && + !(subFlags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) && isValidLink(current, sub) ) { sub.flags = subFlags | SubscriberFlags.Recursed | targetFlag @@ -194,7 +194,7 @@ export function propagate(current: Link): void { sub.flags = subFlags | targetFlag } else if ( !(subFlags & targetFlag) && - subFlags & SubscriberFlags.Propagated && + subFlags & (SubscriberFlags.Dirty | SubscriberFlags.Pending) && isValidLink(current, sub) ) { sub.flags = subFlags | targetFlag @@ -229,7 +229,12 @@ 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 } From 9b1acbd8cb876d1120b3253253188fff164c1992 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 16 Apr 2025 23:28:59 +0800 Subject: [PATCH 15/30] refactor(reactivity): do not use effect active getter --- packages/reactivity/src/effect.ts | 16 ++++++++-------- packages/reactivity/src/effectScope.ts | 8 +++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 5f1375dd024..bb5ee0404a4 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -122,9 +122,8 @@ export class ReactiveEffect implements ReactiveEffectOptions { } run(): T { - // TODO cleanupEffect - - if (!this.active) { + let flags = this.flags + if (flags & EffectFlags.STOP) { // stopped during cleanup return this.fn() } @@ -144,10 +143,10 @@ export class ReactiveEffect implements ReactiveEffectOptions { } setActiveSub(prevSub) endTracking(this) - const flags = this.flags + flags = this.flags if ( - flags & SubscriberFlags.Recursed && - flags & EffectFlags.ALLOW_RECURSE + (flags & (SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE)) === + (SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE) ) { this.flags = flags & ~SubscriberFlags.Recursed this.notify() @@ -156,12 +155,13 @@ export class ReactiveEffect implements ReactiveEffectOptions { } stop(): void { - if (this.active) { + const flags = this.flags + if (!(flags & EffectFlags.STOP)) { + this.flags = flags | EffectFlags.STOP startTracking(this) endTracking(this) cleanupEffect(this) this.onStop && this.onStop() - this.flags |= EffectFlags.STOP if (this.subs !== undefined) { unlink(this.subs) diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index 0b7aca716af..85ead32b416 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -70,7 +70,8 @@ export class EffectScope implements Subscriber, Dependency { } run(fn: () => T): T | undefined { - if (this.active) { + const flags = this.flags + if (!(flags & EffectFlags.STOP)) { const prevEffectScope = activeEffectScope try { activeEffectScope = this @@ -84,8 +85,9 @@ export class EffectScope implements Subscriber, Dependency { } stop(): void { - if (this.active) { - this.flags |= EffectFlags.STOP + const flags = this.flags + if (!(flags & EffectFlags.STOP)) { + this.flags = flags | EffectFlags.STOP while (this.deps !== undefined) { unlink(this.deps) } From da80efbed53015725809d1401e4ad186c3eef5a3 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 17 Apr 2025 01:41:52 +0800 Subject: [PATCH 16/30] perf(runtime-vapor): fast path to replace all rows --- packages/runtime-vapor/src/apiCreateFor.ts | 105 ++++++++++++++------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index b8ebc1cc770..9df529f995e 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -85,8 +85,8 @@ export const createFor = ( 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 @@ -196,28 +196,80 @@ export const createFor = ( pendingNews.push([i, item, key]) } + 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]) + } + } + for (let i = pendingNews.length - 1; i >= 0; i--) { const [index, item, key] = pendingNews[i] moveOrMount( - oldKeyToIndexMap, - source, index, item, key, - index < prepareLength - 1 - ? normalizeAnchor(newBlocks[index + 1].nodes) - : defaultAnchor, + 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(oldKeyToIndexMap, source, i, item, key, defaultAnchor) + moveOrMount(i, item, key, -1) } + const shouldUseFastRemove = pendingMounts.length === newLength + for (const i of oldKeyToIndexMap.values()) { - unmount(oldBlocks[i]) + 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), + ) } } } @@ -272,25 +324,6 @@ export const createFor = ( return block } - const moveOrMount = ( - keyToOldIndexMap: Map, - source: ResolvedSource, - index: number, - item: ReturnType, - key: any, - anchor: Node, - ) => { - const oldIndex = keyToOldIndexMap.get(key) - if (oldIndex !== undefined) { - const block = (newBlocks[index] = oldBlocks[oldIndex]) - update(block, ...item) - insert(block, parent!, anchor) - keyToOldIndexMap.delete(key) - } else { - mount(source, index, item, key, anchor) - } - } - const update = ( { itemRef, keyRef, indexRef }: ForBlock, newItem: any, @@ -308,16 +341,16 @@ export const createFor = ( } } - const unmount = ( - { nodes, scope, key }: ForBlock, - doRemove = true, - doDeregister = 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(key) + selector.deregister(block.key) } } } From 0545b643e66113d8b3d288336235f862d1d29cf0 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 18 Apr 2025 03:24:10 +0800 Subject: [PATCH 17/30] Revert "feat(benchmark): add fast-bench script" This reverts commit 8178e9997c3059e568c350957beece8acc6befe3. --- packages-private/benchmark/fast-bench.js | 244 ----------------------- packages-private/benchmark/package.json | 9 +- packages/runtime-vapor/src/index.ts | 14 +- pnpm-lock.yaml | 52 +++-- 4 files changed, 27 insertions(+), 292 deletions(-) delete mode 100644 packages-private/benchmark/fast-bench.js diff --git a/packages-private/benchmark/fast-bench.js b/packages-private/benchmark/fast-bench.js deleted file mode 100644 index 18bc3267916..00000000000 --- a/packages-private/benchmark/fast-bench.js +++ /dev/null @@ -1,244 +0,0 @@ -const DEV = !!process.env.VSCODE_INSPECTOR_OPTIONS -const repeat = arr => arr.flatMap(v => [v, v, v]) - -await polyfill() -await build() -await bench() - -async function polyfill() { - const { JSDOM } = await import('jsdom') - - const dom = new JSDOM('') - globalThis.document = dom.window.document - globalThis.Node = dom.window.Node -} - -async function build() { - const { compile } = await import( - '../../packages/compiler-vapor/dist/compiler-vapor.esm-browser.js' - ) - - const code = compile( - ` - - - - - - - - - -
{{ row.id }} - {{ row.label.value }} - - - - -
-`, - { - mode: 'module', - prefixIdentifiers: true, - }, - ) - .code.replace( - ` from 'vue'`, - ` from '../../../packages/vue/dist/vue.runtime-with-vapor.esm-browser${!DEV ? '.prod' : ''}.js'`, - ) - .replaceAll(`_delegateEvents(`, `// _delegateEvents(`) - - const { writeFileSync, existsSync, mkdirSync } = await import('node:fs') - const { dirname, resolve } = await import('node:path') - const { fileURLToPath } = await import('node:url') - - const __filename = fileURLToPath(import.meta.url) - const __dirname = dirname(__filename) - const outPath = resolve(__dirname, 'dist', 'component.js') - - if (!existsSync(dirname(outPath))) { - mkdirSync(dirname(outPath)) - } - writeFileSync(outPath, code) -} - -async function bench() { - let ID = 1 - - const { render } = await import('./dist/component.js') - - const { - shallowRef, - reactive, - VaporComponentInstance, - simpleSetCurrentInstance, - nextTick, - } = await import( - `../../packages/vue/dist/vue.runtime-with-vapor.esm-browser${!DEV ? '.prod' : ''}.js` - ) - - const ctx = reactive({ - selected: null, - rows: [], - }) - - simpleSetCurrentInstance(new VaporComponentInstance({}, {}, null)) - render(ctx) - - const { run, bench, boxplot } = await import('mitata') - - boxplot(() => { - for (const shouldRender of [false, true]) { - const subfix = shouldRender ? ' (render)' : '' - - // bench('append: $n' + subfix, async function* (state) { - // const n = state.get('n') - // ctx.rows = [] - // await nextTick() - // yield () => { - // ctx.rows = ctx.rows.concat(buildData(n)) - // if (shouldRender) { - // await nextTick() - // } - // } - // }).args('n', [1]) - - bench('select: $n' + subfix, async function* (state) { - const n = state.get('n') - ctx.rows = buildData(n) - await nextTick() - const ids = ctx.rows.map(d => d.id) - let i = 0 - yield shouldRender - ? async () => { - ctx.selected = ids[i++ % ids.length] - await nextTick() - } - : () => { - ctx.selected = ids[i++ % ids.length] - } - }).args('n', repeat([100])) - - bench('full-update: $n' + subfix, async function* (state) { - const n = state.get('n') - ctx.rows = buildData(n) - await nextTick() - yield shouldRender - ? async () => { - for (const row of ctx.rows) { - row.label += ' !!!' - } - await nextTick() - } - : () => { - for (const row of ctx.rows) { - row.label += ' !!!' - } - } - }).args('n', repeat([100])) - - bench('partial-update: $n' + subfix, async function* (state) { - const n = state.get('n') - ctx.rows = buildData(n) - await nextTick() - const toUpdate = [] - for (let i = 0; i < ctx.rows.length; i += 10) { - toUpdate.push(ctx.rows[i]) - } - yield shouldRender - ? async () => { - for (const row of toUpdate) { - row.label += ' !!!' - } - await nextTick() - } - : () => { - for (const row of toUpdate) { - row.label += ' !!!' - } - } - }).args('n', repeat([1000])) - } - }) - - run({ format: 'markdown' }) - - function _random(max) { - return Math.round(Math.random() * 1000) % max - } - - function buildData(count = 1000) { - const adjectives = [ - 'pretty', - 'large', - 'big', - 'small', - 'tall', - 'short', - 'long', - 'handsome', - 'plain', - 'quaint', - 'clean', - 'elegant', - 'easy', - 'angry', - 'crazy', - 'helpful', - 'mushy', - 'odd', - 'unsightly', - 'adorable', - 'important', - 'inexpensive', - 'cheap', - 'expensive', - 'fancy', - ] - const colours = [ - 'red', - 'yellow', - 'blue', - 'green', - 'pink', - 'brown', - 'purple', - 'brown', - 'white', - 'black', - 'orange', - ] - const nouns = [ - 'table', - 'chair', - 'house', - 'bbq', - 'desk', - 'car', - 'pony', - 'cookie', - 'sandwich', - 'burger', - 'pizza', - 'mouse', - 'keyboard', - ] - const data = [] - for (let i = 0; i < count; i++) - data.push({ - id: ID++, - label: shallowRef( - adjectives[_random(adjectives.length)] + - ' ' + - colours[_random(colours.length)] + - ' ' + - nouns[_random(nouns.length)], - ), - }) - return data - } -} diff --git a/packages-private/benchmark/package.json b/packages-private/benchmark/package.json index 52469cb5173..e6eb08e9539 100644 --- a/packages-private/benchmark/package.json +++ b/packages-private/benchmark/package.json @@ -6,9 +6,7 @@ "type": "module", "scripts": { "dev": "pnpm start --noMinify --skipBench --vdom", - "start": "node index.js", - "prefast-bench": "cd ../.. && npm run build", - "fast-bench": "node fast-bench.js" + "start": "node index.js" }, "dependencies": { "@vitejs/plugin-vue": "catalog:", @@ -17,9 +15,6 @@ "vite": "catalog:" }, "devDependencies": { - "@types/connect": "^3.4.38", - "@types/jsdom": "latest", - "jsdom": "latest", - "mitata": "latest" + "@types/connect": "^3.4.38" } } diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 5e52b95aa7f..682532fa4d8 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -7,14 +7,7 @@ export type { VaporDirective } from './directives/custom' // compiler-use only export { insert, prepend, remove, isFragment, VaporFragment } from './block' export { setInsertionState } from './insertionState' -export { - createComponent, - createComponentWithFallback, - /** - * @internal - */ - VaporComponentInstance, -} from './component' +export { createComponent, createComponentWithFallback } from './component' export { renderEffect } from './renderEffect' export { createSlot } from './componentSlots' export { template } from './dom/template' @@ -49,8 +42,3 @@ export { applyDynamicModel, } from './directives/vModel' export { withVaporDirectives } from './directives/custom' - -/** - * @internal - */ -export { simpleSetCurrentInstance } from '@vue/runtime-core' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a127bcd647..c72eaa1ab19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,15 +201,6 @@ importers: '@types/connect': specifier: ^3.4.38 version: 3.4.38 - '@types/jsdom': - specifier: latest - version: 21.1.7 - jsdom: - specifier: latest - version: 26.0.0 - mitata: - specifier: latest - version: 1.0.34 packages-private/dts-built-test: dependencies: @@ -1068,30 +1059,35 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-glibc@2.4.1': resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.4.1': resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.4.1': resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.4.1': resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.4.1': resolution: {integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==} @@ -1224,56 +1220,67 @@ packages: resolution: {integrity: sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.37.0': resolution: {integrity: sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.37.0': resolution: {integrity: sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.37.0': resolution: {integrity: sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.37.0': resolution: {integrity: sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.37.0': resolution: {integrity: sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.37.0': resolution: {integrity: sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.37.0': resolution: {integrity: sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.37.0': resolution: {integrity: sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.37.0': resolution: {integrity: sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.37.0': resolution: {integrity: sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.37.0': resolution: {integrity: sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==} @@ -1313,24 +1320,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.11.12': resolution: {integrity: sha512-3dlHowBgYBgi23ZBSvFHe/tD3PowEhxfVAy08NckWBeaG/e4dyrYMhAiccfuy6jkDYXEF1L2DtpRtxGImxoaPg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.11.12': resolution: {integrity: sha512-ToEWzLA5lXlYCbGNzMow6+uy4zhpXKQyFb3RHM8AYVb0n4pNPWvwF+8ybWDimeGBBaHJLgRQsUMuJ4NV6urSrA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.11.12': resolution: {integrity: sha512-N5xF+MDZr79e8gvVXX3YP1bMeaRL16Kst/R7bGUQvvCq1UGD86qMUtSr5KfCl0h5SNKP2YKtkN98HQLnGEikow==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.11.12': resolution: {integrity: sha512-/PYiyYWSQRtMoOamMfhAfq0y3RWk9LpUZ49yetJn2XI85TRkL5u2DTLLNkTPvoTiCfo0eZOJF9t5b7Z6ly0iHQ==} @@ -1383,9 +1394,6 @@ packages: '@types/hash-sum@1.0.2': resolution: {integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw==} - '@types/jsdom@21.1.7': - resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1404,9 +1412,6 @@ packages: '@types/serve-handler@6.1.4': resolution: {integrity: sha512-aXy58tNie0NkuSCY291xUxl0X+kGYy986l4kqW6Gi4kEXgr6Tx0fpSH7YwUSa5usPpG3s9DBeIR6hHcDtL2IvQ==} - '@types/tough-cookie@4.0.5': - resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1487,21 +1492,25 @@ packages: resolution: {integrity: sha512-fp4Azi8kHz6TX8SFmKfyScZrMLfp++uRm2srpqRjsRZIIBzH74NtSkdEUHImR4G7f7XJ+sVZjCc6KDDK04YEpQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/rspack-resolver-binding-linux-arm64-musl@1.2.2': resolution: {integrity: sha512-gMiG3DCFioJxdGBzhlL86KcFgt9HGz0iDhw0YVYPsShItpN5pqIkNrI+L/Q/0gfDiGrfcE0X3VANSYIPmqEAlQ==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/rspack-resolver-binding-linux-x64-gnu@1.2.2': resolution: {integrity: sha512-n/4n2CxaUF9tcaJxEaZm+lqvaw2gflfWQ1R9I7WQgYkKEKbRKbpG/R3hopYdUmLSRI4xaW1Cy0Bz40eS2Yi4Sw==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/rspack-resolver-binding-linux-x64-musl@1.2.2': resolution: {integrity: sha512-cHyhAr6rlYYbon1L2Ag449YCj3p6XMfcYTP0AQX+KkQo025d1y/VFtPWvjMhuEsE2lLvtHm7GdJozj6BOMtzVg==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/rspack-resolver-binding-wasm32-wasi@1.2.2': resolution: {integrity: sha512-eogDKuICghDLGc32FtP+WniG38IB1RcGOGz0G3z8406dUdjJvxfHGuGs/dSlM9YEp/v0lEqhJ4mBu6X2nL9pog==} @@ -2889,9 +2898,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mitata@1.0.34: - resolution: {integrity: sha512-Mc3zrtNBKIMeHSCQ0XqRLo1vbdIx1wvFV9c8NJAiyho6AjNfMY8bVhbS12bwciUdd1t4rj8099CH3N3NFahaUA==} - mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} @@ -4568,12 +4574,6 @@ snapshots: '@types/hash-sum@1.0.2': {} - '@types/jsdom@21.1.7': - dependencies: - '@types/node': 22.13.13 - '@types/tough-cookie': 4.0.5 - parse5: 7.2.1 - '@types/json-schema@7.0.15': {} '@types/node@22.13.13': @@ -4590,8 +4590,6 @@ snapshots: dependencies: '@types/node': 22.13.13 - '@types/tough-cookie@4.0.5': {} - '@types/trusted-types@2.0.7': {} '@types/web-bluetooth@0.0.20': {} @@ -6201,8 +6199,6 @@ snapshots: minipass@7.1.2: {} - mitata@1.0.34: {} - mitt@3.0.1: {} monaco-editor@0.52.2: {} From 34cb64c54c40bf81dae49d7a5d376b839bd93a57 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 2 May 2025 02:24:10 +0800 Subject: [PATCH 18/30] fix(runtime-vapor): add simpleSetCurrentInstance to address performance regression --- packages/runtime-vapor/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 682532fa4d8..87d5c4ac91d 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -42,3 +42,9 @@ export { applyDynamicModel, } from './directives/vModel' export { withVaporDirectives } from './directives/custom' + +/** + * TODO: Somehow remove this line which causes performance regression on select key. + * @internal + */ +export { simpleSetCurrentInstance } from '@vue/runtime-core' From 2b186d36d3ddc615b55b67e28d941153e07da4ab Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 18 Apr 2025 08:14:33 +0800 Subject: [PATCH 19/30] refactor(reactivity): optimize dirty flag checks --- packages/reactivity/src/computed.ts | 7 +++++-- packages/reactivity/src/effect.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index dcaa5b797a1..6e6488cea87 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -93,8 +93,11 @@ export class ComputedRefImpl implements Dependency, Subscriber { */ get _dirty(): boolean { const flags = this.flags - if (flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) { - if (flags & SubscriberFlags.Dirty || checkDirty(this.deps!)) { + if (flags & SubscriberFlags.Dirty) { + return true + } + if (flags & SubscriberFlags.Pending) { + if (checkDirty(this.deps!)) { this.flags = flags | SubscriberFlags.Dirty return true } else { diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index bb5ee0404a4..cf8836fb478 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -171,8 +171,11 @@ export class ReactiveEffect implements ReactiveEffectOptions { get dirty(): boolean { const flags = this.flags - if (flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) { - if (flags & SubscriberFlags.Dirty || checkDirty(this.deps!)) { + if (flags & SubscriberFlags.Dirty) { + return true + } + if (flags & SubscriberFlags.Pending) { + if (checkDirty(this.deps!)) { this.flags = flags | SubscriberFlags.Dirty return true } else { From 46048c010b28a5494aeedb9ea11a8722fe0dc093 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 26 Apr 2025 06:59:28 +0800 Subject: [PATCH 20/30] perf(runtime-vapor): preallocate memory --- packages/runtime-vapor/src/apiCreateFor.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 9df529f995e..e319f1d9593 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -143,16 +143,18 @@ export const createFor = ( } } else { const commonLength = Math.min(oldLength, newLength) - const oldKeyToIndexMap = new Map() + const oldKeyToIndex: [any, number][] = new Array(oldLength) const pendingNews: [ index: number, item: ReturnType, key: any, - ][] = [] + ][] = new Array(newLength) let defaultAnchor: Node = parentAnchor let right = 0 let left = 0 + let l1 = 0 + let l2 = 0 while (right < commonLength) { const index = newLength - right - 1 @@ -179,23 +181,27 @@ export const createFor = ( if (oldKey === key) { update((newBlocks[left] = oldBlock), item[0]) } else { - pendingNews.push([left, item, key]) - oldKeyToIndexMap.set(oldKey, left) + pendingNews[l1++] = [left, item, key] + oldKeyToIndex[l2++] = [oldKey, left] } left++ } for (let i = left; i < oldLength - right; i++) { - oldKeyToIndexMap.set(oldBlocks[i].key, i) + oldKeyToIndex[l2++] = [oldBlocks[i].key, 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]) + pendingNews[l1++] = [i, item, key] } + pendingNews.length = l1 + oldKeyToIndex.length = l2 + + const oldKeyToIndexMap = new Map(oldKeyToIndex) const pendingMounts: [ index: number, item: ReturnType, From 52804c7242993288a17ba31a40b44b565c337400 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 26 Apr 2025 08:06:08 +0800 Subject: [PATCH 21/30] pref(runtime-vapor): add fast path for append rows --- packages/runtime-vapor/src/apiCreateFor.ts | 146 +++++++++++---------- 1 file changed, 77 insertions(+), 69 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index e319f1d9593..de3d18a808b 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -198,84 +198,92 @@ export const createFor = ( pendingNews[l1++] = [i, item, key] } - pendingNews.length = l1 - oldKeyToIndex.length = l2 + if (!l1 && !l2) { + for (let i = prepareLength; i < newLength - right; i++) { + const item = getItem(source, i) + const key = getKey.apply(null, item) + mount(source, i, item, key, defaultAnchor) + } + } else { + pendingNews.length = l1 + oldKeyToIndex.length = l2 + + const oldKeyToIndexMap = new Map(oldKeyToIndex) + 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]) + } + } - const oldKeyToIndexMap = new Map(oldKeyToIndex) - 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), + 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, ) - oldKeyToIndexMap.delete(key) - } else { - pendingMounts.push([index, item, key, anchorIndex]) } - } - 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) - } + for (let i = prepareLength; i < newLength - right; i++) { + const item = getItem(source, i) + const key = getKey.apply(null, item) + moveOrMount(i, item, key, -1) + } - const shouldUseFastRemove = pendingMounts.length === newLength + 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() + for (const i of oldKeyToIndexMap.values()) { + unmount( + oldBlocks[i], + !(shouldUseFastRemove && canUseFastRemove), + !shouldUseFastRemove, + ) } - if (canUseFastRemove) { - parent!.textContent = '' - parent!.appendChild(parentAnchor) + 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), - ) + for (const [index, item, key, anchorIndex] of pendingMounts) { + mount( + source, + index, + item, + key, + anchorIndex === -1 + ? defaultAnchor + : normalizeAnchor(newBlocks[anchorIndex].nodes), + ) + } } } } From 335a450ae988480ea414e70574c4c4b4b39051a2 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sun, 27 Apr 2025 09:18:56 +0800 Subject: [PATCH 22/30] refactor(runtime-core): improve scheduler code reusability --- packages/runtime-core/src/scheduler.ts | 255 ++++++++++++------------- 1 file changed, 118 insertions(+), 137 deletions(-) diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 2b5b8e98ed8..8a58ac9a296 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -1,4 +1,4 @@ -import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling' +import { ErrorCodes, handleError } from './errorHandling' import { isArray } from '@vue/shared' import { type GenericComponentInstance, getComponentName } from './component' @@ -39,23 +39,21 @@ export interface SchedulerJob extends Function { export type SchedulerJobs = SchedulerJob | SchedulerJob[] -const queueMainJobs: (SchedulerJob | undefined)[] = [] -const queuePreJobs: (SchedulerJob | undefined)[] = [] -const queuePostJobs: (SchedulerJob | undefined)[] = [] +const jobs: SchedulerJob[] = [] +const preJobs: SchedulerJob[] = [] -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 postJobs: SchedulerJob[] = [] +let activePostJobs: SchedulerJob[] | null = null let currentFlushPromise: Promise | null = null +let jobsLength = 0 +let preJobsLength = 0 +let flushIndex = 0 +let preFlushIndex = 0 +let postFlushIndex = 0 const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise - const RECURSION_LIMIT = 100 + type CountMap = Map export function nextTick( @@ -75,21 +73,21 @@ 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, isPre: boolean) { - let start = (isPre ? preFlushIndex : mainFlushIndex) + 1 - let end = isPre ? preJobsLength : mainJobsLength - const queue = isPre ? queuePreJobs : queueMainJobs - +function findInsertionIndex( + id: number, + queue: SchedulerJob[], + start: number, + end: number, +) { while (start < end) { const middle = (start + end) >>> 1 - const middleJob = queue[middle]! + const middleJob = queue[middle] if (middleJob.id! <= id) { start = middle + 1 } else { end = middle } } - return start } @@ -97,26 +95,48 @@ function findInsertionIndex(id: number, isPre: boolean) { * @internal for runtime-vapor only */ export function queueJob(job: SchedulerJob, isPre = false): void { + if (isPre) { + if (_queueJob(job, preJobs, preJobsLength, preFlushIndex, -1)) { + preJobsLength++ + queueFlush() + } + } else { + if (_queueJob(job, jobs, jobsLength, flushIndex, Infinity)) { + jobsLength++ + queueFlush() + } + } +} + +function _queueJob( + job: SchedulerJob, + queue: SchedulerJob[], + length: number, + flushIndex: number, + defaultId: number, +) { const flags = job.flags! if (!(flags & SchedulerJobFlags.QUEUED)) { + job.flags! = flags | SchedulerJobFlags.QUEUED if (job.id === undefined) { - job.id = isPre ? -1 : Infinity + job.id = defaultId } - const queueLength = isPre ? preJobsLength : mainJobsLength - const queue = isPre ? queuePreJobs : queueMainJobs if ( - !queueLength || + flushIndex === length || // fast path when the job id is larger than the tail - job.id >= queue[queueLength - 1]!.id! + job.id >= queue[length - 1].id! ) { - queue[queueLength] = job + queue[length] = job } else { - queue.splice(findInsertionIndex(job.id, isPre), 0, job) + queue.splice( + findInsertionIndex(job.id, queue, flushIndex, length), + 0, + job, + ) } - isPre ? preJobsLength++ : mainJobsLength++ - job.flags! = flags | SchedulerJobFlags.QUEUED - queueFlush() + return true } + return false } function queueFlush() { @@ -128,26 +148,19 @@ 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)) { - queuePostJobs[postJobsLength++] = cb - cb.flags! |= SchedulerJobFlags.QUEUED +export function queuePostFlushCb(jobs: SchedulerJobs): void { + if (!isArray(jobs)) { + if (activePostJobs && jobs.id === -1) { + activePostJobs.splice(postFlushIndex, 0, jobs) + } else { + _queueJob(jobs, postJobs, postJobs.length, 0, Infinity) } } 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 - for (const job of cb) { - if (job.id === undefined) { - job.id = Infinity - } - queuePostJobs[postJobsLength++] = job + for (const job of jobs) { + _queueJob(job, postJobs, postJobs.length, 0, Infinity) } } queueFlush() @@ -160,62 +173,45 @@ export function flushPreFlushCbs( if (__DEV__) { seen = seen || new Map() } - 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 - } - queuePreJobs.splice(i, 1) - preJobsLength-- - i-- - if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) { - cb.flags! &= ~SchedulerJobFlags.QUEUED - } - cb() - if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) { - cb.flags! &= ~SchedulerJobFlags.QUEUED - } + for (let i = preFlushIndex; i < preJobsLength; i++) { + const cb = preJobs[i] + if (instance && cb.id !== instance.uid) { + continue + } + if (__DEV__ && checkRecursiveUpdates(seen!, cb)) { + continue + } + preJobs.splice(i, 1) + i-- + preJobsLength-- + if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) { + cb.flags! &= ~SchedulerJobFlags.QUEUED + } + cb() + if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) { + cb.flags! &= ~SchedulerJobFlags.QUEUED } } } export function flushPostFlushCbs(seen?: CountMap): void { - 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!) - + if (postJobs.length) { // #1947 already has active queue, nested flushPostFlushCbs call - if (activePostFlushCbs) { - activePostFlushCbs.push(...sorted) + if (activePostJobs) { + activePostJobs.push(...postJobs) + postJobs.length = 0 return } - activePostFlushCbs = sorted + activePostJobs = postJobs + postJobs = [] + if (__DEV__) { seen = seen || new Map() } - for ( - postFlushIndex = 0; - postFlushIndex < activePostFlushCbs.length; - postFlushIndex++ - ) { - const cb = activePostFlushCbs[postFlushIndex] + while (postFlushIndex < activePostJobs.length) { + const cb = activePostJobs[postFlushIndex++] if (__DEV__ && checkRecursiveUpdates(seen!, cb)) { continue } @@ -230,7 +226,8 @@ export function flushPostFlushCbs(seen?: CountMap): void { } } } - activePostFlushCbs = null + + activePostJobs = null postFlushIndex = 0 } } @@ -250,33 +247,30 @@ export function flushOnAppMount(): void { function flushJobs(seen?: CountMap) { if (__DEV__) { - seen = seen || new Map() + seen ||= new Map() } try { - preFlushIndex = 0 - mainFlushIndex = 0 - - while (preFlushIndex < preJobsLength || mainFlushIndex < mainJobsLength) { + while (preFlushIndex < preJobsLength || flushIndex < jobsLength) { let job: SchedulerJob if (preFlushIndex < preJobsLength) { - if (mainFlushIndex < mainJobsLength) { - const preJob = queuePreJobs[preFlushIndex]! - const mainJob = queueMainJobs[mainFlushIndex]! + if (flushIndex < jobsLength) { + const mainJob = jobs[flushIndex] + const preJob = preJobs[preFlushIndex] if (preJob.id! <= mainJob.id!) { job = preJob - flushingPreJob = true + preJobs[preFlushIndex++] = undefined as any } else { job = mainJob - flushingPreJob = false + jobs[flushIndex++] = undefined as any } } else { - job = queuePreJobs[preFlushIndex]! - flushingPreJob = true + job = preJobs[preFlushIndex] + preJobs[preFlushIndex++] = undefined as any } } else { - job = queueMainJobs[mainFlushIndex]! - flushingPreJob = false + job = jobs[flushIndex] + jobs[flushIndex++] = undefined as any } if (!(job.flags! & SchedulerJobFlags.DISPOSED)) { @@ -286,60 +280,47 @@ function flushJobs(seen?: CountMap) { // 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) { job.flags! &= ~SchedulerJobFlags.QUEUED } - callWithErrorHandling( - job, - job.i, - job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER, - ) - if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) { - job.flags! &= ~SchedulerJobFlags.QUEUED + try { + job() + } catch (err) { + handleError( + err, + job.i, + job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER, + ) + } finally { + if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) { + 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 while (preFlushIndex < preJobsLength) { - const job = queuePreJobs[preFlushIndex] - queuePreJobs[preFlushIndex++] = undefined - if (job) { - job.flags! &= ~SchedulerJobFlags.QUEUED - } + preJobs[preFlushIndex].flags! &= ~SchedulerJobFlags.QUEUED + preJobs[preFlushIndex++] = undefined as any } - while (mainFlushIndex < mainJobsLength) { - const job = queueMainJobs[mainFlushIndex] - queueMainJobs[mainFlushIndex++] = undefined - if (job) { - job.flags! &= ~SchedulerJobFlags.QUEUED - } + while (flushIndex < jobsLength) { + jobs[flushIndex].flags! &= ~SchedulerJobFlags.QUEUED + jobs[flushIndex++] = undefined as any } - preFlushIndex = -1 - mainFlushIndex = -1 + flushIndex = 0 + preFlushIndex = 0 + jobsLength = 0 preJobsLength = 0 - mainJobsLength = 0 - flushingPreJob = false flushPostFlushCbs(seen) currentFlushPromise = null // If new jobs have been added to either queue, keep flushing - if (preJobsLength || mainJobsLength || postJobsLength) { + if (preJobsLength || jobsLength || postJobs.length) { flushJobs(seen) } } From cd123503e5e33c134f936c9001fecb0d47765bf2 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 18 Apr 2025 03:05:57 +0800 Subject: [PATCH 23/30] refactor(reactivity): simplify Effect, EffectScope cleanup behavior --- .../reactivity/__tests__/effectScope.spec.ts | 13 ++- packages/reactivity/src/effect.ts | 109 +++++++++++------- packages/reactivity/src/effectScope.ts | 60 ++++------ packages/reactivity/src/system.ts | 27 +++-- packages/reactivity/src/watch.ts | 10 +- 5 files changed, 118 insertions(+), 101 deletions(-) diff --git a/packages/reactivity/__tests__/effectScope.spec.ts b/packages/reactivity/__tests__/effectScope.spec.ts index 93ba9e89073..a4672f60189 100644 --- a/packages/reactivity/__tests__/effectScope.spec.ts +++ b/packages/reactivity/__tests__/effectScope.spec.ts @@ -30,7 +30,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) @@ -170,17 +171,17 @@ describe('reactivity/effect/scope', () => { 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(getEffectsCount(scope)).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', () => { @@ -359,7 +360,7 @@ describe('reactivity/effect/scope', () => { expect(cleanupCalls).toBe(1) expect(getEffectsCount(scope)).toBe(0) - expect(scope.cleanups.length).toBe(0) + expect(scope.cleanups).toBe(0) }) }) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index cf8836fb478..53bcca1e0a3 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -3,6 +3,7 @@ import type { TrackOpTypes, TriggerOpTypes } from './constants' import { setupOnTrigger } from './debug' import { activeEffectScope } from './effectScope' import { + type Dependency, type Link, type Subscriber, SubscriberFlags, @@ -51,26 +52,24 @@ 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 = 0 + cleanups: number = 0 // Dependency subs: Link | undefined = undefined subsTail: Link | undefined = undefined - /** - * @internal - */ - cleanup?: () => void = undefined - - onStop?: () => void + // dev only onTrack?: (event: DebuggerEvent) => void + // dev only onTrigger?: (event: DebuggerEvent) => void // @ts-expect-error @@ -80,13 +79,13 @@ export class ReactiveEffect implements ReactiveEffectOptions { if (fn !== undefined) { this.fn = fn } - if (activeEffectScope && activeEffectScope.active) { + if (activeEffectScope) { link(this, activeEffectScope) } } get active(): boolean { - return !(this.flags & EffectFlags.STOP) + return this.deps !== undefined } pause(): void { @@ -122,12 +121,10 @@ export class ReactiveEffect implements ReactiveEffectOptions { } run(): T { - let flags = this.flags - if (flags & EffectFlags.STOP) { - // stopped during cleanup - return this.fn() + const cleanups = this.cleanups + if (cleanups) { + cleanup(this, cleanups) } - cleanupEffect(this) const prevSub = activeSub setActiveSub(this) startTracking(this) @@ -143,7 +140,7 @@ export class ReactiveEffect implements ReactiveEffectOptions { } setActiveSub(prevSub) endTracking(this) - flags = this.flags + const flags = this.flags if ( (flags & (SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE)) === (SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE) @@ -155,17 +152,15 @@ export class ReactiveEffect implements ReactiveEffectOptions { } stop(): void { - const flags = this.flags - if (!(flags & EffectFlags.STOP)) { - this.flags = flags | EffectFlags.STOP - startTracking(this) - endTracking(this) - cleanupEffect(this) - this.onStop && this.onStop() - - if (this.subs !== undefined) { - unlink(this.subs) - } + const sub = this.subs + const cleanups = this.cleanups + if (sub !== undefined) { + unlink(sub) + } + startTracking(this) + endTracking(this) + if (cleanups) { + cleanup(this, cleanups) } } @@ -205,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 { @@ -276,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 @@ -289,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` + @@ -299,18 +330,14 @@ export function onEffectCleanup(fn: () => void, failSilently = false): void { } } -function cleanupEffect(e: ReactiveEffect) { - const cleanup = e.cleanup - if (cleanup !== undefined) { - e.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 85ead32b416..2c109c2084f 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -1,9 +1,11 @@ -import { EffectFlags } 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' @@ -15,20 +17,12 @@ export class EffectScope implements Subscriber, Dependency { deps: Link | undefined = undefined depsTail: Link | undefined = undefined flags: number = 0 + cleanups: number = 0 // Dependency subs: Link | undefined = undefined subsTail: Link | undefined = undefined - /** - * @internal - */ - cleanups: (() => void)[] = [] - /** - * @internal - */ - cleanupsLength = 0 - constructor(detached = false) { if (!detached && activeEffectScope) { link(this, activeEffectScope) @@ -36,7 +30,7 @@ export class EffectScope implements Subscriber, Dependency { } get active(): boolean { - return !(this.flags & EffectFlags.STOP) + return this.deps !== undefined } notify(): void {} @@ -70,35 +64,25 @@ export class EffectScope implements Subscriber, Dependency { } run(fn: () => T): T | undefined { - const flags = this.flags - if (!(flags & EffectFlags.STOP)) { - const prevEffectScope = activeEffectScope - try { - activeEffectScope = this - return fn() - } finally { - activeEffectScope = prevEffectScope - } - } else if (__DEV__) { - warn(`cannot run an inactive effect scope.`) + const prevEffectScope = activeEffectScope + try { + activeEffectScope = this + return fn() + } finally { + activeEffectScope = prevEffectScope } } stop(): void { - const flags = this.flags - if (!(flags & EffectFlags.STOP)) { - this.flags = flags | EffectFlags.STOP - while (this.deps !== undefined) { - unlink(this.deps) - } - const l = this.cleanupsLength - for (let i = 0; i < l; i++) { - this.cleanups[i]() - } - this.cleanupsLength = 0 - if (this.subs !== undefined) { - unlink(this.subs) - } + const sub = this.subs + const cleanups = this.cleanups + if (sub !== undefined) { + unlink(sub) + } + startTracking(this) + endTracking(this) + if (cleanups) { + cleanup(this, cleanups) } } } @@ -141,8 +125,8 @@ export function setCurrentScope( * @see {@link https://vuejs.org/api/reactivity-advanced.html#onscopedispose} */ export function onScopeDispose(fn: () => void, failSilently = false): void { - if (activeEffectScope) { - activeEffectScope.cleanups[activeEffectScope.cleanupsLength++] = 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/system.ts b/packages/reactivity/src/system.ts index a50c1a7861d..0baec9fd9d7 100644 --- a/packages/reactivity/src/system.ts +++ b/packages/reactivity/src/system.ts @@ -16,7 +16,7 @@ export interface Subscriber { } export interface Link { - dep: Dependency | Computed | Effect + dep: Dependency | Computed | Effect | EffectScope sub: Subscriber | Computed | Effect | EffectScope prevSub: Link | undefined nextSub: Link | undefined @@ -97,9 +97,11 @@ export function link(dep: Dependency, sub: Subscriber): void { dep.subsTail = newLink } -export function unlink(link: Link): void { +export function unlink( + link: Link, + sub: Subscriber = link.sub, +): Link | undefined { const dep = link.dep - const sub = link.sub const prevDep = link.prevDep const nextDep = link.nextDep const nextSub = link.nextSub @@ -125,16 +127,18 @@ export function unlink(link: Link): void { 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 } - while (dep.deps !== undefined) { - unlink(dep.deps) - } } + return nextDep } export function propagate(current: Link): void { @@ -240,14 +244,9 @@ export function startTracking(sub: Subscriber): void { export function endTracking(sub: Subscriber): void { const depsTail = sub.depsTail - if (depsTail !== undefined) { - while (depsTail.nextDep !== undefined) { - unlink(depsTail.nextDep) - } - } else { - while (sub.deps !== undefined) { - unlink(sub.deps) - } + let toRemove = depsTail !== undefined ? depsTail.nextDep : sub.deps + while (toRemove !== undefined) { + toRemove = unlink(toRemove, sub) } sub.flags &= ~SubscriberFlags.Tracking } diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts index f1f8f26a23d..2c205bd9195 100644 --- a/packages/reactivity/src/watch.ts +++ b/packages/reactivity/src/watch.ts @@ -224,7 +224,7 @@ export function watch( : INITIAL_WATCHER_VALUE const job = (immediateFirstRun?: boolean) => { - if (!effect.active || (!immediateFirstRun && !effect.dirty)) { + if (!immediateFirstRun && !effect.dirty) { return } if (cb) { @@ -281,7 +281,7 @@ export function watch( boundCleanup = fn => onWatcherCleanup(fn, false, effect) - cleanup = effect.onStop = () => { + cleanup = () => { const cleanups = cleanupMap.get(effect) if (cleanups) { if (call) { @@ -315,6 +315,12 @@ export function watch( watchHandle.resume = effect.resume.bind(effect) watchHandle.stop = watchHandle + const stop = effect.stop.bind(effect) + effect.stop = () => { + stop() + cleanup() + } + return watchHandle } From 281e1f94be9fa52d3ad573d36b0700fe7be066d2 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 18 Apr 2025 19:46:53 +0800 Subject: [PATCH 24/30] refactor(runtime-core): pre-refactoring of scheduling rewrite --- .../reactivity/__tests__/effectScope.spec.ts | 5 +- packages/reactivity/src/effect.ts | 2 +- packages/reactivity/src/system.ts | 10 +- packages/reactivity/src/watch.ts | 349 +++++++++--------- .../runtime-core/__tests__/apiWatch.spec.ts | 3 +- .../runtime-core/src/apiAsyncComponent.ts | 2 +- packages/runtime-core/src/component.ts | 5 - .../src/componentPublicInstance.ts | 2 +- .../src/components/BaseTransition.ts | 5 +- packages/runtime-core/src/hmr.ts | 4 +- packages/runtime-core/src/renderer.ts | 100 +++-- .../runtime-vapor/__tests__/apiWatch.spec.ts | 3 +- .../runtime-vapor/__tests__/component.spec.ts | 3 +- 13 files changed, 261 insertions(+), 232 deletions(-) diff --git a/packages/reactivity/__tests__/effectScope.spec.ts b/packages/reactivity/__tests__/effectScope.spec.ts index a4672f60189..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, @@ -89,7 +90,7 @@ describe('reactivity/effect/scope', () => { }) }) - expect(getEffectsCount(scope)).toBe(2) + expect(getEffectsCount(scope)).toBe(1) expect(scope.deps?.nextDep?.dep).toBeInstanceOf(EffectScope) expect(dummy).toBe(0) @@ -367,7 +368,7 @@ describe('reactivity/effect/scope', () => { function getEffectsCount(scope: EffectScope): number { let n = 0 for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) { - if ('notify' in dep.dep) { + if (dep.dep instanceof ReactiveEffect) { n++ } } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 53bcca1e0a3..c26d455807b 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -60,7 +60,7 @@ export class ReactiveEffect // Subscriber deps: Link | undefined = undefined depsTail: Link | undefined = undefined - flags: number = 0 + flags: number = SubscriberFlags.Dirty cleanups: number = 0 // Dependency diff --git a/packages/reactivity/src/system.ts b/packages/reactivity/src/system.ts index 0baec9fd9d7..6e6ecfd6467 100644 --- a/packages/reactivity/src/system.ts +++ b/packages/reactivity/src/system.ts @@ -151,7 +151,7 @@ export function propagate(current: Link): void { const sub = current.sub const subFlags = sub.flags - let shouldNotify = 0 + let shouldNotify = false if ( !( @@ -163,23 +163,23 @@ export function propagate(current: Link): void { ) ) { sub.flags = subFlags | targetFlag - shouldNotify = 1 + shouldNotify = true } else if ( subFlags & SubscriberFlags.Recursed && !(subFlags & SubscriberFlags.Tracking) ) { sub.flags = (subFlags & ~SubscriberFlags.Recursed) | targetFlag - shouldNotify = 1 + shouldNotify = true } else if ( !(subFlags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) && isValidLink(current, sub) ) { sub.flags = subFlags | SubscriberFlags.Recursed | targetFlag - shouldNotify = 2 + shouldNotify = !('notify' in sub) } if (shouldNotify) { - if (shouldNotify === 1 && 'notify' in sub) { + if ('notify' in sub) { notifyBuffer[notifyBufferLength++] = sub } else { const subSubs = (sub as Dependency).subs diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts index 2c205bd9195..bac8e1ba40d 100644 --- a/packages/reactivity/src/watch.ts +++ b/packages/reactivity/src/watch.ts @@ -13,8 +13,9 @@ import type { ComputedRef } from './computed' import { ReactiveFlags } from './constants' import { type DebuggerOptions, - type EffectScheduler, ReactiveEffect, + cleanup, + onCleanup, pauseTracking, resetTracking, } from './effect' @@ -76,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. @@ -100,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` + @@ -114,214 +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 watchHandle: WatchHandle = () => { - effect.stop() - } + super(getter) + this.forceTrigger = forceTrigger + this.isMultiSource = isMultiSource - if (once && cb) { - const _cb = cb - cb = (...args) => { - _cb(...args) - watchHandle() + if (once && cb) { + const _cb = cb + cb = (...args) => { + _cb(...args) + this.stop() + } } - } - let oldValue: any = isMultiSource - ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) - : INITIAL_WATCHER_VALUE + this.cb = cb - const job = (immediateFirstRun?: boolean) => { - if (!immediateFirstRun && !effect.dirty) { - return + 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 } + + // initial run if (cb) { + if (immediate) { + job() + } else { + this.oldValue = this.run() + } + } else if (scheduler) { + scheduler(job, true) + } else { + this.run() + } + } + + scheduler(): void { + if (!this.dirty) { + return + } + 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() - } - } - - if (augmentJob) { - augmentJob(job) - } - - effect = new ReactiveEffect(getter) - - effect.scheduler = scheduler - ? () => scheduler(job, false) - : (job as EffectScheduler) - - boundCleanup = fn => onWatcherCleanup(fn, false, effect) - - cleanup = () => { - const cleanups = cleanupMap.get(effect) - if (cleanups) { - if (call) { - call(cleanups, WatchErrorCodes.WATCH_CLEANUP) - } else { - for (const cleanup of cleanups) cleanup() - } - cleanupMap.delete(effect) + this.run() } } +} - 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() - } - - watchHandle.pause = effect.pause.bind(effect) - watchHandle.resume = effect.resume.bind(effect) - watchHandle.stop = watchHandle +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) +} - const stop = effect.stop.bind(effect) - effect.stop = () => { - stop() - cleanup() - } +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 8912524f4b2..932a762da90 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -27,6 +27,7 @@ import { type DebuggerEvent, type EffectScope, ITERATE_KEY, + ReactiveEffect, type Ref, type ShallowRef, TrackOpTypes, @@ -2015,7 +2016,7 @@ describe('api: watch', () => { function getEffectsCount(scope: EffectScope): number { let n = 0 for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) { - if ('notify' in dep.dep) { + if (dep.dep instanceof ReactiveEffect) { n++ } } 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/component.ts b/packages/runtime-core/src/component.ts index 6074ea42648..43c0877bd2b 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -553,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) */ @@ -717,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, 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..bbd9cd7175e 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -20,7 +20,6 @@ import { PatchFlags, ShapeFlags, isArray, isFunction } from '@vue/shared' import { onBeforeUnmount, onMounted } from '../apiLifecycle' import { isTeleport } from './Teleport' import type { RendererElement } from '../renderer' -import { SchedulerJobFlags } from '../scheduler' type Hook void> = T | T[] @@ -223,8 +222,8 @@ const BaseTransitionImpl: ComponentOptions = { state.isLeaving = false // #6835 // it also needs to be updated when active is undefined - if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) { - instance.update() + if (instance.effect.active) { + 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/renderer.ts b/packages/runtime-core/src/renderer.ts index 86b17c209c1..a60af1c1263 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1304,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 @@ -1313,16 +1313,45 @@ function baseCreateRenderer( } } - const setupRenderEffect: SetupRenderEffectFn = ( - instance, - initialVNode, - container, - anchor, - parentSuspense, - namespace: ElementNamespace, - optimized, - ) => { - const componentUpdateFn = () => { + class SetupRenderEffect extends ReactiveEffect { + constructor( + private instance: ComponentInternalInstance, + private initialVNode: VNode, + private container: RendererElement, + private anchor: RendererNode | null, + private parentSuspense: SuspenseBoundary | null, + private namespace: ElementNamespace, + private optimized: boolean, + ) { + const prevScope = setCurrentScope(instance.scope) + super() + setCurrentScope(prevScope) + + const job: SchedulerJob = (instance.job = this.scheduler.bind(this)) + job.i = instance + job.id = instance.uid + this.scheduler = () => queueJob(job) + + if (__DEV__) { + this.onTrack = instance.rtc + ? e => invokeArrayFns(instance.rtc!, e) + : void 0 + this.onTrigger = instance.rtg + ? e => invokeArrayFns(instance.rtg!, e) + : void 0 + } + } + + fn() { + const { + instance, + initialVNode, + container, + anchor, + parentSuspense, + namespace, + optimized, + } = this if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined const { el, props } = initialVNode @@ -1469,7 +1498,7 @@ function baseCreateRenderer( } // #2458: deference mount-only object parameters to prevent memleaks - initialVNode = container = anchor = null as any + this.initialVNode = this.container = this.anchor = null as any } else { let { next, bu, u, parent, vnode } = instance @@ -1487,7 +1516,7 @@ function baseCreateRenderer( nonHydratedAsyncRoot.asyncDep!.then(() => { // the instance may be destroyed during the time period if (!instance.isUnmounted) { - componentUpdateFn() + this.fn() } }) return @@ -1593,32 +1622,33 @@ function baseCreateRenderer( } } } + } + const setupRenderEffect: SetupRenderEffectFn = ( + instance, + initialVNode, + container, + anchor, + parentSuspense, + namespace: ElementNamespace, + optimized, + ) => { // create reactive effect for rendering - const prevScope = setCurrentScope(instance.scope) - const effect = (instance.effect = new ReactiveEffect(componentUpdateFn)) - setCurrentScope(prevScope) - - const update = (instance.update = effect.run.bind(effect)) - const job: SchedulerJob = (instance.job = effect.scheduler.bind(effect)) - job.i = instance - job.id = instance.uid - effect.scheduler = () => queueJob(job) + const effect = (instance.effect = new SetupRenderEffect( + instance, + initialVNode, + container, + anchor, + parentSuspense, + namespace, + optimized, + )) // allowRecurse // #1801, #2043 component render effects should allow recursive updates toggleRecurse(instance, true) - if (__DEV__) { - effect.onTrack = instance.rtc - ? e => invokeArrayFns(instance.rtc!, e) - : void 0 - effect.onTrigger = instance.rtg - ? e => invokeArrayFns(instance.rtg!, e) - : void 0 - } - - update() + effect.run() } const updateComponentPreRender = ( @@ -2343,7 +2373,7 @@ function baseCreateRenderer( unregisterHMR(instance) } - const { bum, scope, job, subTree, um, m, a } = instance + const { bum, scope, effect, subTree, um, m, a } = instance invalidateMount(m) invalidateMount(a) @@ -2364,9 +2394,9 @@ function baseCreateRenderer( // job may be null if a component is unmounted before its async // setup has resolved. - if (job) { + if (effect) { // so that scheduler will no longer invoke it - job.flags! |= SchedulerJobFlags.DISPOSED + effect.stop() unmount(subTree, instance, parentSuspense, doRemove) } // unmounted hook diff --git a/packages/runtime-vapor/__tests__/apiWatch.spec.ts b/packages/runtime-vapor/__tests__/apiWatch.spec.ts index 76fe78b7963..290c509552c 100644 --- a/packages/runtime-vapor/__tests__/apiWatch.spec.ts +++ b/packages/runtime-vapor/__tests__/apiWatch.spec.ts @@ -1,5 +1,6 @@ import { type EffectScope, + ReactiveEffect, currentInstance, effectScope, nextTick, @@ -341,7 +342,7 @@ describe('apiWatch', () => { function getEffectsCount(scope: EffectScope): number { let n = 0 for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) { - if ('notify' in dep.dep) { + if (dep.dep instanceof ReactiveEffect) { n++ } } diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts index 374aaf6cf43..07699ba0fc4 100644 --- a/packages/runtime-vapor/__tests__/component.spec.ts +++ b/packages/runtime-vapor/__tests__/component.spec.ts @@ -1,5 +1,6 @@ import { type EffectScope, + ReactiveEffect, type Ref, inject, nextTick, @@ -333,7 +334,7 @@ describe('component', () => { function getEffectsCount(scope: EffectScope): number { let n = 0 for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) { - if ('notify' in dep.dep) { + if (dep.dep instanceof ReactiveEffect) { n++ } } From 7a410ad0b34c28fdf37f713c8d1f5e677fdd09e9 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sun, 27 Apr 2025 10:45:01 +0800 Subject: [PATCH 25/30] refactor(runtime-core): reduce the abstraction of base watcher --- packages/reactivity/__tests__/watch.spec.ts | 76 ----------- packages/reactivity/src/index.ts | 5 +- packages/reactivity/src/watch.ts | 123 +++++++---------- .../runtime-core/__tests__/apiWatch.spec.ts | 48 +++++++ packages/runtime-core/src/apiWatch.ts | 126 ++++++++++++------ 5 files changed, 189 insertions(+), 189 deletions(-) diff --git a/packages/reactivity/__tests__/watch.spec.ts b/packages/reactivity/__tests__/watch.spec.ts index 245acfd63be..4b614be2e7a 100644 --- a/packages/reactivity/__tests__/watch.spec.ts +++ b/packages/reactivity/__tests__/watch.spec.ts @@ -3,40 +3,12 @@ import { type Ref, WatchErrorCodes, type WatchOptions, - type WatchScheduler, computed, onWatcherCleanup, ref, watch, } from '../src' -const queue: (() => void)[] = [] - -// a simple scheduler for testing purposes -let isFlushPending = false -const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise -const nextTick = (fn?: () => any) => - fn ? resolvedPromise.then(fn) : resolvedPromise - -const scheduler: WatchScheduler = (job, isFirstRun) => { - if (isFirstRun) { - job() - } else { - queue.push(job) - flushJobs() - } -} - -const flushJobs = () => { - if (isFlushPending) return - isFlushPending = true - resolvedPromise.then(() => { - queue.forEach(job => job()) - queue.length = 0 - isFlushPending = false - }) -} - describe('watch', () => { test('effect', () => { let dummy: any @@ -147,54 +119,6 @@ describe('watch', () => { expect(dummy).toBe(30) }) - test('nested calls to baseWatch and onWatcherCleanup', async () => { - let calls: string[] = [] - let source: Ref - let copyist: Ref - const scope = new EffectScope() - - scope.run(() => { - source = ref(0) - copyist = ref(0) - // sync by default - watch( - () => { - const current = (copyist.value = source.value) - onWatcherCleanup(() => calls.push(`sync ${current}`)) - }, - null, - {}, - ) - // with scheduler - watch( - () => { - const current = copyist.value - onWatcherCleanup(() => calls.push(`post ${current}`)) - }, - null, - { scheduler }, - ) - }) - - await nextTick() - expect(calls).toEqual([]) - - scope.run(() => source.value++) - expect(calls).toEqual(['sync 0']) - await nextTick() - expect(calls).toEqual(['sync 0', 'post 0']) - calls.length = 0 - - scope.run(() => source.value++) - expect(calls).toEqual(['sync 1']) - await nextTick() - expect(calls).toEqual(['sync 1', 'post 1']) - calls.length = 0 - - scope.stop() - expect(calls).toEqual(['sync 2', 'post 2']) - }) - test('once option should be ignored by simple watch', async () => { let dummy: any const source = ref(0) diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 509152663da..5eaad3ff268 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -90,8 +90,11 @@ export { traverse, onWatcherCleanup, WatchErrorCodes, + /** + * @internal + */ + WatcherEffect, type WatchOptions, - type WatchScheduler, type WatchStopHandle, type WatchHandle, type WatchEffect, diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts index bac8e1ba40d..1fc279a78db 100644 --- a/packages/reactivity/src/watch.ts +++ b/packages/reactivity/src/watch.ts @@ -48,12 +48,7 @@ export interface WatchOptions extends DebuggerOptions { immediate?: Immediate deep?: boolean | number once?: boolean - scheduler?: WatchScheduler onWarn?: (msg: string, ...args: any[]) => void - /** - * @internal - */ - augmentJob?: (job: (...args: any[]) => void) => void /** * @internal */ @@ -75,8 +70,6 @@ export interface WatchHandle extends WatchStopHandle { // initial value for watchers to trigger on undefined initial values const INITIAL_WATCHER_VALUE = {} -export type WatchScheduler = (job: () => void, isFirstRun: boolean) => void - let activeWatcher: WatcherEffect | undefined = undefined /** @@ -117,7 +110,7 @@ export function onWatcherCleanup( } } -class WatcherEffect extends ReactiveEffect { +export class WatcherEffect extends ReactiveEffect { forceTrigger: boolean isMultiSource: boolean oldValue: any @@ -129,8 +122,7 @@ class WatcherEffect extends ReactiveEffect { public cb?: WatchCallback | null | undefined, public options: WatchOptions = EMPTY_OBJ, ) { - const { immediate, deep, once, scheduler, augmentJob, call, onWarn } = - options + const { deep, once, call, onWarn } = options let getter: () => any let forceTrigger = false @@ -216,79 +208,53 @@ class WatcherEffect extends ReactiveEffect { ? 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 } - - // initial run - if (cb) { - if (immediate) { - job() - } else { - this.oldValue = this.run() - } - } else if (scheduler) { - scheduler(job, true) - } else { - this.run() - } } scheduler(): void { if (!this.dirty) { return } - if (this.cb) { - // watch(source, cb) - const newValue = this.run() - const { deep, call } = this.options - if ( - deep || - 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 (this.cleanups) { - cleanup(this, this.cleanups) - } - const currentWatcher = activeWatcher - activeWatcher = this - try { - const args = [ - newValue, - // pass undefined as the old value when it's changed for the first time - this.oldValue === INITIAL_WATCHER_VALUE - ? undefined - : this.isMultiSource && this.oldValue[0] === INITIAL_WATCHER_VALUE - ? [] - : this.oldValue, - this.boundCleanup, - ] - call - ? call(this.cb, WatchErrorCodes.WATCH_CALLBACK, args) - : // @ts-expect-error - this.cb(...args) - this.oldValue = newValue - } finally { - activeWatcher = currentWatcher - } + const newValue = this.run() + if (!this.cb) { + return + } + const { deep, call } = this.options + if ( + deep || + 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 (this.cleanups) { + cleanup(this, this.cleanups) + } + const currentWatcher = activeWatcher + activeWatcher = this + try { + const args = [ + newValue, + // pass undefined as the old value when it's changed for the first time + this.oldValue === INITIAL_WATCHER_VALUE + ? undefined + : this.isMultiSource && this.oldValue[0] === INITIAL_WATCHER_VALUE + ? [] + : this.oldValue, + this.boundCleanup, + ] + call + ? call(this.cb, WatchErrorCodes.WATCH_CALLBACK, args) + : // @ts-expect-error + this.cb(...args) + this.oldValue = newValue + } finally { + activeWatcher = currentWatcher } - } else { - // watchEffect - this.run() } } } @@ -318,10 +284,23 @@ export function watch( options: WatchOptions = EMPTY_OBJ, ): WatchHandle { const effect = new WatcherEffect(source, cb, options) + + // initial run + if (cb) { + if (options.immediate) { + effect.scheduler() + } else { + effect.oldValue = effect.run() + } + } else { + effect.run() + } + 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 } diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 932a762da90..34849105167 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -505,6 +505,54 @@ describe('api: watch', () => { expect(cleanupWatch).toHaveBeenCalledTimes(2) }) + it('nested calls to baseWatch and onWatcherCleanup', async () => { + let calls: string[] = [] + let source: Ref + let copyist: Ref + const scope = effectScope() + + scope.run(() => { + source = ref(0) + copyist = ref(0) + // sync flush + watch( + () => { + const current = (copyist.value = source.value) + onWatcherCleanup(() => calls.push(`sync ${current}`)) + }, + null, + { flush: 'sync' }, + ) + // post flush + watch( + () => { + const current = copyist.value + onWatcherCleanup(() => calls.push(`post ${current}`)) + }, + null, + { flush: 'post' }, + ) + }) + + await nextTick() + expect(calls).toEqual([]) + + scope.run(() => source.value++) + expect(calls).toEqual(['sync 0']) + await nextTick() + expect(calls).toEqual(['sync 0', 'post 0']) + calls.length = 0 + + scope.run(() => source.value++) + expect(calls).toEqual(['sync 1']) + await nextTick() + expect(calls).toEqual(['sync 1', 'post 1']) + calls.length = 0 + + scope.stop() + expect(calls).toEqual(['sync 2', 'post 2']) + }) + it('flush timing: pre (default)', async () => { const count = ref(0) const count2 = ref(0) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index ec2a5a2482c..c475c997ab3 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -1,17 +1,19 @@ import { type WatchOptions as BaseWatchOptions, type DebuggerOptions, + EffectFlags, type ReactiveMarker, type WatchCallback, type WatchEffect, type WatchHandle, type WatchSource, - watch as baseWatch, + WatcherEffect, } from '@vue/reactivity' import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler' import { EMPTY_OBJ, NOOP, extend, isFunction, isString } from '@vue/shared' import { type ComponentInternalInstance, + type GenericComponentInstance, currentInstance, isInSSRComponentSetup, setCurrentInstance, @@ -86,7 +88,7 @@ export type MultiWatchSources = (WatchSource | object)[] // overload: single source + cb export function watch = false>( source: WatchSource, - cb: WatchCallback>, + cb: WatchCallback> | null, options?: WatchOptions, ): WatchHandle @@ -96,9 +98,11 @@ export function watch< Immediate extends Readonly = false, >( sources: readonly [...T] | T, - cb: [T] extends [ReactiveMarker] - ? WatchCallback> - : WatchCallback, MapSources>, + cb: + | ([T] extends [ReactiveMarker] + ? WatchCallback> + : WatchCallback, MapSources>) + | null, options?: WatchOptions, ): WatchHandle @@ -108,7 +112,7 @@ export function watch< Immediate extends Readonly = false, >( sources: [...T], - cb: WatchCallback, MapSources>, + cb: WatchCallback, MapSources> | null, options?: WatchOptions, ): WatchHandle @@ -118,17 +122,17 @@ export function watch< Immediate extends Readonly = false, >( source: T, - cb: WatchCallback>, + cb: WatchCallback> | null, options?: WatchOptions, ): WatchHandle // implementation export function watch = false>( source: T | WatchSource, - cb: any, + cb: WatchCallback | null, options?: WatchOptions, ): WatchHandle { - if (__DEV__ && !isFunction(cb)) { + if (__DEV__ && cb && !isFunction(cb)) { warn( `\`watch(fn, options?)\` signature has been moved to a separate API. ` + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + @@ -138,12 +142,60 @@ export function watch = false>( return doWatch(source as any, cb, options) } +class RenderWatcherEffect extends WatcherEffect { + job: SchedulerJob + + constructor( + public instance: GenericComponentInstance | null, + source: WatchSource | WatchSource[] | WatchEffect | object, + cb: WatchCallback | null, + options: BaseWatchOptions, + public flush: 'pre' | 'post' | 'sync', + ) { + super(source, cb, options) + + // important: mark the job as a watcher callback so that scheduler knows + // it is allowed to self-trigger (#1727) + if (cb) { + this.flags |= EffectFlags.ALLOW_RECURSE + } + + const job: SchedulerJob = this.scheduler.bind(this) + // important: mark the job as a watcher callback so that scheduler knows + // it is allowed to self-trigger (#1727) + if (cb) { + job.flags! |= SchedulerJobFlags.ALLOW_RECURSE + } + if (flush === 'pre' && instance) { + job.id = instance.uid + ;(job as SchedulerJob).i = instance + } + this.job = job + } + + notify(): void { + const flags = this.flags + if (!(flags & EffectFlags.PAUSED)) { + const flush = this.flush + if (flush === 'post') { + queuePostRenderEffect(this.job, this.instance && this.instance.suspense) + } else if (flush === 'pre') { + queueJob(this.job, true) + } else { + this.scheduler() + } + } else { + this.flags = flags | EffectFlags.NOTIFIED + } + } +} + function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, options: WatchOptions = EMPTY_OBJ, ): WatchHandle { - const { immediate, deep, flush, once } = options + const { immediate, deep, flush = 'pre', once } = options if (__DEV__ && !cb) { if (immediate !== undefined) { @@ -190,47 +242,41 @@ function doWatch( baseWatchOptions.call = (fn, type, args) => callWithAsyncErrorHandling(fn, instance, type, args) - // scheduler - let isPre = false - if (flush === 'post') { - baseWatchOptions.scheduler = job => { - queuePostRenderEffect(job, instance && instance.suspense) - } - } else if (flush !== 'sync') { - // default: 'pre' - isPre = true - baseWatchOptions.scheduler = (job, isFirstRun) => { - if (isFirstRun) { - job() - } else { - queueJob(job, true) - } - } - } + const effect = new RenderWatcherEffect( + instance, + source, + cb, + baseWatchOptions, + flush, + ) - baseWatchOptions.augmentJob = (job: SchedulerJob) => { - // important: mark the job as a watcher callback so that scheduler knows - // it is allowed to self-trigger (#1727) - if (cb) { - job.flags! |= SchedulerJobFlags.ALLOW_RECURSE - } - if (isPre && instance) { - job.id = instance.uid - ;(job as SchedulerJob).i = instance + // initial run + if (cb) { + if (options.immediate) { + effect.scheduler() + } else { + effect.oldValue = effect.run() } + } else if (flush === 'post') { + queuePostRenderEffect(effect.job, instance && instance.suspense) + } else { + effect.scheduler() } - const watchHandle = baseWatch(source, cb, baseWatchOptions) + const stop = effect.stop.bind(effect) as WatchHandle + stop.pause = effect.pause.bind(effect) + stop.resume = effect.resume.bind(effect) + stop.stop = stop if (__SSR__ && isInSSRComponentSetup) { if (ssrCleanup) { - ssrCleanup.push(watchHandle) + ssrCleanup.push(stop) } else if (runsImmediately) { - watchHandle() + stop() } } - return watchHandle + return stop } // this.$watch From 7a161f12fab7da5c51606f2c25feb49cd0695bfa Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 28 Apr 2025 20:18:59 +0800 Subject: [PATCH 26/30] perf(reactivity): use fast path for notify a single effect --- packages/reactivity/src/computed.ts | 16 +++++-- packages/reactivity/src/system.ts | 67 ++++++++++++----------------- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 6e6488cea87..1f7d27316ee 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -17,7 +17,7 @@ import { checkDirty, endTracking, link, - processComputedUpdate, + shallowPropagate, startTracking, } from './system' import { warn } from './warning' @@ -138,8 +138,18 @@ export class ComputedRefImpl implements Dependency, Subscriber { get value(): T { const flags = this.flags - if (flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) { - processComputedUpdate(this, flags) + if ( + flags & SubscriberFlags.Dirty || + (flags & SubscriberFlags.Pending && checkDirty(this.deps!)) + ) { + if (this.update()) { + const subs = this.subs + if (subs !== undefined) { + shallowPropagate(subs) + } + } + } else if (flags & SubscriberFlags.Pending) { + this.flags = flags & ~SubscriberFlags.Pending } if (activeSub !== undefined) { if (__DEV__) { diff --git a/packages/reactivity/src/system.ts b/packages/reactivity/src/system.ts index 6e6ecfd6467..d3882e38c64 100644 --- a/packages/reactivity/src/system.ts +++ b/packages/reactivity/src/system.ts @@ -47,7 +47,7 @@ export function startBatch(): void { } export function endBatch(): void { - if (!--batchDepth) { + if (!--batchDepth && notifyBufferLength) { processEffectNotifications() } } @@ -62,39 +62,38 @@ export function link(dep: Dependency, sub: Subscriber): void { sub.depsTail = nextDep return } - const depLastSub = dep.subsTail + const prevSub = dep.subsTail if ( - depLastSub !== undefined && - depLastSub.sub === sub && - isValidLink(depLastSub, sub) + prevSub !== undefined && + prevSub.sub === sub && + isValidLink(prevSub, sub) ) { return } - const newLink: Link = { - dep, - sub, - prevDep, - nextDep, - prevSub: undefined, - nextSub: undefined, + const newLink = + (sub.depsTail = + dep.subsTail = + { + dep, + sub, + prevDep, + nextDep, + prevSub, + nextSub: undefined, + }) + if (nextDep !== undefined) { + nextDep.prevDep = newLink } if (prevDep === undefined) { sub.deps = newLink } else { prevDep.nextDep = newLink } - if (dep.subs === undefined) { + if (prevSub === undefined) { dep.subs = newLink } else { - const oldTail = dep.subsTail! - newLink.prevSub = oldTail - oldTail.nextSub = newLink - } - if (nextDep !== undefined) { - nextDep.prevDep = newLink + prevSub.nextSub = newLink } - sub.depsTail = newLink - dep.subsTail = newLink } export function unlink( @@ -180,7 +179,11 @@ export function propagate(current: Link): void { if (shouldNotify) { if ('notify' in sub) { - notifyBuffer[notifyBufferLength++] = sub + if (!batchDepth && !notifyBufferLength && next === undefined) { + sub.notify() + } else { + notifyBuffer[notifyBufferLength++] = sub + } } else { const subSubs = (sub as Dependency).subs if (subSubs !== undefined) { @@ -225,7 +228,7 @@ export function propagate(current: Link): void { break } while (true) - if (!batchDepth) { + if (!batchDepth && notifyBufferLength) { processEffectNotifications() } } @@ -251,22 +254,6 @@ export function endTracking(sub: Subscriber): void { sub.flags &= ~SubscriberFlags.Tracking } -export function processComputedUpdate( - computed: Computed, - flags: SubscriberFlags, -): void { - if (flags & SubscriberFlags.Dirty || checkDirty(computed.deps!)) { - if (computed.update()) { - const subs = computed.subs - if (subs !== undefined) { - shallowPropagate(subs) - } - } - } else { - computed.flags = flags & ~SubscriberFlags.Pending - } -} - export function processEffectNotifications(): void { while (notifyIndex < notifyBufferLength) { const effect = notifyBuffer[notifyIndex]! @@ -348,7 +335,7 @@ export function checkDirty(current: Link): boolean { } while (true) } -function shallowPropagate(link: Link): void { +export function shallowPropagate(link: Link): void { do { const sub = link.sub const subFlags = sub.flags From 8e2d14021c5c820a9f5ece114b01cad9f7a87551 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 2 May 2025 22:03:04 +0800 Subject: [PATCH 27/30] refactor(reactivity): fix cleanup performance regression --- .../reactivity/__tests__/effectScope.spec.ts | 2 +- packages/reactivity/src/effect.ts | 56 ++++++++----------- packages/reactivity/src/effectScope.ts | 22 +++++--- packages/reactivity/src/watch.ts | 14 ++--- 4 files changed, 43 insertions(+), 51 deletions(-) diff --git a/packages/reactivity/__tests__/effectScope.spec.ts b/packages/reactivity/__tests__/effectScope.spec.ts index 6e42ef94ecb..93ee648e2df 100644 --- a/packages/reactivity/__tests__/effectScope.spec.ts +++ b/packages/reactivity/__tests__/effectScope.spec.ts @@ -361,7 +361,7 @@ describe('reactivity/effect/scope', () => { expect(cleanupCalls).toBe(1) expect(getEffectsCount(scope)).toBe(0) - expect(scope.cleanups).toBe(0) + expect(scope.cleanupsLength).toBe(0) }) }) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index c26d455807b..aa16f43f46c 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -61,12 +61,20 @@ export class ReactiveEffect deps: Link | undefined = undefined depsTail: Link | undefined = undefined flags: number = SubscriberFlags.Dirty - cleanups: number = 0 // Dependency subs: Link | undefined = undefined subsTail: Link | undefined = undefined + /** + * @internal + */ + cleanups: (() => void)[] = [] + /** + * @internal + */ + cleanupsLength = 0 + // dev only onTrack?: (event: DebuggerEvent) => void // dev only @@ -85,7 +93,7 @@ export class ReactiveEffect } get active(): boolean { - return this.deps !== undefined + return !!this.flags || this.deps !== undefined } pause(): void { @@ -121,10 +129,7 @@ export class ReactiveEffect } run(): T { - const cleanups = this.cleanups - if (cleanups) { - cleanup(this, cleanups) - } + cleanup(this) const prevSub = activeSub setActiveSub(this) startTracking(this) @@ -153,15 +158,13 @@ export class ReactiveEffect stop(): void { const sub = this.subs - const cleanups = this.cleanups if (sub !== undefined) { unlink(sub) } startTracking(this) endTracking(this) - if (cleanups) { - cleanup(this, cleanups) - } + cleanup(this) + this.flags = 0 } get dirty(): boolean { @@ -280,30 +283,16 @@ 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, + sub: Subscriber & { cleanups: (() => void)[]; cleanupsLength: number }, ): void { - const cbs = cleanupCbs.get(sub)! - for (let i = 0; i < length; ++i) { - cbs[i]() + const l = sub.cleanupsLength + if (l) { + for (let i = 0; i < l; i++) { + sub.cleanups[i]() + } + sub.cleanupsLength = 0 } - sub.cleanups = 0 } /** @@ -319,9 +308,8 @@ export function cleanup( * an active effect. */ export function onEffectCleanup(fn: () => void, failSilently = false): void { - const e = activeSub - if (e instanceof ReactiveEffect) { - onCleanup(e, () => cleanupEffect(fn)) + if (activeSub instanceof ReactiveEffect) { + activeSub.cleanups[activeSub.cleanupsLength++] = () => cleanupEffect(fn) } else if (__DEV__ && !failSilently) { warn( `onEffectCleanup() was called when there was no active effect` + diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index 2c109c2084f..aa1b6b97553 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -1,4 +1,4 @@ -import { EffectFlags, cleanup, onCleanup } from './effect' +import { EffectFlags, cleanup } from './effect' import { type Dependency, type Link, @@ -17,12 +17,20 @@ export class EffectScope implements Subscriber, Dependency { deps: Link | undefined = undefined depsTail: Link | undefined = undefined flags: number = 0 - cleanups: number = 0 // Dependency subs: Link | undefined = undefined subsTail: Link | undefined = undefined + /** + * @internal + */ + cleanups: (() => void)[] = [] + /** + * @internal + */ + cleanupsLength = 0 + constructor(detached = false) { if (!detached && activeEffectScope) { link(this, activeEffectScope) @@ -30,7 +38,7 @@ export class EffectScope implements Subscriber, Dependency { } get active(): boolean { - return this.deps !== undefined + return !!this.flags || this.deps !== undefined } notify(): void {} @@ -75,15 +83,13 @@ export class EffectScope implements Subscriber, Dependency { stop(): void { const sub = this.subs - const cleanups = this.cleanups if (sub !== undefined) { unlink(sub) } startTracking(this) endTracking(this) - if (cleanups) { - cleanup(this, cleanups) - } + cleanup(this) + this.flags = 0 } } @@ -126,7 +132,7 @@ export function setCurrentScope( */ export function onScopeDispose(fn: () => void, failSilently = false): void { if (activeEffectScope !== undefined) { - onCleanup(activeEffectScope, fn) + activeEffectScope.cleanups[activeEffectScope.cleanupsLength++] = fn } else if (__DEV__ && !failSilently) { warn( `onScopeDispose() is called when there is no active effect scope` + diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts index 1fc279a78db..54c1b74afd1 100644 --- a/packages/reactivity/src/watch.ts +++ b/packages/reactivity/src/watch.ts @@ -15,7 +15,6 @@ import { type DebuggerOptions, ReactiveEffect, cleanup, - onCleanup, pauseTracking, resetTracking, } from './effect' @@ -98,9 +97,10 @@ export function onWatcherCleanup( if (owner) { const { call } = owner.options if (call) { - onCleanup(owner, () => call(cleanupFn, WatchErrorCodes.WATCH_CLEANUP)) + owner.cleanups[owner.cleanupsLength++] = () => + call(cleanupFn, WatchErrorCodes.WATCH_CLEANUP) } else { - onCleanup(owner, cleanupFn) + owner.cleanups[owner.cleanupsLength++] = cleanupFn } } else if (__DEV__ && !failSilently) { warn( @@ -158,10 +158,10 @@ export class WatcherEffect extends ReactiveEffect { } else { // no cb -> simple effect getter = () => { - if (this.cleanups) { + if (this.cleanupsLength) { pauseTracking() try { - cleanup(this, this.cleanups) + cleanup(this) } finally { resetTracking() } @@ -231,9 +231,7 @@ export class WatcherEffect extends ReactiveEffect { : hasChanged(newValue, this.oldValue)) ) { // cleanup before running cb again - if (this.cleanups) { - cleanup(this, this.cleanups) - } + cleanup(this) const currentWatcher = activeWatcher activeWatcher = this try { From 0ab95b7fa13f361457506ac0e3631b922cc19648 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 3 May 2025 01:00:01 +0800 Subject: [PATCH 28/30] refactor(runtime-vapor): replace apply with spread operator for key extraction --- packages/runtime-vapor/src/apiCreateFor.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index de3d18a808b..b2000306f51 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -159,7 +159,7 @@ export const createFor = ( while (right < commonLength) { const index = newLength - right - 1 const item = getItem(source, index) - const key = getKey.apply(null, item) + const key = getKey(...item) const block = oldBlocks[oldLength - right - 1] if (block.key === key) { update(block, ...item) @@ -175,7 +175,7 @@ export const createFor = ( while (left < commonLength - right) { const item = getItem(source, left) - const key = getKey.apply(null, item) + const key = getKey(...item) const oldBlock = oldBlocks[left] const oldKey = oldBlock.key if (oldKey === key) { @@ -194,14 +194,14 @@ export const createFor = ( 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) + const key = getKey(...item) pendingNews[l1++] = [i, item, key] } if (!l1 && !l2) { for (let i = prepareLength; i < newLength - right; i++) { const item = getItem(source, i) - const key = getKey.apply(null, item) + const key = getKey(...item) mount(source, i, item, key, defaultAnchor) } } else { @@ -250,7 +250,7 @@ export const createFor = ( for (let i = prepareLength; i < newLength - right; i++) { const item = getItem(source, i) - const key = getKey.apply(null, item) + const key = getKey(...item) moveOrMount(i, item, key, -1) } From b4c4fef1c8cec7491529cf957a11bd4444040640 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 3 May 2025 03:04:56 +0800 Subject: [PATCH 29/30] refactor(runtime-core): update queueJob to accept order parameter for job prioritization --- .../runtime-core/__tests__/scheduler.spec.ts | 144 +++++++----------- packages/runtime-core/src/apiWatch.ts | 11 +- .../runtime-core/src/components/KeepAlive.ts | 66 ++++---- .../runtime-core/src/components/Suspense.ts | 3 +- .../runtime-core/src/components/Teleport.ts | 46 +++--- packages/runtime-core/src/hydration.ts | 14 +- packages/runtime-core/src/renderer.ts | 80 ++++++---- .../runtime-core/src/rendererTemplateRef.ts | 4 +- packages/runtime-core/src/scheduler.ts | 105 +++++-------- packages/runtime-vapor/src/apiTemplateRef.ts | 3 +- packages/runtime-vapor/src/renderEffect.ts | 3 +- 11 files changed, 235 insertions(+), 244 deletions(-) diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index 39a316fcbb3..90b27eaf4e3 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -49,8 +49,8 @@ describe('scheduler', () => { const job1 = () => { calls.push('job1') - queueJob(job2) - queueJob(job3) + queueJob(job2, 10) + queueJob(job3, 1) } const job2 = () => { @@ -58,12 +58,10 @@ describe('scheduler', () => { queueJob(job4) queueJob(job5) } - job2.id = 10 const job3 = () => { calls.push('job3') } - job3.id = 1 const job4 = () => { calls.push('job4') @@ -126,7 +124,7 @@ describe('scheduler', () => { queueJob(job1) } - queueJob(cb1, true) + queueJob(cb1, undefined, true) await nextTick() expect(calls).toEqual(['cb1', 'job1']) }) @@ -136,27 +134,23 @@ describe('scheduler', () => { const job1 = () => { calls.push('job1') } - job1.id = 1 const cb1: SchedulerJob = () => { calls.push('cb1') - queueJob(job1) + queueJob(job1, 1) // cb2 should execute before the job - queueJob(cb2, true) - queueJob(cb3, true) + queueJob(cb2, 1, true) + queueJob(cb3, 1, true) } const cb2: SchedulerJob = () => { calls.push('cb2') } - cb2.id = 1 - const cb3: SchedulerJob = () => { calls.push('cb3') } - cb3.id = 1 - queueJob(cb1, true) + queueJob(cb1, undefined, true) await nextTick() expect(calls).toEqual(['cb1', 'cb2', 'cb3', 'job1']) }) @@ -166,36 +160,30 @@ describe('scheduler', () => { const job1: SchedulerJob = () => { calls.push('job1') } - job1.id = 1 const job2: SchedulerJob = () => { calls.push('job2') - queueJob(job5) - queueJob(job6, true) + queueJob(job5, 2) + queueJob(job6, 2, true) } - job2.id = 2 const job3: SchedulerJob = () => { calls.push('job3') } - job3.id = 2 const job4: SchedulerJob = () => { calls.push('job4') } - job4.id = 3 const job5: SchedulerJob = () => { calls.push('job5') } - job5.id = 2 const job6: SchedulerJob = () => { calls.push('job6') } - job6.id = 2 // We need several jobs to test this properly, otherwise // findInsertionIndex can yield the correct index by chance - queueJob(job4, true) - queueJob(job2, true) - queueJob(job3, true) - queueJob(job1, true) + queueJob(job4, 3, true) + queueJob(job2, 2, true) + queueJob(job3, 2, true) + queueJob(job1, 1, true) await nextTick() expect(calls).toEqual(['job1', 'job2', 'job3', 'job6', 'job5', 'job4']) @@ -208,8 +196,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, true) - queueJob(cb2, true) + queueJob(cb1, undefined, true) + queueJob(cb2, undefined, true) flushPreFlushCbs() calls.push('job1') } @@ -231,14 +219,13 @@ describe('scheduler', () => { const calls: string[] = [] const job1: SchedulerJob = () => { calls.push('job1') - queueJob(job3, true) - queueJob(job4, true) + queueJob(job3, undefined, true) + queueJob(job4, undefined, true) } // job1 has no id const job2: SchedulerJob = () => { calls.push('job2') } - job2.id = 1 const job3: SchedulerJob = () => { calls.push('job3') } @@ -248,8 +235,8 @@ describe('scheduler', () => { } // job4 has no id - queueJob(job1, true) - queueJob(job2, true) + queueJob(job1, undefined, true) + queueJob(job2, 1, true) await nextTick() expect(calls).toEqual(['job1', 'job3', 'job4', 'job2']) }) @@ -259,7 +246,7 @@ describe('scheduler', () => { const spy = vi.fn() const cb: SchedulerJob = () => spy() queuePostFlushCb(() => { - queueJob(cb, true) + queueJob(cb, undefined, true) }) await nextTick() expect(spy).toHaveBeenCalled() @@ -432,16 +419,13 @@ describe('scheduler', () => { const job1: SchedulerJob = () => { calls.push('job1') } - job1.id = 1 - const job2: SchedulerJob = () => { calls.push('job2') } - job2.id = 2 queuePostFlushCb(() => { - queueJob(job2) - queueJob(job1) + queueJob(job2, 2) + queueJob(job1, 1) }) await nextTick() @@ -455,19 +439,16 @@ describe('scheduler', () => { const job1 = () => calls.push('job1') // job1 has no id const job2 = () => calls.push('job2') - job2.id = 2 const job3 = () => calls.push('job3') - job3.id = 1 const job4: SchedulerJob = () => calls.push('job4') - job4.id = 2 const job5: SchedulerJob = () => calls.push('job5') // job5 has no id queueJob(job1) - queueJob(job2) - queueJob(job3) - queueJob(job4, true) - queueJob(job5, true) + queueJob(job2, 2) + queueJob(job3, 1) + queueJob(job4, 2, true) + queueJob(job5, undefined, true) await nextTick() expect(calls).toEqual(['job5', 'job3', 'job4', 'job2', 'job1']) }) @@ -477,13 +458,11 @@ describe('scheduler', () => { const cb1 = () => calls.push('cb1') // cb1 has no id const cb2 = () => calls.push('cb2') - cb2.id = 2 const cb3 = () => calls.push('cb3') - cb3.id = 1 queuePostFlushCb(cb1) - queuePostFlushCb(cb2) - queuePostFlushCb(cb3) + queuePostFlushCb(cb2, 2) + queuePostFlushCb(cb3, 1) await nextTick() expect(calls).toEqual(['cb3', 'cb2', 'cb1']) }) @@ -532,13 +511,10 @@ describe('scheduler', () => { throw err } }) - job1.id = 1 - const job2: SchedulerJob = vi.fn() - job2.id = 2 - queueJob(job1) - queueJob(job2) + queueJob(job1, 1) + queueJob(job2, 2) try { await nextTick() @@ -552,8 +528,8 @@ describe('scheduler', () => { expect(job1).toHaveBeenCalledTimes(1) expect(job2).toHaveBeenCalledTimes(0) - queueJob(job1) - queueJob(job2) + queueJob(job1, 1) + queueJob(job2, 2) await nextTick() @@ -604,11 +580,10 @@ describe('scheduler', () => { test('recursive jobs can only be queued once non-recursively', async () => { const job: SchedulerJob = vi.fn() - job.id = 1 job.flags = SchedulerJobFlags.ALLOW_RECURSE - queueJob(job) - queueJob(job) + queueJob(job, 1) + queueJob(job, 1) await nextTick() @@ -620,15 +595,14 @@ describe('scheduler', () => { const job: SchedulerJob = vi.fn(() => { if (recurse) { - queueJob(job) - queueJob(job) + queueJob(job, 1) + queueJob(job, 1) recurse = false } }) - job.id = 1 job.flags = SchedulerJobFlags.ALLOW_RECURSE - queueJob(job) + queueJob(job, 1) await nextTick() @@ -641,22 +615,19 @@ describe('scheduler', () => { const job1: SchedulerJob = () => { if (recurse) { // job2 is already queued, so this shouldn't do anything - queueJob(job2) + queueJob(job2, 2) recurse = false } } - job1.id = 1 - const job2: SchedulerJob = vi.fn(() => { if (recurse) { - queueJob(job1) - queueJob(job2) + queueJob(job1, 1) + queueJob(job2, 2) } }) - job2.id = 2 job2.flags = SchedulerJobFlags.ALLOW_RECURSE - queueJob(job2) + queueJob(job2, 2) await nextTick() @@ -667,38 +638,35 @@ describe('scheduler', () => { let recurse = true const job1: SchedulerJob = vi.fn(() => { - queueJob(job3, true) - queueJob(job3, true) + queueJob(job3, 3, true) + queueJob(job3, 3, true) flushPreFlushCbs() }) - job1.id = 1 const job2: SchedulerJob = vi.fn(() => { if (recurse) { // job2 does not allow recurse, so this shouldn't do anything - queueJob(job2, true) + queueJob(job2, 2, true) // job3 is already queued, so this shouldn't do anything - queueJob(job3, true) + queueJob(job3, 3, true) recurse = false } }) - job2.id = 2 const job3: SchedulerJob = vi.fn(() => { if (recurse) { - queueJob(job2, true) - queueJob(job3, true) + queueJob(job2, 2, true) + queueJob(job3, 3, true) // The jobs are already queued, so these should have no effect - queueJob(job2, true) - queueJob(job3, true) + queueJob(job2, 2, true) + queueJob(job3, 3, true) } }) - job3.id = 3 job3.flags = SchedulerJobFlags.ALLOW_RECURSE - queueJob(job1, true) + queueJob(job1, 1, true) await nextTick() @@ -755,7 +723,7 @@ describe('scheduler', () => { spy() flushPreFlushCbs() } - queueJob(job, true) + queueJob(job, undefined, true) await nextTick() expect(spy).toHaveBeenCalledTimes(1) }) @@ -767,16 +735,14 @@ describe('scheduler', () => { const job1: SchedulerJob = () => { calls.push('job1') } - job1.id = 1 const job2: SchedulerJob = () => { calls.push('job2') } - job2.id = 2 queuePostFlushCb(() => { - queueJob(job2, true) - queueJob(job1, true) + queueJob(job2, 2, true) + queueJob(job1, 1, true) // e.g. nested app.mount() call flushPreFlushCbs() @@ -807,14 +773,14 @@ describe('scheduler', () => { const cb1 = () => calls.push('cb1') // cb1 has no id const cb2 = () => calls.push('cb2') - cb2.id = -1 const queueAndFlush = (hook: Function) => { queuePostFlushCb(hook) flushPostFlushCbs() } queueAndFlush(() => { - queuePostFlushCb([cb1, cb2]) + queuePostFlushCb(cb1) + queuePostFlushCb(cb2, -1) flushPostFlushCbs() }) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index c475c997ab3..dab2d643159 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -167,7 +167,6 @@ class RenderWatcherEffect extends WatcherEffect { job.flags! |= SchedulerJobFlags.ALLOW_RECURSE } if (flush === 'pre' && instance) { - job.id = instance.uid ;(job as SchedulerJob).i = instance } this.job = job @@ -178,9 +177,13 @@ class RenderWatcherEffect extends WatcherEffect { if (!(flags & EffectFlags.PAUSED)) { const flush = this.flush if (flush === 'post') { - queuePostRenderEffect(this.job, this.instance && this.instance.suspense) + queuePostRenderEffect( + this.job, + undefined, + this.instance && this.instance.suspense, + ) } else if (flush === 'pre') { - queueJob(this.job, true) + queueJob(this.job, this.instance ? this.instance.uid : undefined, true) } else { this.scheduler() } @@ -258,7 +261,7 @@ function doWatch( effect.oldValue = effect.run() } } else if (flush === 'post') { - queuePostRenderEffect(effect.job, instance && instance.suspense) + queuePostRenderEffect(effect.job, undefined, instance && instance.suspense) } else { effect.scheduler() } diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index d18d5a48b8f..f4244f360e3 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -156,16 +156,20 @@ const KeepAliveImpl: ComponentOptions = { vnode.slotScopeIds, optimized, ) - queuePostRenderEffect(() => { - instance.isDeactivated = false - if (instance.a) { - invokeArrayFns(instance.a) - } - const vnodeHook = vnode.props && vnode.props.onVnodeMounted - if (vnodeHook) { - invokeVNodeHook(vnodeHook, instance.parent, vnode) - } - }, parentSuspense) + queuePostRenderEffect( + () => { + instance.isDeactivated = false + if (instance.a) { + invokeArrayFns(instance.a) + } + const vnodeHook = vnode.props && vnode.props.onVnodeMounted + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance.parent, vnode) + } + }, + undefined, + parentSuspense, + ) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { // Update components tree @@ -186,16 +190,20 @@ const KeepAliveImpl: ComponentOptions = { keepAliveInstance, parentSuspense, ) - queuePostRenderEffect(() => { - if (instance.da) { - invokeArrayFns(instance.da) - } - const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted - if (vnodeHook) { - invokeVNodeHook(vnodeHook, instance.parent, vnode) - } - instance.isDeactivated = true - }, parentSuspense) + queuePostRenderEffect( + () => { + if (instance.da) { + invokeArrayFns(instance.da) + } + const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance.parent, vnode) + } + instance.isDeactivated = true + }, + undefined, + parentSuspense, + ) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { // Update components tree @@ -255,12 +263,16 @@ const KeepAliveImpl: ComponentOptions = { // if KeepAlive child is a Suspense, it needs to be cached after Suspense resolves // avoid caching vnode that not been mounted if (isSuspense(keepAliveInstance.subTree.type)) { - queuePostRenderEffect(() => { - cache.set( - pendingCacheKey!, - getInnerChild(keepAliveInstance.subTree), - ) - }, keepAliveInstance.subTree.suspense) + queuePostRenderEffect( + () => { + cache.set( + pendingCacheKey!, + getInnerChild(keepAliveInstance.subTree), + ) + }, + undefined, + keepAliveInstance.subTree.suspense, + ) } else { cache.set(pendingCacheKey, getInnerChild(keepAliveInstance.subTree)) } @@ -278,7 +290,7 @@ const KeepAliveImpl: ComponentOptions = { resetShapeFlag(vnode) // but invoke its deactivated hook here const da = vnode.component!.da - da && queuePostRenderEffect(da, suspense) + da && queuePostRenderEffect(da, undefined, suspense) return } unmount(cached) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 0f6f69c6526..2539145bd00 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -873,6 +873,7 @@ function normalizeSuspenseSlot(s: any) { export function queueEffectWithSuspense( fn: Function | Function[], + id: number | undefined, suspense: SuspenseBoundary | null, ): void { if (suspense && suspense.pendingBranch) { @@ -882,7 +883,7 @@ export function queueEffectWithSuspense( suspense.effects.push(fn) } } else { - queuePostFlushCb(fn) + queuePostFlushCb(fn, id) } } diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index a6445df7b05..7f7a3c56e26 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -164,30 +164,38 @@ export const TeleportImpl = { } if (isTeleportDeferred(n2.props)) { - queuePostRenderEffect(() => { - mountToTarget() - n2.el!.__isMounted = true - }, parentSuspense) + queuePostRenderEffect( + () => { + mountToTarget() + n2.el!.__isMounted = true + }, + undefined, + parentSuspense, + ) } else { mountToTarget() } } else { if (isTeleportDeferred(n2.props) && !n1.el!.__isMounted) { - queuePostRenderEffect(() => { - TeleportImpl.process( - n1, - n2, - container, - anchor, - parentComponent, - parentSuspense, - namespace, - slotScopeIds, - optimized, - internals, - ) - delete n1.el!.__isMounted - }, parentSuspense) + queuePostRenderEffect( + () => { + TeleportImpl.process( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized, + internals, + ) + delete n1.el!.__isMounted + }, + undefined, + parentSuspense, + ) return } // update content diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index ef6f1918c31..0592cab763b 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -545,11 +545,15 @@ export function createHydrationFunctions( dirs || needCallTransitionHooks ) { - queueEffectWithSuspense(() => { - vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode) - needCallTransitionHooks && transition!.enter(el) - dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') - }, parentSuspense) + queueEffectWithSuspense( + () => { + vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode) + needCallTransitionHooks && transition!.enter(el) + dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') + }, + undefined, + parentSuspense, + ) } } diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index a60af1c1263..6dfbe26519e 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -307,12 +307,16 @@ export enum MoveType { export const queuePostRenderEffect: ( fn: SchedulerJobs, + id: number | undefined, suspense: SuspenseBoundary | null, ) => void = __FEATURE_SUSPENSE__ ? __TEST__ ? // vitest can't seem to handle eager circular dependency - (fn: Function | Function[], suspense: SuspenseBoundary | null) => - queueEffectWithSuspense(fn, suspense) + ( + fn: Function | Function[], + id: number | undefined, + suspense: SuspenseBoundary | null, + ) => queueEffectWithSuspense(fn, id, suspense) : queueEffectWithSuspense : queuePostFlushCb @@ -742,11 +746,15 @@ function baseCreateRenderer( needCallTransitionHooks || dirs ) { - queuePostRenderEffect(() => { - vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) - needCallTransitionHooks && transition!.enter(el) - dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') - }, parentSuspense) + queuePostRenderEffect( + () => { + vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) + needCallTransitionHooks && transition!.enter(el) + dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') + }, + undefined, + parentSuspense, + ) } } @@ -954,10 +962,14 @@ function baseCreateRenderer( } if ((vnodeHook = newProps.onVnodeUpdated) || dirs) { - queuePostRenderEffect(() => { - vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1) - dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated') - }, parentSuspense) + queuePostRenderEffect( + () => { + vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1) + dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated') + }, + undefined, + parentSuspense, + ) } } @@ -1329,8 +1341,7 @@ function baseCreateRenderer( const job: SchedulerJob = (instance.job = this.scheduler.bind(this)) job.i = instance - job.id = instance.uid - this.scheduler = () => queueJob(job) + this.scheduler = () => queueJob(job, instance.uid) if (__DEV__) { this.onTrack = instance.rtc @@ -1447,7 +1458,7 @@ function baseCreateRenderer( } // mounted hook if (m) { - queuePostRenderEffect(m, parentSuspense) + queuePostRenderEffect(m, undefined, parentSuspense) } // onVnodeMounted if ( @@ -1457,6 +1468,7 @@ function baseCreateRenderer( const scopedInitialVNode = initialVNode queuePostRenderEffect( () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode), + undefined, parentSuspense, ) } @@ -1466,6 +1478,7 @@ function baseCreateRenderer( ) { queuePostRenderEffect( () => instance.emit('hook:mounted'), + undefined, parentSuspense, ) } @@ -1480,13 +1493,15 @@ function baseCreateRenderer( isAsyncWrapper(parent.vnode) && parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) ) { - instance.a && queuePostRenderEffect(instance.a, parentSuspense) + instance.a && + queuePostRenderEffect(instance.a, undefined, parentSuspense) if ( __COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance) ) { queuePostRenderEffect( () => instance.emit('hook:activated'), + undefined, parentSuspense, ) } @@ -1594,12 +1609,13 @@ function baseCreateRenderer( } // updated hook if (u) { - queuePostRenderEffect(u, parentSuspense) + queuePostRenderEffect(u, undefined, parentSuspense) } // onVnodeUpdated if ((vnodeHook = next.props && next.props.onVnodeUpdated)) { queuePostRenderEffect( () => invokeVNodeHook(vnodeHook!, parent, next!, vnode), + undefined, parentSuspense, ) } @@ -1609,6 +1625,7 @@ function baseCreateRenderer( ) { queuePostRenderEffect( () => instance.emit('hook:updated'), + undefined, parentSuspense, ) } @@ -2147,7 +2164,11 @@ function baseCreateRenderer( if (moveType === MoveType.ENTER) { transition!.beforeEnter(el!) hostInsert(el!, container, anchor) - queuePostRenderEffect(() => transition!.enter(el!), parentSuspense) + queuePostRenderEffect( + () => transition!.enter(el!), + undefined, + parentSuspense, + ) } else { const { leave, delayLeave, afterLeave } = transition! const remove = () => { @@ -2292,11 +2313,15 @@ function baseCreateRenderer( (vnodeHook = props && props.onVnodeUnmounted)) || shouldInvokeDirs ) { - queuePostRenderEffect(() => { - vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) - shouldInvokeDirs && - invokeDirectiveHook(vnode, null, parentComponent, 'unmounted') - }, parentSuspense) + queuePostRenderEffect( + () => { + vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) + shouldInvokeDirs && + invokeDirectiveHook(vnode, null, parentComponent, 'unmounted') + }, + undefined, + parentSuspense, + ) } } @@ -2401,7 +2426,7 @@ function baseCreateRenderer( } // unmounted hook if (um) { - queuePostRenderEffect(um, parentSuspense) + queuePostRenderEffect(um, undefined, parentSuspense) } if ( __COMPAT__ && @@ -2409,12 +2434,15 @@ function baseCreateRenderer( ) { queuePostRenderEffect( () => instance.emit('hook:destroyed'), + undefined, parentSuspense, ) } - queuePostRenderEffect(() => { - instance.isUnmounted = true - }, parentSuspense) + queuePostRenderEffect( + () => (instance.isUnmounted = true), + undefined, + parentSuspense, + ) // A component with async dep inside a pending suspense is unmounted before // its async dep resolves. This should remove the dep from the suspense, and diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts index ca21030dc35..31fcf8c2d5b 100644 --- a/packages/runtime-core/src/rendererTemplateRef.ts +++ b/packages/runtime-core/src/rendererTemplateRef.ts @@ -13,7 +13,6 @@ import { isAsyncWrapper } from './apiAsyncComponent' import { warn } from './warning' import { isRef, toRaw } from '@vue/reactivity' import { ErrorCodes, callWithErrorHandling } from './errorHandling' -import type { SchedulerJob } from './scheduler' import { queuePostRenderEffect } from './renderer' import { type ComponentOptions, getComponentPublicInstance } from './component' import { knownTemplateRefs } from './helpers/useTemplateRef' @@ -153,8 +152,7 @@ export function setRef( // #1789: for non-null values, set them after render // null values means this is unmount and it should not overwrite another // ref with the same key - ;(doSet as SchedulerJob).id = -1 - queuePostRenderEffect(doSet, parentSuspense) + queuePostRenderEffect(doSet, -1, parentSuspense) } else { doSet() } diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 8a58ac9a296..f3482aca5e4 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -24,7 +24,7 @@ export enum SchedulerJobFlags { } export interface SchedulerJob extends Function { - id?: number + order?: number /** * flags can technically be undefined, but it can still be used in bitwise * operations just like 0. @@ -40,15 +40,12 @@ export interface SchedulerJob extends Function { export type SchedulerJobs = SchedulerJob | SchedulerJob[] const jobs: SchedulerJob[] = [] -const preJobs: SchedulerJob[] = [] let postJobs: SchedulerJob[] = [] let activePostJobs: SchedulerJob[] | null = null let currentFlushPromise: Promise | null = null let jobsLength = 0 -let preJobsLength = 0 let flushIndex = 0 -let preFlushIndex = 0 let postFlushIndex = 0 const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise @@ -74,15 +71,14 @@ export function nextTick( // 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, + order: number, queue: SchedulerJob[], start: number, end: number, ) { while (start < end) { const middle = (start + end) >>> 1 - const middleJob = queue[middle] - if (middleJob.id! <= id) { + if (queue[middle].order! <= order) { start = middle + 1 } else { end = middle @@ -94,45 +90,40 @@ function findInsertionIndex( /** * @internal for runtime-vapor only */ -export function queueJob(job: SchedulerJob, isPre = false): void { - if (isPre) { - if (_queueJob(job, preJobs, preJobsLength, preFlushIndex, -1)) { - preJobsLength++ - queueFlush() - } - } else { - if (_queueJob(job, jobs, jobsLength, flushIndex, Infinity)) { - jobsLength++ - queueFlush() - } +export function queueJob(job: SchedulerJob, id?: number, isPre = false): void { + if ( + queueJobWorker( + job, + id === undefined ? (isPre ? -2 : Infinity) : isPre ? id * 2 : id * 2 + 1, + jobs, + jobsLength, + flushIndex, + ) + ) { + jobsLength++ + queueFlush() } } -function _queueJob( +function queueJobWorker( job: SchedulerJob, + order: number, queue: SchedulerJob[], length: number, flushIndex: number, - defaultId: number, ) { const flags = job.flags! if (!(flags & SchedulerJobFlags.QUEUED)) { job.flags! = flags | SchedulerJobFlags.QUEUED - if (job.id === undefined) { - job.id = defaultId - } + job.order = order if ( flushIndex === length || // fast path when the job id is larger than the tail - job.id >= queue[length - 1].id! + order >= queue[length - 1].order! ) { queue[length] = job } else { - queue.splice( - findInsertionIndex(job.id, queue, flushIndex, length), - 0, - job, - ) + queue.splice(findInsertionIndex(order, queue, flushIndex, length), 0, job) } return true } @@ -148,19 +139,22 @@ function queueFlush() { } } -export function queuePostFlushCb(jobs: SchedulerJobs): void { +export function queuePostFlushCb( + jobs: SchedulerJobs, + id: number = Infinity, +): void { if (!isArray(jobs)) { - if (activePostJobs && jobs.id === -1) { + if (activePostJobs && id === -1) { activePostJobs.splice(postFlushIndex, 0, jobs) } else { - _queueJob(jobs, postJobs, postJobs.length, 0, Infinity) + queueJobWorker(jobs, id, postJobs, postJobs.length, 0) } } 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 for (const job of jobs) { - _queueJob(job, postJobs, postJobs.length, 0, Infinity) + queueJobWorker(job, id, postJobs, postJobs.length, 0) } } queueFlush() @@ -173,17 +167,20 @@ export function flushPreFlushCbs( if (__DEV__) { seen = seen || new Map() } - for (let i = preFlushIndex; i < preJobsLength; i++) { - const cb = preJobs[i] - if (instance && cb.id !== instance.uid) { + for (let i = flushIndex; i < jobsLength; i++) { + const cb = jobs[i] + if (cb.order! & 1 || cb.order === Infinity) { + continue + } + if (instance && cb.order !== instance.uid * 2) { continue } if (__DEV__ && checkRecursiveUpdates(seen!, cb)) { continue } - preJobs.splice(i, 1) + jobs.splice(i, 1) i-- - preJobsLength-- + jobsLength-- if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) { cb.flags! &= ~SchedulerJobFlags.QUEUED } @@ -251,27 +248,9 @@ function flushJobs(seen?: CountMap) { } try { - while (preFlushIndex < preJobsLength || flushIndex < jobsLength) { - let job: SchedulerJob - if (preFlushIndex < preJobsLength) { - if (flushIndex < jobsLength) { - const mainJob = jobs[flushIndex] - const preJob = preJobs[preFlushIndex] - if (preJob.id! <= mainJob.id!) { - job = preJob - preJobs[preFlushIndex++] = undefined as any - } else { - job = mainJob - jobs[flushIndex++] = undefined as any - } - } else { - job = preJobs[preFlushIndex] - preJobs[preFlushIndex++] = undefined as any - } - } else { - job = jobs[flushIndex] - jobs[flushIndex++] = undefined as any - } + while (flushIndex < jobsLength) { + const job = jobs[flushIndex] + jobs[flushIndex++] = undefined as any if (!(job.flags! & SchedulerJobFlags.DISPOSED)) { // conditional usage of checkRecursiveUpdate must be determined out of @@ -302,25 +281,19 @@ function flushJobs(seen?: CountMap) { } } finally { // If there was an error we still need to clear the QUEUED flags - while (preFlushIndex < preJobsLength) { - preJobs[preFlushIndex].flags! &= ~SchedulerJobFlags.QUEUED - preJobs[preFlushIndex++] = undefined as any - } while (flushIndex < jobsLength) { jobs[flushIndex].flags! &= ~SchedulerJobFlags.QUEUED jobs[flushIndex++] = undefined as any } flushIndex = 0 - preFlushIndex = 0 jobsLength = 0 - preJobsLength = 0 flushPostFlushCbs(seen) currentFlushPromise = null // If new jobs have been added to either queue, keep flushing - if (preJobsLength || jobsLength || postJobs.length) { + if (jobsLength || postJobs.length) { flushJobs(seen) } } diff --git a/packages/runtime-vapor/src/apiTemplateRef.ts b/packages/runtime-vapor/src/apiTemplateRef.ts index c5a6c5fb2b6..367eb8459f3 100644 --- a/packages/runtime-vapor/src/apiTemplateRef.ts +++ b/packages/runtime-vapor/src/apiTemplateRef.ts @@ -119,8 +119,7 @@ export function setRef( warn('Invalid template ref type:', ref, `(${typeof ref})`) } } - doSet.id = -1 - queuePostFlushCb(doSet) + queuePostFlushCb(doSet, -1) // TODO this gets called repeatedly in renderEffect when it's dynamic ref? onScopeDispose(() => { diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index 6ea2a2dc17c..ac50744827e 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -39,7 +39,6 @@ class RenderEffect extends ReactiveEffect { : void 0 } job.i = instance - job.id = instance.uid } this.job = job @@ -74,7 +73,7 @@ class RenderEffect extends ReactiveEffect { notify(): void { const flags = this.flags if (!(flags & EffectFlags.PAUSED)) { - queueJob(this.job) + queueJob(this.job, this.i ? this.i.uid : undefined) } else { this.flags = flags | EffectFlags.NOTIFIED } From 4765af3a1e79eec88904d4fd5935f5f24856ccad Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Sat, 3 May 2025 04:34:13 +0800 Subject: [PATCH 30/30] refactor(reactivity): alien-signals 2.0.1 --- .../reactivity/__tests__/computed.spec.ts | 6 +- packages/reactivity/__tests__/effect.spec.ts | 4 +- packages/reactivity/src/computed.ts | 36 +- packages/reactivity/src/debug.ts | 8 +- packages/reactivity/src/dep.ts | 12 +- packages/reactivity/src/effect.ts | 49 +-- packages/reactivity/src/effectScope.ts | 25 +- packages/reactivity/src/ref.ts | 61 +++- packages/reactivity/src/system.ts | 324 ++++++++---------- 9 files changed, 266 insertions(+), 259 deletions(-) diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index 62b3237c429..31562cf22b3 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -27,7 +27,7 @@ import { } from '../src' import type { ComputedRef, ComputedRefImpl } from '../src/computed' import { pauseTracking, resetTracking } from '../src/effect' -import { SubscriberFlags } from '../src/system' +import { ReactiveFlags } from '../src/system' describe('reactivity/computed', () => { it('should return updated value', () => { @@ -467,8 +467,8 @@ describe('reactivity/computed', () => { const c2 = computed(() => c1.value) as unknown as ComputedRefImpl c2.value - expect(c1.flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)).toBe(0) - expect(c2.flags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)).toBe(0) + expect(c1.flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)).toBe(0) + expect(c2.flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)).toBe(0) }) it('should chained computeds dirtyLevel update with first computed effect', () => { diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 20f0244a7bc..5d7a1e39cef 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -22,7 +22,7 @@ import { stop, toRaw, } from '../src/index' -import { type Dependency, endBatch, startBatch } from '../src/system' +import { type ReactiveNode, endBatch, startBatch } from '../src/system' describe('reactivity/effect', () => { it('should run the passed function once (wrapped by a effect)', () => { @@ -1178,7 +1178,7 @@ describe('reactivity/effect', () => { }) describe('dep unsubscribe', () => { - function getSubCount(dep: Dependency | undefined) { + function getSubCount(dep: ReactiveNode | undefined) { let count = 0 let sub = dep!.subs while (sub) { diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 1f7d27316ee..6238bac33a0 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -10,10 +10,9 @@ import { import { activeEffectScope } from './effectScope' import type { Ref } from './ref' import { - type Dependency, type Link, - type Subscriber, - SubscriberFlags, + type ReactiveNode, + ReactiveFlags as _ReactiveFlags, checkDirty, endTracking, link, @@ -53,20 +52,17 @@ export interface WritableComputedOptions { * @private exported by @vue/reactivity for Vue core use, but not exported from * the main vue package */ -export class ComputedRefImpl implements Dependency, Subscriber { +export class ComputedRefImpl implements ReactiveNode { /** * @internal */ _value: T | undefined = undefined - // Dependency subs: Link | undefined = undefined subsTail: Link | undefined = undefined - - // Subscriber deps: Link | undefined = undefined depsTail: Link | undefined = undefined - flags: SubscriberFlags = SubscriberFlags.Dirty + flags: _ReactiveFlags = _ReactiveFlags.Mutable | _ReactiveFlags.Dirty /** * @internal @@ -84,7 +80,7 @@ export class ComputedRefImpl implements Dependency, Subscriber { return this } // for backwards compat - get dep(): Dependency { + get dep(): ReactiveNode { return this } /** @@ -93,15 +89,15 @@ export class ComputedRefImpl implements Dependency, Subscriber { */ get _dirty(): boolean { const flags = this.flags - if (flags & SubscriberFlags.Dirty) { + if (flags & _ReactiveFlags.Dirty) { return true } - if (flags & SubscriberFlags.Pending) { - if (checkDirty(this.deps!)) { - this.flags = flags | SubscriberFlags.Dirty + if (flags & _ReactiveFlags.Pending) { + if (checkDirty(this.deps!, this)) { + this.flags = flags | _ReactiveFlags.Dirty return true } else { - this.flags = flags & ~SubscriberFlags.Pending + this.flags = flags & ~_ReactiveFlags.Pending } } return false @@ -112,9 +108,9 @@ export class ComputedRefImpl implements Dependency, Subscriber { */ set _dirty(v: boolean) { if (v) { - this.flags |= SubscriberFlags.Dirty + this.flags |= _ReactiveFlags.Dirty } else { - this.flags &= ~(SubscriberFlags.Dirty | SubscriberFlags.Pending) + this.flags &= ~(_ReactiveFlags.Dirty | _ReactiveFlags.Pending) } } @@ -139,8 +135,8 @@ export class ComputedRefImpl implements Dependency, Subscriber { get value(): T { const flags = this.flags if ( - flags & SubscriberFlags.Dirty || - (flags & SubscriberFlags.Pending && checkDirty(this.deps!)) + flags & _ReactiveFlags.Dirty || + (flags & _ReactiveFlags.Pending && checkDirty(this.deps!, this)) ) { if (this.update()) { const subs = this.subs @@ -148,8 +144,8 @@ export class ComputedRefImpl implements Dependency, Subscriber { shallowPropagate(subs) } } - } else if (flags & SubscriberFlags.Pending) { - this.flags = flags & ~SubscriberFlags.Pending + } else if (flags & _ReactiveFlags.Pending) { + this.flags = flags & ~_ReactiveFlags.Pending } if (activeSub !== undefined) { if (__DEV__) { diff --git a/packages/reactivity/src/debug.ts b/packages/reactivity/src/debug.ts index c1f35b62ad8..ba323d8993c 100644 --- a/packages/reactivity/src/debug.ts +++ b/packages/reactivity/src/debug.ts @@ -1,6 +1,6 @@ import { extend } from '@vue/shared' import type { DebuggerEventExtraInfo, ReactiveEffectOptions } from './effect' -import { type Link, type Subscriber, SubscriberFlags } from './system' +import { type Link, ReactiveFlags, type ReactiveNode } from './system' export const triggerEventInfos: DebuggerEventExtraInfo[] = [] @@ -61,7 +61,7 @@ export function setupOnTrigger(target: { new (...args: any[]): any }): void { }) } -function setupFlagsHandler(target: Subscriber): void { +function setupFlagsHandler(target: ReactiveNode): void { ;(target as any)._flags = target.flags Object.defineProperty(target, 'flags', { get() { @@ -71,9 +71,9 @@ function setupFlagsHandler(target: Subscriber): void { if ( !( (target as any)._flags & - (SubscriberFlags.Dirty | SubscriberFlags.Pending) + (ReactiveFlags.Dirty | ReactiveFlags.Pending) ) && - !!(value & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) + !!(value & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) ) { onTrigger(this) } diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index 184964c17b8..1cf5351d663 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -3,17 +3,20 @@ import { type TrackOpTypes, TriggerOpTypes } from './constants' import { onTrack, triggerEventInfos } from './debug' import { activeSub } from './effect' import { - type Dependency, type Link, + ReactiveFlags, + type ReactiveNode, endBatch, link, propagate, + shallowPropagate, startBatch, } from './system' -class Dep implements Dependency { +class Dep implements ReactiveNode { _subs: Link | undefined = undefined subsTail: Link | undefined = undefined + flags: ReactiveFlags = ReactiveFlags.None constructor( private map: KeyToDepMap, @@ -103,7 +106,7 @@ export function trigger( return } - const run = (dep: Dependency | undefined) => { + const run = (dep: ReactiveNode | undefined) => { if (dep !== undefined && dep.subs !== undefined) { if (__DEV__) { triggerEventInfos.push({ @@ -116,6 +119,7 @@ export function trigger( }) } propagate(dep.subs) + shallowPropagate(dep.subs) if (__DEV__) { triggerEventInfos.pop() } @@ -190,7 +194,7 @@ export function trigger( export function getDepFromReactive( object: any, key: string | number | symbol, -): Dependency | undefined { +): ReactiveNode | undefined { const depMap = targetMap.get(object) return depMap && depMap.get(key) } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index aa16f43f46c..aa4abb12930 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -3,10 +3,9 @@ import type { TrackOpTypes, TriggerOpTypes } from './constants' import { setupOnTrigger } from './debug' import { activeEffectScope } from './effectScope' import { - type Dependency, type Link, - type Subscriber, - SubscriberFlags, + ReactiveFlags, + type ReactiveNode, checkDirty, endTracking, link, @@ -18,7 +17,7 @@ import { warn } from './warning' export type EffectScheduler = (...args: any[]) => any export type DebuggerEvent = { - effect: Subscriber + effect: ReactiveNode } & DebuggerEventExtraInfo export type DebuggerEventExtraInfo = { @@ -55,16 +54,13 @@ export enum EffectFlags { } export class ReactiveEffect - implements ReactiveEffectOptions, Dependency, Subscriber + implements ReactiveEffectOptions, ReactiveNode { - // Subscriber deps: Link | undefined = undefined depsTail: Link | undefined = undefined - flags: number = SubscriberFlags.Dirty - - // Dependency subs: Link | undefined = undefined subsTail: Link | undefined = undefined + flags: number = ReactiveFlags.Watching | ReactiveFlags.Dirty /** * @internal @@ -129,6 +125,9 @@ export class ReactiveEffect } run(): T { + if (!this.active) { + return this.fn() + } cleanup(this) const prevSub = activeSub setActiveSub(this) @@ -147,10 +146,10 @@ export class ReactiveEffect endTracking(this) const flags = this.flags if ( - (flags & (SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE)) === - (SubscriberFlags.Recursed | EffectFlags.ALLOW_RECURSE) + (flags & (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)) === + (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE) ) { - this.flags = flags & ~SubscriberFlags.Recursed + this.flags = flags & ~ReactiveFlags.Recursed this.notify() } } @@ -161,23 +160,25 @@ export class ReactiveEffect if (sub !== undefined) { unlink(sub) } - startTracking(this) - endTracking(this) - cleanup(this) + let dep = this.deps + while (dep !== undefined) { + dep = unlink(dep, this) + } this.flags = 0 + cleanup(this) } get dirty(): boolean { const flags = this.flags - if (flags & SubscriberFlags.Dirty) { + if (flags & ReactiveFlags.Dirty) { return true } - if (flags & SubscriberFlags.Pending) { - if (checkDirty(this.deps!)) { - this.flags = flags | SubscriberFlags.Dirty + if (flags & ReactiveFlags.Pending) { + if (checkDirty(this.deps!, this)) { + this.flags = flags | ReactiveFlags.Dirty return true } else { - this.flags = flags & ~SubscriberFlags.Pending + this.flags = flags & ~ReactiveFlags.Pending } } return false @@ -234,7 +235,7 @@ export function stop(runner: ReactiveEffectRunner): void { runner.effect.stop() } -const resetTrackingStack: (Subscriber | undefined)[] = [] +const resetTrackingStack: (ReactiveNode | undefined)[] = [] /** * Temporarily pauses tracking. @@ -284,7 +285,7 @@ export function resetTracking(): void { } export function cleanup( - sub: Subscriber & { cleanups: (() => void)[]; cleanupsLength: number }, + sub: ReactiveNode & { cleanups: (() => void)[]; cleanupsLength: number }, ): void { const l = sub.cleanupsLength if (l) { @@ -329,8 +330,8 @@ function cleanupEffect(fn: () => void) { } } -export let activeSub: Subscriber | undefined = undefined +export let activeSub: ReactiveNode | undefined = undefined -export function setActiveSub(sub: Subscriber | undefined): void { +export function setActiveSub(sub: ReactiveNode | undefined): void { activeSub = sub } diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index aa1b6b97553..ca148071e31 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -1,26 +1,15 @@ import { EffectFlags, cleanup } from './effect' -import { - type Dependency, - type Link, - type Subscriber, - endTracking, - link, - startTracking, - unlink, -} from './system' +import { type Link, type ReactiveNode, link, unlink } from './system' import { warn } from './warning' export let activeEffectScope: EffectScope | undefined -export class EffectScope implements Subscriber, Dependency { - // Subscriber +export class EffectScope implements ReactiveNode { deps: Link | undefined = undefined depsTail: Link | undefined = undefined - flags: number = 0 - - // Dependency subs: Link | undefined = undefined subsTail: Link | undefined = undefined + flags: number = 0 /** * @internal @@ -86,10 +75,12 @@ export class EffectScope implements Subscriber, Dependency { if (sub !== undefined) { unlink(sub) } - startTracking(this) - endTracking(this) - cleanup(this) + let dep = this.deps + while (dep !== undefined) { + dep = unlink(dep, this) + } this.flags = 0 + cleanup(this) } } diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index b8b1c1e32fc..5dd656d73a1 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -19,7 +19,16 @@ import { toRaw, toReactive, } from './reactive' -import { type Dependency, type Link, link, propagate } from './system' +import { + type Link, + type ReactiveNode, + ReactiveFlags as _ReactiveFlags, + batchDepth, + link, + processEffectNotifications, + propagate, + shallowPropagate, +} from './system' declare const RefSymbol: unique symbol export declare const RawSymbol: unique symbol @@ -106,31 +115,46 @@ function createRef(rawValue: unknown, wrap?: (v: T) => T) { /** * @internal */ -class RefImpl implements Dependency { - // Dependency +class RefImpl implements ReactiveNode { subs: Link | undefined = undefined subsTail: Link | undefined = undefined + flags: _ReactiveFlags = _ReactiveFlags.Mutable _value: T _wrap?: (v: T) => T + private _oldValue: T private _rawValue: T - public readonly [ReactiveFlags.IS_REF] = true - public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false + /** + * @internal + */ + readonly __v_isRef = true + // TODO isolatedDeclarations ReactiveFlags.IS_REF + /** + * @internal + */ + readonly __v_isShallow: boolean = false + // TODO isolatedDeclarations ReactiveFlags.IS_SHALLOW constructor(value: T, wrap: ((v: T) => T) | undefined) { - this._rawValue = wrap ? toRaw(value) : value + this._oldValue = this._rawValue = wrap ? toRaw(value) : value this._value = wrap ? wrap(value) : value this._wrap = wrap this[ReactiveFlags.IS_SHALLOW] = !wrap } - get dep() { + get dep(): this { return this } - get value() { + get value(): T { trackRef(this) + if (this.flags & _ReactiveFlags.Dirty && this.update()) { + const subs = this.subs + if (subs !== undefined) { + shallowPropagate(subs) + } + } return this._value } @@ -142,6 +166,7 @@ class RefImpl implements Dependency { isReadonly(newValue) newValue = useDirectValue ? newValue : toRaw(newValue) if (hasChanged(newValue, oldValue)) { + this.flags |= _ReactiveFlags.Dirty this._rawValue = newValue this._value = !useDirectValue && this._wrap ? this._wrap(newValue) : newValue @@ -157,12 +182,20 @@ class RefImpl implements Dependency { }) } propagate(subs) + if (!batchDepth) { + processEffectNotifications() + } if (__DEV__) { triggerEventInfos.pop() } } } } + + update(): boolean { + this.flags &= ~_ReactiveFlags.Dirty + return hasChanged(this._oldValue, (this._oldValue = this._rawValue)) + } } /** @@ -195,10 +228,14 @@ export function triggerRef(ref: Ref): void { const dep = (ref as unknown as RefImpl).dep if (dep !== undefined && dep.subs !== undefined) { propagate(dep.subs) + shallowPropagate(dep.subs) + if (!batchDepth) { + processEffectNotifications() + } } } -function trackRef(dep: Dependency) { +function trackRef(dep: ReactiveNode) { if (activeSub !== undefined) { if (__DEV__) { onTrack(activeSub!, { @@ -299,10 +336,10 @@ export type CustomRefFactory = ( set: (value: T) => void } -class CustomRefImpl implements Dependency { - // Dependency +class CustomRefImpl implements ReactiveNode { subs: Link | undefined = undefined subsTail: Link | undefined = undefined + flags: _ReactiveFlags = _ReactiveFlags.None private readonly _get: ReturnType>['get'] private readonly _set: ReturnType>['set'] @@ -383,7 +420,7 @@ class ObjectRefImpl { this._object[this._key] = newVal } - get dep(): Dependency | undefined { + get dep(): ReactiveNode | undefined { return getDepFromReactive(toRaw(this._object), this._key) } } diff --git a/packages/reactivity/src/system.ts b/packages/reactivity/src/system.ts index d3882e38c64..a591d45aa88 100644 --- a/packages/reactivity/src/system.ts +++ b/packages/reactivity/src/system.ts @@ -1,44 +1,44 @@ /* eslint-disable */ -// Ported from https://github.com/stackblitz/alien-signals/blob/v1.0.13/src/system.ts +// Ported from https://github.com/stackblitz/alien-signals/blob/v2.0.1/src/system.ts import type { ComputedRefImpl as Computed } from './computed.js' import type { ReactiveEffect as Effect } from './effect.js' -import { EffectScope } from './effectScope.js' +import type { EffectScope } from './effectScope.js' -export interface Dependency { - subs: Link | undefined - subsTail: Link | undefined -} - -export interface Subscriber { - flags: SubscriberFlags - deps: Link | undefined - depsTail: Link | undefined +export interface ReactiveNode { + deps?: Link + depsTail?: Link + subs?: Link + subsTail?: Link + flags: ReactiveFlags } export interface Link { - dep: Dependency | Computed | Effect | EffectScope - sub: Subscriber | Computed | Effect | EffectScope + dep: ReactiveNode | Computed | Effect | EffectScope + sub: ReactiveNode | Computed | Effect | EffectScope prevSub: Link | undefined nextSub: Link | undefined prevDep: Link | undefined nextDep: Link | undefined } -export const enum SubscriberFlags { - Tracking = 1 << 2, - Recursed = 1 << 4, - Dirty = 1 << 5, - Pending = 1 << 6, +interface Stack { + value: T + prev: Stack | undefined } -interface OneWayLink { - target: T - linked: OneWayLink | undefined +export const enum ReactiveFlags { + None = 0, + Mutable = 1 << 0, + Watching = 1 << 1, + RecursedCheck = 1 << 2, + Recursed = 1 << 3, + Dirty = 1 << 4, + Pending = 1 << 5, } -const notifyBuffer: (Effect | EffectScope | undefined)[] = [] +const notifyBuffer: (Effect | undefined)[] = [] -let batchDepth = 0 +export let batchDepth = 0 let notifyIndex = 0 let notifyBufferLength = 0 @@ -52,21 +52,25 @@ export function endBatch(): void { } } -export function link(dep: Dependency, sub: Subscriber): void { +export function link(dep: ReactiveNode, sub: ReactiveNode): void { const prevDep = sub.depsTail if (prevDep !== undefined && prevDep.dep === dep) { return } - const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps - if (nextDep !== undefined && nextDep.dep === dep) { - sub.depsTail = nextDep - return + let nextDep: Link | undefined = undefined + const recursedCheck = sub.flags & ReactiveFlags.RecursedCheck + if (recursedCheck) { + nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps + if (nextDep !== undefined && nextDep.dep === dep) { + sub.depsTail = nextDep + return + } } const prevSub = dep.subsTail if ( prevSub !== undefined && prevSub.sub === sub && - isValidLink(prevSub, sub) + (!recursedCheck || isValidLink(prevSub, sub)) ) { return } @@ -84,37 +88,27 @@ export function link(dep: Dependency, sub: Subscriber): void { if (nextDep !== undefined) { nextDep.prevDep = newLink } - if (prevDep === undefined) { - sub.deps = newLink - } else { + if (prevDep !== undefined) { prevDep.nextDep = newLink - } - if (prevSub === undefined) { - dep.subs = newLink } else { + sub.deps = newLink + } + if (prevSub !== undefined) { prevSub.nextSub = newLink + } else { + dep.subs = newLink } } export function unlink( link: Link, - sub: Subscriber = link.sub, + sub: ReactiveNode = 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 { @@ -125,133 +119,113 @@ export function unlink( } else { sub.deps = nextDep } - if (dep.subs === undefined && 'deps' in dep) { + if (nextSub !== undefined) { + nextSub.prevSub = prevSub + } else { + dep.subsTail = prevSub + } + if (prevSub !== undefined) { + prevSub.nextSub = nextSub + } else if ((dep.subs = nextSub) === undefined) { 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 + } else { + dep.flags |= ReactiveFlags.Dirty } } return nextDep } -export function propagate(current: Link): void { - let next = current.nextSub - let branchs: OneWayLink | undefined - let branchDepth = 0 - let targetFlag = SubscriberFlags.Dirty +export function propagate(link: Link): void { + let next = link.nextSub + let stack: Stack | undefined top: do { - const sub = current.sub - const subFlags = sub.flags - - let shouldNotify = false + const sub = link.sub - if ( - !( - subFlags & - (SubscriberFlags.Tracking | - SubscriberFlags.Recursed | - SubscriberFlags.Dirty | - SubscriberFlags.Pending) - ) - ) { - sub.flags = subFlags | targetFlag - shouldNotify = true - } else if ( - subFlags & SubscriberFlags.Recursed && - !(subFlags & SubscriberFlags.Tracking) - ) { - sub.flags = (subFlags & ~SubscriberFlags.Recursed) | targetFlag - shouldNotify = true - } else if ( - !(subFlags & (SubscriberFlags.Dirty | SubscriberFlags.Pending)) && - isValidLink(current, sub) - ) { - sub.flags = subFlags | SubscriberFlags.Recursed | targetFlag - shouldNotify = !('notify' in sub) - } + let flags = sub.flags - if (shouldNotify) { - if ('notify' in sub) { - if (!batchDepth && !notifyBufferLength && next === undefined) { - sub.notify() - } else { - notifyBuffer[notifyBufferLength++] = sub - } + if (flags & (ReactiveFlags.Mutable | ReactiveFlags.Watching)) { + if ( + !( + flags & + (ReactiveFlags.RecursedCheck | + ReactiveFlags.Recursed | + ReactiveFlags.Dirty | + ReactiveFlags.Pending) + ) + ) { + sub.flags = flags | ReactiveFlags.Pending + } else if ( + !(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed)) + ) { + flags = ReactiveFlags.None + } else if (!(flags & ReactiveFlags.RecursedCheck)) { + sub.flags = (flags & ~ReactiveFlags.Recursed) | ReactiveFlags.Pending + } else if ( + !(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) && + isValidLink(link, sub) + ) { + sub.flags = flags | ReactiveFlags.Recursed | ReactiveFlags.Pending + flags &= ReactiveFlags.Mutable } else { - const subSubs = (sub as Dependency).subs + flags = ReactiveFlags.None + } + + if (flags & ReactiveFlags.Watching) { + notifyBuffer[notifyBufferLength++] = sub as Effect + } + + if (flags & ReactiveFlags.Mutable) { + const subSubs = sub.subs if (subSubs !== undefined) { - current = subSubs + link = subSubs if (subSubs.nextSub !== undefined) { - branchs = { target: next, linked: branchs } - ++branchDepth - next = current.nextSub + stack = { value: next, prev: stack } + next = link.nextSub } - targetFlag = SubscriberFlags.Pending continue } } - } else if (!(subFlags & (SubscriberFlags.Tracking | targetFlag))) { - sub.flags = subFlags | targetFlag - } else if ( - !(subFlags & targetFlag) && - subFlags & (SubscriberFlags.Dirty | SubscriberFlags.Pending) && - isValidLink(current, sub) - ) { - sub.flags = subFlags | targetFlag } - if ((current = next!) !== undefined) { - next = current.nextSub - targetFlag = branchDepth ? SubscriberFlags.Pending : SubscriberFlags.Dirty + if ((link = next!) !== undefined) { + next = link.nextSub continue } - while (branchDepth--) { - current = branchs!.target! - branchs = branchs!.linked - if (current !== undefined) { - next = current.nextSub - targetFlag = branchDepth - ? SubscriberFlags.Pending - : SubscriberFlags.Dirty + while (stack !== undefined) { + link = stack.value! + stack = stack.prev + if (link !== undefined) { + next = link.nextSub continue top } } break } while (true) - - if (!batchDepth && notifyBufferLength) { - processEffectNotifications() - } } -export function startTracking(sub: Subscriber): void { +export function startTracking(sub: ReactiveNode): void { sub.depsTail = undefined sub.flags = (sub.flags & - ~( - SubscriberFlags.Recursed | - SubscriberFlags.Dirty | - SubscriberFlags.Pending - )) | - SubscriberFlags.Tracking + ~(ReactiveFlags.Recursed | ReactiveFlags.Dirty | ReactiveFlags.Pending)) | + ReactiveFlags.RecursedCheck } -export function endTracking(sub: Subscriber): void { +export function endTracking(sub: ReactiveNode): void { const depsTail = sub.depsTail let toRemove = depsTail !== undefined ? depsTail.nextDep : sub.deps while (toRemove !== undefined) { toRemove = unlink(toRemove, sub) } - sub.flags &= ~SubscriberFlags.Tracking + sub.flags &= ~ReactiveFlags.RecursedCheck } export function processEffectNotifications(): void { @@ -264,68 +238,71 @@ export function processEffectNotifications(): void { notifyBufferLength = 0 } -export function checkDirty(current: Link): boolean { - let prevLinks: OneWayLink | undefined +export function checkDirty(link: Link, sub: ReactiveNode): boolean { + let stack: Stack | undefined let checkDepth = 0 - let dirty: boolean top: do { - dirty = false - const dep = current.dep + const dep = link.dep + const depFlags = dep.flags + + let dirty = false - if (current.sub.flags & SubscriberFlags.Dirty) { + if (sub.flags & ReactiveFlags.Dirty) { dirty = true - } else if ('update' in dep) { - const depFlags = dep.flags - if (depFlags & SubscriberFlags.Dirty) { - if ((dep as Computed).update()) { - const subs = dep.subs! - if (subs.nextSub !== undefined) { - shallowPropagate(subs) - } - dirty = true - } - } else if (depFlags & SubscriberFlags.Pending) { - if (current.nextSub !== undefined || current.prevSub !== undefined) { - prevLinks = { target: current, linked: prevLinks } + } else if ( + (depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) === + (ReactiveFlags.Mutable | ReactiveFlags.Dirty) + ) { + if ((dep as Computed).update()) { + const subs = dep.subs! + if (subs.nextSub !== undefined) { + shallowPropagate(subs) } - current = dep.deps! - ++checkDepth - continue + dirty = true } + } else if ( + (depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Pending)) === + (ReactiveFlags.Mutable | ReactiveFlags.Pending) + ) { + if (link.nextSub !== undefined || link.prevSub !== undefined) { + stack = { value: link, prev: stack } + } + link = dep.deps! + sub = dep + ++checkDepth + continue } - if (!dirty && current.nextDep !== undefined) { - current = current.nextDep + if (!dirty && link.nextDep !== undefined) { + link = link.nextDep continue } while (checkDepth) { --checkDepth - const sub = current.sub as Computed const firstSub = sub.subs! + const hasMultipleSubs = firstSub.nextSub !== undefined + if (hasMultipleSubs) { + link = stack!.value + stack = stack!.prev + } else { + link = firstSub + } if (dirty) { - if (sub.update()) { - if (firstSub.nextSub !== undefined) { - current = prevLinks!.target - prevLinks = prevLinks!.linked + if ((sub as Computed).update()) { + if (hasMultipleSubs) { shallowPropagate(firstSub) - } else { - current = firstSub } + sub = link.sub continue } } else { - sub.flags &= ~SubscriberFlags.Pending - } - if (firstSub.nextSub !== undefined) { - current = prevLinks!.target - prevLinks = prevLinks!.linked - } else { - current = firstSub + sub.flags &= ~ReactiveFlags.Pending } - if (current.nextDep !== undefined) { - current = current.nextDep + sub = link.sub + if (link.nextDep !== undefined) { + link = link.nextDep continue top } dirty = false @@ -338,18 +315,19 @@ export function checkDirty(current: Link): boolean { export function shallowPropagate(link: Link): void { do { const sub = link.sub + const nextSub = link.nextSub const subFlags = sub.flags if ( - (subFlags & (SubscriberFlags.Pending | SubscriberFlags.Dirty)) === - SubscriberFlags.Pending + (subFlags & (ReactiveFlags.Pending | ReactiveFlags.Dirty)) === + ReactiveFlags.Pending ) { - sub.flags = subFlags | SubscriberFlags.Dirty + sub.flags = subFlags | ReactiveFlags.Dirty } - link = link.nextSub! + link = nextSub! } while (link !== undefined) } -function isValidLink(checkLink: Link, sub: Subscriber): boolean { +function isValidLink(checkLink: Link, sub: ReactiveNode): boolean { const depsTail = sub.depsTail if (depsTail !== undefined) { let link = sub.deps!