Evan You 4 лет назад
Родитель
Сommit
c9136b1275

+ 6 - 0
src/core/instance/lifecycle.ts

@@ -218,6 +218,12 @@ export function mountComponent(
   // manually mounted instance, call mounted on self
   // mounted is called for render-created child components in its inserted hook
   if (vm.$vnode == null) {
+    const preWatchers = vm._preWatchers
+    if (preWatchers) {
+      for (let i = 0; i < preWatchers.length; i++) {
+        preWatchers[i].run()
+      }
+    }
     vm._isMounted = true
     callHook(vm, 'mounted')
   }

+ 29 - 22
src/core/observer/scheduler.ts

@@ -1,5 +1,6 @@
 import type Watcher from './watcher'
 import config from '../config'
+import Dep from './dep'
 import { callHook, activateChildComponent } from '../instance/lifecycle'
 
 import { warn, nextTick, devtools, inBrowser, isIE } from '../util/index'
@@ -119,12 +120,12 @@ function flushSchedulerQueue() {
   }
 }
 
-function callUpdatedHooks(queue) {
+function callUpdatedHooks(queue: Watcher[]) {
   let i = queue.length
   while (i--) {
     const watcher = queue[i]
     const vm = watcher.vm
-    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
+    if (vm && vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
       callHook(vm, 'updated')
     }
   }
@@ -155,28 +156,34 @@ function callActivatedHooks(queue) {
  */
 export function queueWatcher(watcher: Watcher) {
   const id = watcher.id
-  if (has[id] == null) {
-    has[id] = true
-    if (!flushing) {
-      queue.push(watcher)
-    } else {
-      // if already flushing, splice the watcher based on its id
-      // if already past its id, it will be run next immediately.
-      let i = queue.length - 1
-      while (i > index && queue[i].id > watcher.id) {
-        i--
-      }
-      queue.splice(i + 1, 0, watcher)
+  if (has[id] != null) {
+    return
+  }
+
+  if (watcher === Dep.target && watcher.noRecurse) {
+    return
+  }
+
+  has[id] = true
+  if (!flushing) {
+    queue.push(watcher)
+  } else {
+    // if already flushing, splice the watcher based on its id
+    // if already past its id, it will be run next immediately.
+    let i = queue.length - 1
+    while (i > index && queue[i].id > watcher.id) {
+      i--
     }
-    // queue the flush
-    if (!waiting) {
-      waiting = true
+    queue.splice(i + 1, 0, watcher)
+  }
+  // queue the flush
+  if (!waiting) {
+    waiting = true
 
-      if (__DEV__ && !config.async) {
-        flushSchedulerQueue()
-        return
-      }
-      nextTick(flushSchedulerQueue)
+    if (__DEV__ && !config.async) {
+      flushSchedulerQueue()
+      return
     }
+    nextTick(flushSchedulerQueue)
   }
 }

+ 1 - 0
src/core/observer/traverse.ts

@@ -12,6 +12,7 @@ const seenObjects = new Set()
 export function traverse(val: any) {
   _traverse(val, seenObjects)
   seenObjects.clear()
+  return val
 }
 
 function _traverse(val: any, seen: SimpleSet) {

+ 6 - 9
src/core/observer/watcher.ts

@@ -7,7 +7,6 @@ import {
   handleError,
   invokeWithErrorHandling,
   noop,
-  bind,
   isFunction
 } from '../util/index'
 
@@ -26,7 +25,7 @@ let uid = 0
  * This is used for both the $watch() api and directives.
  */
 export default class Watcher implements DepTarget {
-  vm: Component | null
+  vm?: Component | null
   expression: string
   cb: Function
   id: number
@@ -41,7 +40,8 @@ export default class Watcher implements DepTarget {
   depIds: SimpleSet
   newDepIds: SimpleSet
   before?: Function
-  scheduler?: Function
+  onStop?: Function
+  noRecurse?: boolean
   getter: Function
   value: any
 
@@ -72,10 +72,6 @@ export default class Watcher implements DepTarget {
       this.lazy = !!options.lazy
       this.sync = !!options.sync
       this.before = options.before
-      if ((this.scheduler = options.scheduler)) {
-        // @ts-ignore
-        this.run = bind(this.run, this)
-      }
     } else {
       this.deep = this.user = this.lazy = this.sync = false
     }
@@ -179,8 +175,6 @@ export default class Watcher implements DepTarget {
       this.dirty = true
     } else if (this.sync) {
       this.run()
-    } else if (this.scheduler) {
-      this.scheduler(this.run)
     } else {
       queueWatcher(this)
     }
@@ -255,6 +249,9 @@ export default class Watcher implements DepTarget {
         this.deps[i].removeSub(this)
       }
       this.active = false
+      if (this.onStop) {
+        this.onStop()
+      }
     }
   }
 }

+ 70 - 41
src/v3/apiWatch.ts

@@ -7,18 +7,15 @@ import {
   isArray,
   isFunction,
   emptyObject,
-  remove,
   hasChanged,
   isServerRendering,
   invokeWithErrorHandling
 } from 'core/util'
 import { currentInstance } from './currentInstance'
 import { traverse } from 'core/observer/traverse'
-import {
-  EffectScheduler,
-  ReactiveEffect,
-  DebuggerEvent
-} from './reactivity/effect'
+import Watcher from '../core/observer/watcher'
+import { queueWatcher } from '../core/observer/scheduler'
+import { TrackOpTypes, TriggerOpTypes } from './reactivity/operations'
 
 const WATCHER = `watcher`
 const WATCHER_CB = `${WATCHER} callback`
@@ -58,6 +55,19 @@ export interface DebuggerOptions {
   onTrigger?: (event: DebuggerEvent) => void
 }
 
+export type DebuggerEvent = {
+  watcher: Watcher
+} & DebuggerEventExtraInfo
+
+export type DebuggerEventExtraInfo = {
+  target: object
+  type: TrackOpTypes | TriggerOpTypes
+  key: any
+  newValue?: any
+  oldValue?: any
+  oldTarget?: Map<any, any> | Set<any>
+}
+
 export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
   immediate?: Immediate
   deep?: boolean
@@ -162,7 +172,13 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
 function doWatch(
   source: WatchSource | WatchSource[] | WatchEffect | object,
   cb: WatchCallback | null,
-  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = emptyObject
+  {
+    immediate,
+    deep,
+    flush = 'pre',
+    onTrack,
+    onTrigger
+  }: WatchOptions = emptyObject
 ): WatchStopHandle {
   if (__DEV__ && !cb) {
     if (immediate !== undefined) {
@@ -200,7 +216,12 @@ function doWatch(
     getter = () => source.value
     forceTrigger = isShallow(source)
   } else if (isReactive(source)) {
-    getter = () => source
+    getter = isArray(source)
+      ? () => {
+          ;(source as any).__ob__.dep.depend()
+          return source
+        }
+      : () => source
     deep = true
   } else if (isArray(source)) {
     isMultiSource = true
@@ -245,7 +266,7 @@ function doWatch(
 
   let cleanup: () => void
   let onCleanup: OnCleanup = (fn: () => void) => {
-    cleanup = effect.onStop = () => {
+    cleanup = watcher.onStop = () => {
       call(fn, WATCHER_CLEANUP)
     }
   }
@@ -267,14 +288,23 @@ function doWatch(
     return noop
   }
 
+  const watcher = new Watcher(currentInstance, getter, noop, {
+    lazy: true
+  })
+  watcher.noRecurse = !cb
+
   let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
-  const job = () => {
-    if (!effect.active) {
+  // overwrite default run
+  watcher.run = () => {
+    if (
+      !watcher.active &&
+      !(flush === 'pre' && instance && instance._isBeingDestroyed)
+    ) {
       return
     }
     if (cb) {
       // watch(source, cb)
-      const newValue = effect.run()
+      const newValue = watcher.get()
       if (
         deep ||
         forceTrigger ||
@@ -298,52 +328,51 @@ function doWatch(
       }
     } else {
       // watchEffect
-      effect.run()
+      watcher.get()
     }
   }
 
-  let scheduler: EffectScheduler
   if (flush === 'sync') {
-    scheduler = job as any // the scheduler function gets called directly
+    watcher.update = watcher.run
   } else if (flush === 'post') {
-    scheduler = () => queuePostRenderEffect(job)
+    watcher.id = Infinity
+    watcher.update = () => queueWatcher(watcher)
   } else {
-    // default: 'pre'
-    scheduler = () => queuePreFlushCb(job)
+    // pre
+    watcher.update = () => {
+      if (!instance || instance._isMounted) {
+        queueWatcher(watcher)
+      } else {
+        const buffer = instance._preWatchers || (instance._preWatchers = [])
+        if (buffer.indexOf(watcher) < 0) buffer.push(watcher)
+      }
+    }
   }
 
-  const effect = new ReactiveEffect(getter, scheduler)
-
-  if (__DEV__) {
-    effect.onTrack = onTrack
-    effect.onTrigger = onTrigger
-  }
+  // TODO
+  // if (__DEV__) {
+  //   effect.onTrack = onTrack
+  //   effect.onTrigger = onTrigger
+  // }
 
   // initial run
   if (cb) {
     if (immediate) {
-      job()
+      watcher.run()
     } else {
-      oldValue = effect.run()
+      oldValue = watcher.get()
     }
-  } else if (flush === 'post') {
-    queuePostRenderEffect(effect.run.bind(effect))
+  } else if (flush === 'post' && instance) {
+    instance.$once('hook:mounted', () => watcher.get())
   } else {
-    effect.run()
+    watcher.get()
   }
 
   return () => {
-    effect.stop()
-    if (instance && instance.scope) {
-      remove(instance.scope.effects!, effect)
-    }
+    watcher.teardown()
+    // TODO
+    // if (instance && instance.scope) {
+    //   remove(instance.scope.effects!, effect)
+    // }
   }
 }
-
-function queuePostRenderEffect(fn: Function) {
-  // TODO
-}
-
-function queuePreFlushCb(fn: Function) {
-  // TODO
-}

+ 2 - 2
src/v3/index.ts

@@ -53,10 +53,10 @@ export {
   WatchCallback,
   WatchSource,
   WatchStopHandle,
-  DebuggerOptions
+  DebuggerOptions,
+  DebuggerEvent
 } from './apiWatch'
 
-export { DebuggerEvent } from './reactivity/effect'
 export { TrackOpTypes, TriggerOpTypes } from './reactivity/operations'
 
 export { h } from './h'

+ 14 - 56
src/v3/reactivity/effect.ts

@@ -1,62 +1,20 @@
 import Watcher from 'core/observer/watcher'
 import { noop } from 'shared/util'
 import { currentInstance } from '../currentInstance'
-import { TrackOpTypes, TriggerOpTypes } from './operations'
 
-export type EffectScheduler = (...args: any[]) => any
-
-export type DebuggerEvent = {
-  effect: ReactiveEffect
-} & DebuggerEventExtraInfo
-
-export type DebuggerEventExtraInfo = {
-  target: object
-  type: TrackOpTypes | TriggerOpTypes
-  key: any
-  newValue?: any
-  oldValue?: any
-  oldTarget?: Map<any, any> | Set<any>
-}
-
-export class ReactiveEffect<T = any> {
-  // TODO
-  onStop?: () => void
-  // dev only
-  onTrack?: (event: DebuggerEvent) => void
-  // dev only
-  onTrigger?: (event: DebuggerEvent) => void
-
-  active = true
-  /**
-   * @private
-   */
-  _watcher: Watcher
-
-  constructor(
-    public fn: () => T,
-    scheduler?: EffectScheduler // TODO scope?: EffectScope
-  ) {
-    // TODO recordEffectScope(this, scope)
-    // TODO debug options
-    this._watcher = new Watcher(currentInstance, fn, noop, {
-      // always force trigger update if has scheduler
-      deep: true,
-      sync: !scheduler,
-      scheduler
-    })
-  }
-
-  run() {
-    this._watcher.run()
-    return this._watcher.value
-  }
-
-  stop() {
-    this._watcher.teardown()
-    this.active = false
+// export type EffectScheduler = (...args: any[]) => any
+
+/**
+ * @private since we are not exposing this in Vue 2, it's used only for
+ * internal testing.
+ */
+export function effect(fn: () => any, scheduler?: (cb: any) => void) {
+  const watcher = new Watcher(currentInstance, fn, noop, {
+    sync: true
+  })
+  if (scheduler) {
+    watcher.update = () => {
+      scheduler(() => watcher.run())
+    }
   }
 }
-
-// since we are not exposing this in Vue 2, it's used only for internal testing.
-export const effect = (fn: () => any, scheduler?: any) =>
-  new ReactiveEffect(fn, scheduler)

+ 88 - 129
test/unit/features/v3/apiWatch.spec.ts

@@ -7,19 +7,13 @@ import {
   reactive,
   computed,
   ref,
-  // DebuggerEvent,
-  // TrackOpTypes,
-  // TriggerOpTypes,
   triggerRef,
   shallowRef,
-  Ref,
   h,
-  getCurrentInstance,
   onMounted
-  // effectScope
 } from 'v3'
 import { nextTick } from 'core/util'
-import { Component } from 'typescript/component'
+import { set } from 'core/observer'
 
 // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
 
@@ -73,7 +67,7 @@ describe('api: watch', () => {
   })
 
   it('watching single source: array', async () => {
-    const array = reactive([] as number[])
+    const array = reactive({ a: [] as number[] }).a
     const spy = vi.fn()
     watch(array, spy)
     array.push(1)
@@ -302,8 +296,9 @@ describe('api: watch', () => {
       callCount++
       // on mount, the watcher callback should be called before DOM render
       // on update, should be called before the count is updated
-      const expectedDOM = callCount === 1 ? `` : `${count - 1}`
-      result1 = root.innerHTML === expectedDOM
+      const expectedDOM =
+        callCount === 1 ? `<div></div>` : `<div>${count - 1}</div>`
+      result1 = container.innerHTML === expectedDOM
 
       // in a pre-flush callback, all state should have been updated
       const expectedState = callCount - 1
@@ -315,10 +310,12 @@ describe('api: watch', () => {
         watchEffect(() => {
           assertion(count.value, count2.value)
         })
-        return () => count.value
+        return () => h('div', count.value)
       }
     }
+    const container = document.createElement('div')
     const root = document.createElement('div')
+    container.appendChild(root)
     new Vue(Comp).$mount(root)
     expect(assertion).toHaveBeenCalledTimes(1)
     expect(result1).toBe(true)
@@ -337,7 +334,7 @@ describe('api: watch', () => {
     const count = ref(0)
     let result
     const assertion = vi.fn(count => {
-      result = root.innerHTML === `${count}`
+      result = container.innerHTML === `<div>${count}</div>`
     })
 
     const Comp = {
@@ -348,10 +345,12 @@ describe('api: watch', () => {
           },
           { flush: 'post' }
         )
-        return () => count.value
+        return () => h('div', count.value)
       }
     }
+    const container = document.createElement('div')
     const root = document.createElement('div')
+    container.appendChild(root)
     new Vue(Comp).$mount(root)
     expect(assertion).toHaveBeenCalledTimes(1)
     expect(result).toBe(true)
@@ -366,7 +365,7 @@ describe('api: watch', () => {
     const count = ref(0)
     let result
     const assertion = vi.fn(count => {
-      result = root.innerHTML === `${count}`
+      result = container.innerHTML === `<div>${count}</div>`
     })
 
     const Comp = {
@@ -374,10 +373,12 @@ describe('api: watch', () => {
         watchPostEffect(() => {
           assertion(count.value)
         })
-        return () => count.value
+        return () => h('div', count.value)
       }
     }
+    const container = document.createElement('div')
     const root = document.createElement('div')
+    container.appendChild(root)
     new Vue(Comp).$mount(root)
     expect(assertion).toHaveBeenCalledTimes(1)
     expect(result).toBe(true)
@@ -399,8 +400,9 @@ describe('api: watch', () => {
       callCount++
       // on mount, the watcher callback should be called before DOM render
       // on update, should be called before the count is updated
-      const expectedDOM = callCount === 1 ? `` : `${count - 1}`
-      result1 = root.innerHTML === expectedDOM
+      const expectedDOM =
+        callCount === 1 ? `<div></div>` : `<div>${count - 1}</div>`
+      result1 = container.innerHTML === expectedDOM
 
       // in a sync callback, state mutation on the next line should not have
       // executed yet on the 2nd call, but will be on the 3rd call.
@@ -418,10 +420,12 @@ describe('api: watch', () => {
             flush: 'sync'
           }
         )
-        return () => count.value
+        return () => h('div', count.value)
       }
     }
+    const container = document.createElement('div')
     const root = document.createElement('div')
+    container.appendChild(root)
     new Vue(Comp).$mount(root)
     expect(assertion).toHaveBeenCalledTimes(1)
     expect(result1).toBe(true)
@@ -446,8 +450,9 @@ describe('api: watch', () => {
       callCount++
       // on mount, the watcher callback should be called before DOM render
       // on update, should be called before the count is updated
-      const expectedDOM = callCount === 1 ? `` : `${count - 1}`
-      result1 = root.innerHTML === expectedDOM
+      const expectedDOM =
+        callCount === 1 ? `<div></div>` : `<div>${count - 1}</div>`
+      result1 = container.innerHTML === expectedDOM
 
       // in a sync callback, state mutation on the next line should not have
       // executed yet on the 2nd call, but will be on the 3rd call.
@@ -460,10 +465,12 @@ describe('api: watch', () => {
         watchSyncEffect(() => {
           assertion(count.value)
         })
-        return () => count.value
+        return () => h('div', count.value)
       }
     }
+    const container = document.createElement('div')
     const root = document.createElement('div')
+    container.appendChild(root)
     new Vue(Comp).$mount(root)
     expect(assertion).toHaveBeenCalledTimes(1)
     expect(result1).toBe(true)
@@ -491,7 +498,7 @@ describe('api: watch', () => {
         return toggle.value ? h(Comp) : null
       }
     }
-    new Vue(App).$mount(document.createElement('div'))
+    new Vue(App).$mount()
     expect(cb).not.toHaveBeenCalled()
     toggle.value = false
     await nextTick()
@@ -512,14 +519,14 @@ describe('api: watch', () => {
         return toggle.value ? h(Comp) : null
       }
     }
-    new Vue(App).$mount(document.createElement('div'))
+    new Vue(App).$mount()
     expect(cb).not.toHaveBeenCalled()
     toggle.value = false
     await nextTick()
     expect(cb).toHaveBeenCalledTimes(1)
   })
 
-  // #1763
+  // vuejs/core#1763
   it('flush: pre watcher watching props should fire before child update', async () => {
     const a = ref(0)
     const b = ref(0)
@@ -538,7 +545,7 @@ describe('api: watch', () => {
           { flush: 'pre' }
         )
 
-        // #1777 chained pre-watcher
+        // vuejs/core#1777 chained pre-watcher
         watch(
           c,
           () => {
@@ -559,7 +566,7 @@ describe('api: watch', () => {
       }
     }
 
-    new Vue(App).$mount(document.createElement('div'))
+    new Vue(App).$mount()
     expect(calls).toEqual(['render'])
 
     // both props are updated
@@ -571,7 +578,7 @@ describe('api: watch', () => {
     expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
   })
 
-  // #5721
+  // vuejs/core#5721
   it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
     const count = ref(0)
     const calls: string[] = []
@@ -594,12 +601,13 @@ describe('api: watch', () => {
         count.value++
       }
     }
-    new Vue(App).$mount(document.createElement('div'))
+    new Vue(App).$mount()
     expect(calls).toMatchObject(['watch 3', 'mounted'])
   })
 
-  // #1852
-  it('flush: post watcher should fire after template refs updated', async () => {
+  // TODO
+  // vuejs/core#1852
+  it.skip('flush: post watcher should fire after template refs updated', async () => {
     const toggle = ref(false)
     let dom: HTMLElement | null = null
 
@@ -621,7 +629,7 @@ describe('api: watch', () => {
       }
     }
 
-    new Vue(App).$mount(document.createElement('div'))
+    new Vue(App).$mount()
     expect(dom).toBe(null)
 
     toggle.value = true
@@ -634,12 +642,12 @@ describe('api: watch', () => {
       nested: {
         count: ref(0)
       },
-      array: [1, 2, 3],
-      map: new Map([
-        ['a', 1],
-        ['b', 2]
-      ]),
-      set: new Set([1, 2, 3])
+      array: [1, 2, 3]
+      // map: new Map([
+      //   ['a', 1],
+      //   ['b', 2]
+      // ]),
+      // set: new Set([1, 2, 3])
     })
 
     let dummy
@@ -648,9 +656,9 @@ describe('api: watch', () => {
       state => {
         dummy = [
           state.nested.count,
-          state.array[0],
-          state.map.get('a'),
-          state.set.has(1)
+          state.array[0]
+          // state.map.get('a'),
+          // state.set.has(1)
         ]
       },
       { deep: true }
@@ -658,34 +666,34 @@ describe('api: watch', () => {
 
     state.nested.count++
     await nextTick()
-    expect(dummy).toEqual([1, 1, 1, true])
+    expect(dummy).toEqual([1, 1])
 
     // nested array mutation
-    state.array[0] = 2
+    set(state.array, 0, 2)
     await nextTick()
-    expect(dummy).toEqual([1, 2, 1, true])
+    expect(dummy).toEqual([1, 2])
 
     // nested map mutation
-    state.map.set('a', 2)
-    await nextTick()
-    expect(dummy).toEqual([1, 2, 2, true])
+    // state.map.set('a', 2)
+    // await nextTick()
+    // expect(dummy).toEqual([1, 2, 2, true])
 
     // nested set mutation
-    state.set.delete(1)
-    await nextTick()
-    expect(dummy).toEqual([1, 2, 2, false])
+    // state.set.delete(1)
+    // await nextTick()
+    // expect(dummy).toEqual([1, 2, 2, false])
   })
 
   it('watching deep ref', async () => {
     const count = ref(0)
     const double = computed(() => count.value * 2)
-    const state = reactive([count, double])
+    const state = reactive({ count, double })
 
     let dummy
     watch(
       () => state,
       state => {
-        dummy = [state[0].value, state[1].value]
+        dummy = [state.count, state.double]
       },
       { deep: true }
     )
@@ -897,7 +905,7 @@ describe('api: watch', () => {
     expect(spy).toHaveBeenCalledTimes(1)
   })
 
-  // #2125
+  // vuejs/core#2125
   test('watchEffect should not recursively trigger itself', async () => {
     const spy = vi.fn()
     const price = ref(10)
@@ -910,7 +918,7 @@ describe('api: watch', () => {
     expect(spy).toHaveBeenCalledTimes(1)
   })
 
-  // #2231
+  // vuejs/core#2231
   test('computed refs should not trigger watch if value has no change', async () => {
     const spy = vi.fn()
     const source = ref(0)
@@ -923,59 +931,6 @@ describe('api: watch', () => {
     expect(spy).toHaveBeenCalledTimes(1)
   })
 
-  // TODO
-  // https://github.com/vuejs/core/issues/2381
-  test('$watch should always register its effects with its own instance', async () => {
-    let instance: Component | null
-    let _show: Ref<boolean>
-
-    const Child = {
-      render: () => h('div'),
-      mounted() {
-        instance = getCurrentInstance()!.proxy
-      },
-      unmounted() {}
-    }
-
-    const Comp = {
-      setup() {
-        const comp = ref<Component | undefined>()
-        const show = ref(true)
-        _show = show
-        return { comp, show }
-      },
-      render() {
-        return this.show
-          ? h(Child, {
-              ref: vm => void (this.comp = vm as Component)
-            })
-          : null
-      },
-      mounted() {
-        // this call runs while Comp is currentInstance, but
-        // the effect for this `$watch` should nontheless be registered with Child
-        this.comp!.$watch(
-          () => this.show,
-          () => void 0
-        )
-      }
-    }
-
-    new Vue(Comp).$mount(document.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)
-
-    _show!.value = false
-
-    await nextTick()
-    await nextTick()
-
-    expect(instance!.scope.effects[0].active).toBe(false)
-  })
-
   test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
     let instance: any
     const source = vi.fn()
@@ -1011,17 +966,19 @@ describe('api: watch', () => {
     expect(source.mock.calls[0]).toMatchObject([])
   })
 
-  // #2728
+  // vuejs/core#2728
   test('pre watcher callbacks should not track dependencies', async () => {
     const a = ref(0)
     const b = ref(0)
     const updated = vi.fn()
+    const cb = vi.fn()
 
     const Child = {
       props: ['a'],
       updated,
       watch: {
         a() {
+          cb()
           b.value
         }
       },
@@ -1032,7 +989,7 @@ describe('api: watch', () => {
 
     const Parent = {
       render() {
-        return h(Child, { a: a.value })
+        return h(Child, { props: { a: a.value } })
       }
     }
 
@@ -1042,11 +999,13 @@ describe('api: watch', () => {
     a.value++
     await nextTick()
     expect(updated).toHaveBeenCalledTimes(1)
+    expect(cb).toHaveBeenCalledTimes(1)
 
     b.value++
     await nextTick()
     // should not track b as dependency of Child
     expect(updated).toHaveBeenCalledTimes(1)
+    expect(cb).toHaveBeenCalledTimes(1)
   })
 
   test('watching keypath', async () => {
@@ -1102,26 +1061,26 @@ describe('api: watch', () => {
     expect(count).toBe(0)
   })
 
-  // #4158
+  // vuejs/core#4158
   // TODO
-  test.skip('watch should not register in owner component if created inside detached scope', () => {
-    let instance: Component
-    const Comp = {
-      setup() {
-        instance = getCurrentInstance()!.proxy
-        effectScope(true).run(() => {
-          watch(
-            () => 1,
-            () => {}
-          )
-        })
-        return () => ''
-      }
-    }
-    const root = document.createElement('div')
-    new Vue(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)
-  })
+  // test.skip('watch should not register in owner component if created inside detached scope', () => {
+  //   let instance: Component
+  //   const Comp = {
+  //     setup() {
+  //       instance = getCurrentInstance()!.proxy
+  //       effectScope(true).run(() => {
+  //         watch(
+  //           () => 1,
+  //           () => {}
+  //         )
+  //       })
+  //       return () => ''
+  //     }
+  //   }
+  //   const root = document.createElement('div')
+  //   new Vue(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)
+  // })
 })

+ 1 - 0
typescript/component.d.ts

@@ -101,6 +101,7 @@ export declare class Component {
   _setupState?: Record<string, any>
   _attrsProxy?: Record<string, any>
   _slotsProxy?: Record<string, () => VNode[]>
+  _preWatchers?: Watcher[]
 
   // private methods