]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(scheduler): use bitwise flags for scheduler jobs + optimize queueJob (#10407)
authorEvan You <yyx990803@gmail.com>
Mon, 26 Feb 2024 02:22:12 +0000 (10:22 +0800)
committerGitHub <noreply@github.com>
Mon, 26 Feb 2024 02:22:12 +0000 (10:22 +0800)
related: https://github.com/vuejs/core-vapor/pull/138

packages/reactivity/src/effect.ts
packages/runtime-core/__tests__/scheduler.spec.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/scheduler.ts

index 5a4d05268dc6ff530a5524e9255f050f38035e1f..b48463d3bf77bef2c32f9c17125dbe0069adc858 100644 (file)
@@ -126,10 +126,6 @@ export class ReactiveEffect<T = any>
    * @internal
    */
   nextEffect?: ReactiveEffect = undefined
-  /**
-   * @internal
-   */
-  allowRecurse?: boolean
 
   scheduler?: EffectScheduler = undefined
   onStop?: () => void
@@ -144,7 +140,10 @@ export class ReactiveEffect<T = any>
    * @internal
    */
   notify() {
-    if (this.flags & EffectFlags.RUNNING && !this.allowRecurse) {
+    if (
+      this.flags & EffectFlags.RUNNING &&
+      !(this.flags & EffectFlags.ALLOW_RECURSE)
+    ) {
       return
     }
     if (this.flags & EffectFlags.NO_BATCH) {
index 781aa6eb0b773ff97b1f9f97991f02363c3c12aa..079ced4bd1ad47762959bfe94d565bfe912773c6 100644 (file)
@@ -1,4 +1,6 @@
 import {
+  type SchedulerJob,
+  SchedulerJobFlags,
   flushPostFlushCbs,
   flushPreFlushCbs,
   invalidateJob,
@@ -119,12 +121,12 @@ describe('scheduler', () => {
       const job1 = () => {
         calls.push('job1')
       }
-      const cb1 = () => {
+      const cb1: SchedulerJob = () => {
         // queueJob in postFlushCb
         calls.push('cb1')
         queueJob(job1)
       }
-      cb1.pre = true
+      cb1.flags! |= SchedulerJobFlags.PRE
 
       queueJob(cb1)
       await nextTick()
@@ -138,25 +140,25 @@ describe('scheduler', () => {
       }
       job1.id = 1
 
-      const cb1 = () => {
+      const cb1: SchedulerJob = () => {
         calls.push('cb1')
         queueJob(job1)
         // cb2 should execute before the job
         queueJob(cb2)
         queueJob(cb3)
       }
-      cb1.pre = true
+      cb1.flags! |= SchedulerJobFlags.PRE
 
-      const cb2 = () => {
+      const cb2: SchedulerJob = () => {
         calls.push('cb2')
       }
-      cb2.pre = true
+      cb2.flags! |= SchedulerJobFlags.PRE
       cb2.id = 1
 
-      const cb3 = () => {
+      const cb3: SchedulerJob = () => {
         calls.push('cb3')
       }
-      cb3.pre = true
+      cb3.flags! |= SchedulerJobFlags.PRE
       cb3.id = 1
 
       queueJob(cb1)
@@ -166,37 +168,37 @@ describe('scheduler', () => {
 
     it('should insert jobs after pre jobs with the same id', async () => {
       const calls: string[] = []
-      const job1 = () => {
+      const job1: SchedulerJob = () => {
         calls.push('job1')
       }
       job1.id = 1
-      job1.pre = true
-      const job2 = () => {
+      job1.flags! |= SchedulerJobFlags.PRE
+      const job2: SchedulerJob = () => {
         calls.push('job2')
         queueJob(job5)
         queueJob(job6)
       }
       job2.id = 2
-      job2.pre = true
-      const job3 = () => {
+      job2.flags! |= SchedulerJobFlags.PRE
+      const job3: SchedulerJob = () => {
         calls.push('job3')
       }
       job3.id = 2
-      job3.pre = true
-      const job4 = () => {
+      job3.flags! |= SchedulerJobFlags.PRE
+      const job4: SchedulerJob = () => {
         calls.push('job4')
       }
       job4.id = 3
-      job4.pre = true
-      const job5 = () => {
+      job4.flags! |= SchedulerJobFlags.PRE
+      const job5: SchedulerJob = () => {
         calls.push('job5')
       }
       job5.id = 2
-      const job6 = () => {
+      const job6: SchedulerJob = () => {
         calls.push('job6')
       }
       job6.id = 2
-      job6.pre = true
+      job6.flags! |= SchedulerJobFlags.PRE
 
       // We need several jobs to test this properly, otherwise
       // findInsertionIndex can yield the correct index by chance
@@ -221,16 +223,16 @@ describe('scheduler', () => {
         flushPreFlushCbs()
         calls.push('job1')
       }
-      const cb1 = () => {
+      const cb1: SchedulerJob = () => {
         calls.push('cb1')
         // a cb triggers its parent job, which should be skipped
         queueJob(job1)
       }
-      cb1.pre = true
-      const cb2 = () => {
+      cb1.flags! |= SchedulerJobFlags.PRE
+      const cb2: SchedulerJob = () => {
         calls.push('cb2')
       }
-      cb2.pre = true
+      cb2.flags! |= SchedulerJobFlags.PRE
 
       queueJob(job1)
       await nextTick()
@@ -240,8 +242,8 @@ describe('scheduler', () => {
     // #3806
     it('queue preFlushCb inside postFlushCb', async () => {
       const spy = vi.fn()
-      const cb = () => spy()
-      cb.pre = true
+      const cb: SchedulerJob = () => spy()
+      cb.flags! |= SchedulerJobFlags.PRE
       queuePostFlushCb(() => {
         queueJob(cb)
       })
@@ -521,25 +523,25 @@ describe('scheduler', () => {
   test('should allow explicitly marked jobs to trigger itself', async () => {
     // normal job
     let count = 0
-    const job = () => {
+    const job: SchedulerJob = () => {
       if (count < 3) {
         count++
         queueJob(job)
       }
     }
-    job.allowRecurse = true
+    job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
     queueJob(job)
     await nextTick()
     expect(count).toBe(3)
 
     // post cb
-    const cb = () => {
+    const cb: SchedulerJob = () => {
       if (count < 5) {
         count++
         queuePostFlushCb(cb)
       }
     }
-    cb.allowRecurse = true
+    cb.flags! |= SchedulerJobFlags.ALLOW_RECURSE
     queuePostFlushCb(cb)
     await nextTick()
     expect(count).toBe(5)
@@ -572,7 +574,7 @@ describe('scheduler', () => {
     // simulate parent component that toggles child
     const job1 = () => {
       // @ts-expect-error
-      job2.active = false
+      job2.flags! |= SchedulerJobFlags.DISPOSED
     }
     // simulate child that's triggered by the same reactive change that
     // triggers its toggle
@@ -589,11 +591,11 @@ describe('scheduler', () => {
 
   it('flushPreFlushCbs inside a pre job', async () => {
     const spy = vi.fn()
-    const job = () => {
+    const job: SchedulerJob = () => {
       spy()
       flushPreFlushCbs()
     }
-    job.pre = true
+    job.flags! |= SchedulerJobFlags.PRE
     queueJob(job)
     await nextTick()
     expect(spy).toHaveBeenCalledTimes(1)
index 556688ebf4bd948824f5ea7d18303e36ea7aaa33..4f69e3068c52977585972fe348473ec40116100a 100644 (file)
@@ -11,7 +11,7 @@ import {
   isRef,
   isShallow,
 } from '@vue/reactivity'
-import { type SchedulerJob, queueJob } from './scheduler'
+import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
 import {
   EMPTY_OBJ,
   NOOP,
@@ -382,7 +382,7 @@ function doWatch(
 
   // important: mark the job as a watcher callback so that scheduler knows
   // it is allowed to self-trigger (#1727)
-  job.allowRecurse = !!cb
+  if (cb) job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
 
   const effect = new ReactiveEffect(getter)
 
@@ -394,7 +394,7 @@ function doWatch(
     scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
   } else {
     // default: 'pre'
-    job.pre = true
+    job.flags! |= SchedulerJobFlags.PRE
     if (instance) job.id = instance.uid
     scheduler = () => queueJob(job)
   }
index bbb9c32f45e0f7cc78be9fa8c4c1238eacfc81f1..0497b9cb228aab6e25a4fd2d4dbd3987d88ecff8 100644 (file)
@@ -19,6 +19,7 @@ import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling'
 import { PatchFlags, ShapeFlags, isArray } from '@vue/shared'
 import { onBeforeUnmount, onMounted } from '../apiLifecycle'
 import type { RendererElement } from '../renderer'
+import { SchedulerJobFlags } from '../scheduler'
 
 type Hook<T = () => void> = T | T[]
 
@@ -231,7 +232,7 @@ const BaseTransitionImpl: ComponentOptions = {
             state.isLeaving = false
             // #6835
             // it also needs to be updated when active is undefined
-            if (instance.job.active !== false) {
+            if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
               instance.update()
             }
           }
index a437771d34a8b0b2bc8b622eaf697cba5c760b3e..0bba1bcb0e6e0ad7245d82ac3993315103e24646 100644 (file)
@@ -39,13 +39,19 @@ import {
 } from '@vue/shared'
 import {
   type SchedulerJob,
+  SchedulerJobFlags,
   flushPostFlushCbs,
   flushPreFlushCbs,
   invalidateJob,
   queueJob,
   queuePostFlushCb,
 } from './scheduler'
-import { ReactiveEffect, pauseTracking, resetTracking } from '@vue/reactivity'
+import {
+  EffectFlags,
+  ReactiveEffect,
+  pauseTracking,
+  resetTracking,
+} from '@vue/reactivity'
 import { updateProps } from './componentProps'
 import { updateSlots } from './componentSlots'
 import { popWarningContext, pushWarningContext, warn } from './warning'
@@ -2281,7 +2287,7 @@ function baseCreateRenderer(
     // setup has resolved.
     if (job) {
       // so that scheduler will no longer invoke it
-      job.active = false
+      job.flags! |= SchedulerJobFlags.DISPOSED
       unmount(subTree, instance, parentSuspense, doRemove)
     }
     // unmounted hook
@@ -2419,7 +2425,13 @@ function toggleRecurse(
   { effect, job }: ComponentInternalInstance,
   allowed: boolean,
 ) {
-  effect.allowRecurse = job.allowRecurse = allowed
+  if (allowed) {
+    effect.flags |= EffectFlags.ALLOW_RECURSE
+    job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
+  } else {
+    effect.flags &= ~EffectFlags.ALLOW_RECURSE
+    job.flags! &= ~SchedulerJobFlags.ALLOW_RECURSE
+  }
 }
 
 export function needTransition(
index 866f4de0fd46ce697493025227b6c3cd37ff549d..e41b9e6a7cb1745e3a5470f0ee21e8b732e468bc 100644 (file)
@@ -2,10 +2,9 @@ import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
 import { type Awaited, NOOP, isArray } from '@vue/shared'
 import { type ComponentInternalInstance, getComponentName } from './component'
 
-export interface SchedulerJob extends Function {
-  id?: number
-  pre?: boolean
-  active?: boolean
+export enum SchedulerJobFlags {
+  QUEUED = 1 << 0,
+  PRE = 1 << 1,
   /**
    * Indicates whether the effect is allowed to recursively trigger itself
    * when managed by the scheduler.
@@ -21,7 +20,17 @@ export interface SchedulerJob extends Function {
    * responsibility to perform recursive state mutation that eventually
    * stabilizes (#1727).
    */
-  allowRecurse?: boolean
+  ALLOW_RECURSE = 1 << 2,
+  DISPOSED = 1 << 3,
+}
+
+export interface SchedulerJob extends Function {
+  id?: number
+  /**
+   * flags can technically be undefined, but it can still be used in bitwise
+   * operations just like 0.
+   */
+  flags?: SchedulerJobFlags
   /**
    * Attached by renderer.ts when setting up a component's render effect
    * Used to obtain component information when reporting max recursive updates.
@@ -69,7 +78,10 @@ function findInsertionIndex(id: number) {
     const middle = (start + end) >>> 1
     const middleJob = queue[middle]
     const middleJobId = getId(middleJob)
-    if (middleJobId < id || (middleJobId === id && middleJob.pre)) {
+    if (
+      middleJobId < id ||
+      (middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
+    ) {
       start = middle + 1
     } else {
       end = middle
@@ -80,24 +92,22 @@ function findInsertionIndex(id: number) {
 }
 
 export function queueJob(job: SchedulerJob) {
-  // the dedupe search uses the startIndex argument of Array.includes()
-  // by default the search index includes the current job that is being run
-  // so it cannot recursively trigger itself again.
-  // if the job is a watch() callback, the search will start with a +1 index to
-  // allow it recursively trigger itself - it is the user's responsibility to
-  // ensure it doesn't end up in an infinite loop.
-  if (
-    !queue.length ||
-    !queue.includes(
-      job,
-      isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
-    )
-  ) {
+  if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
     if (job.id == null) {
       queue.push(job)
+    } else if (
+      // fast path when the job id is larger than the tail
+      !(job.flags! & SchedulerJobFlags.PRE) &&
+      job.id >= (queue[queue.length - 1]?.id || 0)
+    ) {
+      queue.push(job)
     } else {
       queue.splice(findInsertionIndex(job.id), 0, job)
     }
+
+    if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
+      job.flags! |= SchedulerJobFlags.QUEUED
+    }
     queueFlush()
   }
 }
@@ -118,14 +128,11 @@ export function invalidateJob(job: SchedulerJob) {
 
 export function queuePostFlushCb(cb: SchedulerJobs) {
   if (!isArray(cb)) {
-    if (
-      !activePostFlushCbs ||
-      !activePostFlushCbs.includes(
-        cb,
-        cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex,
-      )
-    ) {
+    if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
       pendingPostFlushCbs.push(cb)
+      if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
+        cb.flags! |= SchedulerJobFlags.QUEUED
+      }
     }
   } else {
     // if cb is an array, it is a component lifecycle hook which can only be
@@ -147,7 +154,7 @@ export function flushPreFlushCbs(
   }
   for (; i < queue.length; i++) {
     const cb = queue[i]
-    if (cb && cb.pre) {
+    if (cb && cb.flags! & SchedulerJobFlags.PRE) {
       if (instance && cb.id !== instance.uid) {
         continue
       }
@@ -157,6 +164,7 @@ export function flushPreFlushCbs(
       queue.splice(i, 1)
       i--
       cb()
+      cb.flags! &= ~SchedulerJobFlags.QUEUED
     }
   }
 }
@@ -191,6 +199,7 @@ export function flushPostFlushCbs(seen?: CountMap) {
         continue
       }
       activePostFlushCbs[postFlushIndex]()
+      activePostFlushCbs[postFlushIndex].flags! &= ~SchedulerJobFlags.QUEUED
     }
     activePostFlushCbs = null
     postFlushIndex = 0
@@ -203,8 +212,10 @@ const getId = (job: SchedulerJob): number =>
 const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
   const diff = getId(a) - getId(b)
   if (diff === 0) {
-    if (a.pre && !b.pre) return -1
-    if (b.pre && !a.pre) return 1
+    const isAPre = a.flags! & SchedulerJobFlags.PRE
+    const isBPre = b.flags! & SchedulerJobFlags.PRE
+    if (isAPre && !isBPre) return -1
+    if (isBPre && !isAPre) return 1
   }
   return diff
 }
@@ -237,11 +248,12 @@ function flushJobs(seen?: CountMap) {
   try {
     for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
       const job = queue[flushIndex]
-      if (job && job.active !== false) {
+      if (job && !(job.flags! & SchedulerJobFlags.DISPOSED)) {
         if (__DEV__ && check(job)) {
           continue
         }
         callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
+        job.flags! &= ~SchedulerJobFlags.QUEUED
       }
     }
   } finally {