瀏覽代碼

wip: rename dir to v3

Evan You 3 年之前
父節點
當前提交
1f1046b7e5

+ 1 - 1
package.json

@@ -28,7 +28,7 @@
     "test:e2e": "npm run build -- web-full-prod,web-server-renderer-basic && vitest run test/e2e",
     "test:transition": "karma start test/transition/karma.conf.js",
     "test:types": "tsc -p ./types/tsconfig.json",
-    "format": "prettier --write --parser typescript \"(src|test|packages)/**/*.ts\"",
+    "format": "prettier --write --parser typescript \"(src|test|packages|types)/**/*.ts\"",
     "ts-check": "tsc -p tsconfig.json --noEmit",
     "ts-check:test": "tsc -p test/tsconfig.json --noEmit",
     "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",

+ 0 - 12
src/composition-api/currentInstance.ts

@@ -1,12 +0,0 @@
-import { Component } from 'typescript/component'
-
-// TODO set this
-export let currentInstance: Component | null = null
-
-export function getCurrentInstance(): Component | null {
-  return currentInstance
-}
-
-export function setCurrentInstance(vm: Component | null) {
-  currentInstance = vm
-}

+ 1 - 1
src/core/observer/index.ts

@@ -15,7 +15,7 @@ import {
   isServerRendering,
   hasChanged
 } from '../util/index'
-import { isReadonly, isRef } from '../../composition-api'
+import { isReadonly, isRef } from '../../v3'
 
 const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
 

+ 2 - 1
src/core/util/debug.ts

@@ -1,6 +1,7 @@
 import config from '../config'
 import { noop, isArray } from 'shared/util'
 import type { Component } from 'typescript/component'
+import { currentInstance } from 'v3/currentInstance'
 
 export let warn = noop
 export let tip = noop
@@ -13,7 +14,7 @@ if (__DEV__) {
   const classify = str =>
     str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
 
-  warn = (msg, vm) => {
+  warn = (msg, vm = currentInstance) => {
     // TODO get current instance
     const trace = vm ? generateComponentTrace(vm) : ''
 

+ 1 - 1
src/platforms/web/entry-runtime-esm.ts

@@ -2,4 +2,4 @@ import Vue from './runtime/index'
 
 export default Vue
 
-export * from 'vca/index'
+export * from 'v3'

+ 1 - 1
src/platforms/web/entry-runtime-with-compiler-esm.ts

@@ -2,4 +2,4 @@ import Vue from './runtime-with-compiler'
 
 export default Vue
 
-export * from 'vca/index'
+export * from 'v3'

+ 2 - 2
src/platforms/web/entry-runtime-with-compiler.ts

@@ -1,10 +1,10 @@
 import Vue from './runtime-with-compiler'
-import * as vca from 'vca/index'
+import * as vca from 'v3'
 import { extend } from 'shared/util'
 
 extend(Vue, vca)
 
-import { effect } from 'vca/reactivity/effect'
+import { effect } from 'v3/reactivity/effect'
 Vue.effect = effect
 
 export default Vue

+ 1 - 1
src/platforms/web/entry-runtime.ts

@@ -1,5 +1,5 @@
 import Vue from './runtime/index'
-import * as vca from 'vca/index'
+import * as vca from 'v3'
 import { extend } from 'shared/util'
 
 extend(Vue, vca)

+ 0 - 0
src/composition-api/apiInject.ts → src/v3/apiInject.ts


+ 0 - 0
src/composition-api/apiLifecycle.ts → src/v3/apiLifecycle.ts


+ 0 - 0
src/composition-api/apiWatch.ts → src/v3/apiWatch.ts


+ 21 - 0
src/v3/currentInstance.ts

@@ -0,0 +1,21 @@
+import { Component } from 'typescript/component'
+
+export let currentInstance: Component | null = null
+
+/**
+ * This is exposed for compatibility with v3 (e.g. some functions in VueUse
+ * relies on it). Do not use this internally, just use `currentInstance`.
+ *
+ * @private this function needs manual type declaration because it relies
+ * on previously manually authored types from Vue 2
+ */
+export function getCurrentInstance(): { proxy: Component } | null {
+  return currentInstance && { proxy: currentInstance }
+}
+
+/**
+ * @private
+ */
+export function setCurrentInstance(vm: Component | null) {
+  currentInstance = vm
+}

+ 27 - 0
src/v3/h.ts

@@ -0,0 +1,27 @@
+import { createElement } from '../core/vdom/create-element'
+import VNode from 'core/vdom/vnode'
+import { VNodeData, VNodeChildren } from 'typescript/vnode'
+import { currentInstance } from './currentInstance'
+import { warn } from 'core/util'
+
+/**
+ * @private this function needs manual type declaration because it relies
+ * on previously manually authored types from Vue 2
+ */
+export function h(type: any, children?: VNodeChildren): VNode
+export function h(
+  type: any,
+  props?: VNodeData | null,
+  children?: VNodeChildren
+): VNode
+
+export function h(type: any, props?: any, children?: any) {
+  if (!currentInstance) {
+    __DEV__ &&
+      warn(
+        `globally imported h() can only be invoked when there is an active ` +
+          `component instance, e.g. synchronously in a component's render or setup function.`
+      )
+  }
+  return createElement(currentInstance!, type, props, children, 2, true)
+}

+ 14 - 1
src/composition-api/index.ts → src/v3/index.ts

@@ -46,5 +46,18 @@ export {
   watch,
   watchEffect,
   watchPostEffect,
-  watchSyncEffect
+  watchSyncEffect,
+  WatchEffect,
+  WatchOptions,
+  WatchOptionsBase,
+  WatchCallback,
+  WatchSource,
+  WatchStopHandle,
+  DebuggerOptions
 } from './apiWatch'
+
+export { DebuggerEvent } from './reactivity/effect'
+export { TrackOpTypes, TriggerOpTypes } from './reactivity/operations'
+
+export { h } from './h'
+export { getCurrentInstance } from './currentInstance'

+ 0 - 0
src/composition-api/reactivity/computed.ts → src/v3/reactivity/computed.ts


+ 0 - 0
src/composition-api/reactivity/effect.ts → src/v3/reactivity/effect.ts


+ 0 - 0
src/composition-api/reactivity/operations.ts → src/v3/reactivity/operations.ts


+ 0 - 0
src/composition-api/reactivity/reactive.ts → src/v3/reactivity/reactive.ts


+ 0 - 0
src/composition-api/reactivity/readonly.ts → src/v3/reactivity/readonly.ts


+ 0 - 0
src/composition-api/reactivity/ref.ts → src/v3/reactivity/ref.ts


+ 3 - 4
test/tsconfig.json

@@ -23,14 +23,13 @@
     "paths": {
       "compiler/*": ["../src/compiler/*"],
       "core/*": ["../src/core/*"],
-
       "server/*": ["../src/server/*"],
       "sfc/*": ["../src/sfc/*"],
       "shared/*": ["../src/shared/*"],
-
       "web/*": ["../src/platforms/web/*"],
-      "vca/*": ["../src/composition-api/*"],
-
+      "v3": ["../src/v3/index"],
+      "v3/*": ["../src/v3/*"],
+      "typescript/*": ["../typescript/*"],
       "vue": ["../src/platforms/web/entry-runtime-with-compiler"]
     }
   },

+ 1126 - 0
test/unit/features/v3/apiWatch.spec.ts

@@ -0,0 +1,1126 @@
+import Vue from 'vue'
+import {
+  watch,
+  watchEffect,
+  watchPostEffect,
+  watchSyncEffect,
+  reactive,
+  computed,
+  ref,
+  DebuggerEvent,
+  TrackOpTypes,
+  TriggerOpTypes,
+  triggerRef,
+  shallowRef,
+  Ref,
+  h,
+  getCurrentInstance
+  // effectScope
+} from 'v3'
+import { nextTick } from 'core/util'
+import { Component } from 'typescript/component'
+
+// reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
+
+describe('api: watch', () => {
+  it('effect', async () => {
+    const state = reactive({ count: 0 })
+    let dummy
+    watchEffect(() => {
+      dummy = state.count
+    })
+    expect(dummy).toBe(0)
+
+    state.count++
+    await nextTick()
+    expect(dummy).toBe(1)
+  })
+
+  it('watching single source: getter', async () => {
+    const state = reactive({ count: 0 })
+    let dummy
+    watch(
+      () => state.count,
+      (count, prevCount) => {
+        dummy = [count, prevCount]
+        // assert types
+        count + 1
+        if (prevCount) {
+          prevCount + 1
+        }
+      }
+    )
+    state.count++
+    await nextTick()
+    expect(dummy).toMatchObject([1, 0])
+  })
+
+  it('watching single source: ref', async () => {
+    const count = ref(0)
+    let dummy
+    watch(count, (count, prevCount) => {
+      dummy = [count, prevCount]
+      // assert types
+      count + 1
+      if (prevCount) {
+        prevCount + 1
+      }
+    })
+    count.value++
+    await nextTick()
+    expect(dummy).toMatchObject([1, 0])
+  })
+
+  it('watching single source: array', async () => {
+    const array = reactive([] as number[])
+    const spy = vi.fn()
+    watch(array, spy)
+    array.push(1)
+    await nextTick()
+    expect(spy).toBeCalledTimes(1)
+    expect(spy).toBeCalledWith([1], expect.anything(), expect.anything())
+  })
+
+  it('should not fire if watched getter result did not change', async () => {
+    const spy = vi.fn()
+    const n = ref(0)
+    watch(() => n.value % 2, spy)
+
+    n.value++
+    await nextTick()
+    expect(spy).toBeCalledTimes(1)
+
+    n.value += 2
+    await nextTick()
+    // should not be called again because getter result did not change
+    expect(spy).toBeCalledTimes(1)
+  })
+
+  it('watching single source: computed ref', async () => {
+    const count = ref(0)
+    const plus = computed(() => count.value + 1)
+    let dummy
+    watch(plus, (count, prevCount) => {
+      dummy = [count, prevCount]
+      // assert types
+      count + 1
+      if (prevCount) {
+        prevCount + 1
+      }
+    })
+    count.value++
+    await nextTick()
+    expect(dummy).toMatchObject([2, 1])
+  })
+
+  it('watching primitive with deep: true', async () => {
+    const count = ref(0)
+    let dummy
+    watch(
+      count,
+      (c, prevCount) => {
+        dummy = [c, prevCount]
+      },
+      {
+        deep: true
+      }
+    )
+    count.value++
+    await nextTick()
+    expect(dummy).toMatchObject([1, 0])
+  })
+
+  it('directly watching reactive object (with automatic deep: true)', async () => {
+    const src = reactive({
+      count: 0
+    })
+    let dummy
+    watch(src, ({ count }) => {
+      dummy = count
+    })
+    src.count++
+    await nextTick()
+    expect(dummy).toBe(1)
+  })
+
+  it('watching multiple sources', async () => {
+    const state = reactive({ count: 1 })
+    const count = ref(1)
+    const plus = computed(() => count.value + 1)
+
+    let dummy
+    watch([() => state.count, count, plus], (vals, oldVals) => {
+      dummy = [vals, oldVals]
+      // assert types
+      vals.concat(1)
+      oldVals.concat(1)
+    })
+
+    state.count++
+    count.value++
+    await nextTick()
+    expect(dummy).toMatchObject([
+      [2, 2, 3],
+      [1, 1, 2]
+    ])
+  })
+
+  it('watching multiple sources: readonly array', async () => {
+    const state = reactive({ count: 1 })
+    const status = ref(false)
+
+    let dummy
+    watch([() => state.count, status] as const, (vals, oldVals) => {
+      dummy = [vals, oldVals]
+      const [count] = vals
+      const [, oldStatus] = oldVals
+      // assert types
+      count + 1
+      oldStatus === true
+    })
+
+    state.count++
+    status.value = true
+    await nextTick()
+    expect(dummy).toMatchObject([
+      [2, true],
+      [1, false]
+    ])
+  })
+
+  it('watching multiple sources: reactive object (with automatic deep: true)', async () => {
+    const src = reactive({ count: 0 })
+    let dummy
+    watch([src], ([state]) => {
+      dummy = state
+      // assert types
+      state.count === 1
+    })
+    src.count++
+    await nextTick()
+    expect(dummy).toMatchObject({ count: 1 })
+  })
+
+  it('warn invalid watch source', () => {
+    // @ts-expect-error
+    watch(1, () => {})
+    expect(`Invalid watch source`).toHaveBeenWarned()
+  })
+
+  it('warn invalid watch source: multiple sources', () => {
+    watch([1], () => {})
+    expect(`Invalid watch source`).toHaveBeenWarned()
+  })
+
+  it('stopping the watcher (effect)', async () => {
+    const state = reactive({ count: 0 })
+    let dummy
+    const stop = watchEffect(() => {
+      dummy = state.count
+    })
+    expect(dummy).toBe(0)
+
+    stop()
+    state.count++
+    await nextTick()
+    // should not update
+    expect(dummy).toBe(0)
+  })
+
+  it('stopping the watcher (with source)', async () => {
+    const state = reactive({ count: 0 })
+    let dummy
+    const stop = watch(
+      () => state.count,
+      count => {
+        dummy = count
+      }
+    )
+
+    state.count++
+    await nextTick()
+    expect(dummy).toBe(1)
+
+    stop()
+    state.count++
+    await nextTick()
+    // should not update
+    expect(dummy).toBe(1)
+  })
+
+  it('cleanup registration (effect)', async () => {
+    const state = reactive({ count: 0 })
+    const cleanup = vi.fn()
+    let dummy
+    const stop = watchEffect(onCleanup => {
+      onCleanup(cleanup)
+      dummy = state.count
+    })
+    expect(dummy).toBe(0)
+
+    state.count++
+    await nextTick()
+    expect(cleanup).toHaveBeenCalledTimes(1)
+    expect(dummy).toBe(1)
+
+    stop()
+    expect(cleanup).toHaveBeenCalledTimes(2)
+  })
+
+  it('cleanup registration (with source)', async () => {
+    const count = ref(0)
+    const cleanup = vi.fn()
+    let dummy
+    const stop = watch(count, (count, prevCount, onCleanup) => {
+      onCleanup(cleanup)
+      dummy = count
+    })
+
+    count.value++
+    await nextTick()
+    expect(cleanup).toHaveBeenCalledTimes(0)
+    expect(dummy).toBe(1)
+
+    count.value++
+    await nextTick()
+    expect(cleanup).toHaveBeenCalledTimes(1)
+    expect(dummy).toBe(2)
+
+    stop()
+    expect(cleanup).toHaveBeenCalledTimes(2)
+  })
+
+  it('flush timing: pre (default)', async () => {
+    const count = ref(0)
+    const count2 = ref(0)
+
+    let callCount = 0
+    let result1
+    let result2
+    const assertion = vi.fn((count, count2Value) => {
+      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
+
+      // in a pre-flush callback, all state should have been updated
+      const expectedState = callCount - 1
+      result2 = count === expectedState && count2Value === expectedState
+    })
+
+    const Comp = {
+      setup() {
+        watchEffect(() => {
+          assertion(count.value, count2.value)
+        })
+        return () => count.value
+      }
+    }
+    const root = document.createElement('div')
+    new Vue(Comp).$mount(root)
+    expect(assertion).toHaveBeenCalledTimes(1)
+    expect(result1).toBe(true)
+    expect(result2).toBe(true)
+
+    count.value++
+    count2.value++
+    await nextTick()
+    // two mutations should result in 1 callback execution
+    expect(assertion).toHaveBeenCalledTimes(2)
+    expect(result1).toBe(true)
+    expect(result2).toBe(true)
+  })
+
+  it('flush timing: post', async () => {
+    const count = ref(0)
+    let result
+    const assertion = vi.fn(count => {
+      result = root.innerHTML === `${count}`
+    })
+
+    const Comp = {
+      setup() {
+        watchEffect(
+          () => {
+            assertion(count.value)
+          },
+          { flush: 'post' }
+        )
+        return () => count.value
+      }
+    }
+    const root = document.createElement('div')
+    new Vue(Comp).$mount(root)
+    expect(assertion).toHaveBeenCalledTimes(1)
+    expect(result).toBe(true)
+
+    count.value++
+    await nextTick()
+    expect(assertion).toHaveBeenCalledTimes(2)
+    expect(result).toBe(true)
+  })
+
+  it('watchPostEffect', async () => {
+    const count = ref(0)
+    let result
+    const assertion = vi.fn(count => {
+      result = root.innerHTML === `${count}`
+    })
+
+    const Comp = {
+      setup() {
+        watchPostEffect(() => {
+          assertion(count.value)
+        })
+        return () => count.value
+      }
+    }
+    const root = document.createElement('div')
+    new Vue(Comp).$mount(root)
+    expect(assertion).toHaveBeenCalledTimes(1)
+    expect(result).toBe(true)
+
+    count.value++
+    await nextTick()
+    expect(assertion).toHaveBeenCalledTimes(2)
+    expect(result).toBe(true)
+  })
+
+  it('flush timing: sync', async () => {
+    const count = ref(0)
+    const count2 = ref(0)
+
+    let callCount = 0
+    let result1
+    let result2
+    const assertion = vi.fn(count => {
+      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
+
+      // 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.
+      const expectedState = callCount < 3 ? 0 : 1
+      result2 = count2.value === expectedState
+    })
+
+    const Comp = {
+      setup() {
+        watchEffect(
+          () => {
+            assertion(count.value)
+          },
+          {
+            flush: 'sync'
+          }
+        )
+        return () => count.value
+      }
+    }
+    const root = document.createElement('div')
+    new Vue(Comp).$mount(root)
+    expect(assertion).toHaveBeenCalledTimes(1)
+    expect(result1).toBe(true)
+    expect(result2).toBe(true)
+
+    count.value++
+    count2.value++
+    await nextTick()
+    expect(assertion).toHaveBeenCalledTimes(3)
+    expect(result1).toBe(true)
+    expect(result2).toBe(true)
+  })
+
+  it('watchSyncEffect', async () => {
+    const count = ref(0)
+    const count2 = ref(0)
+
+    let callCount = 0
+    let result1
+    let result2
+    const assertion = vi.fn(count => {
+      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
+
+      // 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.
+      const expectedState = callCount < 3 ? 0 : 1
+      result2 = count2.value === expectedState
+    })
+
+    const Comp = {
+      setup() {
+        watchSyncEffect(() => {
+          assertion(count.value)
+        })
+        return () => count.value
+      }
+    }
+    const root = document.createElement('div')
+    new Vue(Comp).$mount(root)
+    expect(assertion).toHaveBeenCalledTimes(1)
+    expect(result1).toBe(true)
+    expect(result2).toBe(true)
+
+    count.value++
+    count2.value++
+    await nextTick()
+    expect(assertion).toHaveBeenCalledTimes(3)
+    expect(result1).toBe(true)
+    expect(result2).toBe(true)
+  })
+
+  it('should not fire on component unmount w/ flush: post', async () => {
+    const toggle = ref(true)
+    const cb = vi.fn()
+    const Comp = {
+      setup() {
+        watch(toggle, cb, { flush: 'post' })
+      },
+      render() {}
+    }
+    const App = {
+      render() {
+        return toggle.value ? h(Comp) : null
+      }
+    }
+    new Vue(App).$mount(document.createElement('div'))
+    expect(cb).not.toHaveBeenCalled()
+    toggle.value = false
+    await nextTick()
+    expect(cb).not.toHaveBeenCalled()
+  })
+
+  it('should fire on component unmount w/ flush: pre', async () => {
+    const toggle = ref(true)
+    const cb = vi.fn()
+    const Comp = {
+      setup() {
+        watch(toggle, cb, { flush: 'pre' })
+      },
+      render() {}
+    }
+    const App = {
+      render() {
+        return toggle.value ? h(Comp) : null
+      }
+    }
+    new Vue(App).$mount(document.createElement('div'))
+    expect(cb).not.toHaveBeenCalled()
+    toggle.value = false
+    await nextTick()
+    expect(cb).toHaveBeenCalledTimes(1)
+  })
+
+  // #1763
+  it('flush: pre watcher watching props should fire before child update', async () => {
+    const a = ref(0)
+    const b = ref(0)
+    const c = ref(0)
+    const calls: string[] = []
+
+    const Comp = {
+      props: ['a', 'b'],
+      setup(props: any) {
+        watch(
+          () => props.a + props.b,
+          () => {
+            calls.push('watcher 1')
+            c.value++
+          },
+          { flush: 'pre' }
+        )
+
+        // #1777 chained pre-watcher
+        watch(
+          c,
+          () => {
+            calls.push('watcher 2')
+          },
+          { flush: 'pre' }
+        )
+        return () => {
+          c.value
+          calls.push('render')
+        }
+      }
+    }
+
+    const App = {
+      render() {
+        return h(Comp, { props: { a: a.value, b: b.value } })
+      }
+    }
+
+    new Vue(App).$mount(document.createElement('div'))
+    expect(calls).toEqual(['render'])
+
+    // both props are updated
+    // should trigger pre-flush watcher first and only once
+    // then trigger child render
+    a.value++
+    b.value++
+    await nextTick()
+    expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
+  })
+
+  // #5721
+  it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
+    const count = ref(0)
+    const calls: string[] = []
+    const App = {
+      render() {},
+      setup() {
+        watch(
+          count,
+          () => {
+            calls.push('watch ' + count.value)
+          },
+          { flush: 'pre' }
+        )
+        onMounted(() => {
+          calls.push('mounted')
+        })
+        // mutate multiple times
+        count.value++
+        count.value++
+        count.value++
+      }
+    }
+    new Vue(App).$mount(document.createElement('div'))
+    expect(calls).toMatchObject(['watch 3', 'mounted'])
+  })
+
+  // #1852
+  it('flush: post watcher should fire after template refs updated', async () => {
+    const toggle = ref(false)
+    let dom: TestElement | null = null
+
+    const App = {
+      setup() {
+        const domRef = ref<TestElement | null>(null)
+
+        watch(
+          toggle,
+          () => {
+            dom = domRef.value
+          },
+          { flush: 'post' }
+        )
+
+        return () => {
+          return toggle.value ? h('p', { ref: domRef }) : null
+        }
+      }
+    }
+
+    new Vue(App).$mount(document.createElement('div'))
+    expect(dom).toBe(null)
+
+    toggle.value = true
+    await nextTick()
+    expect(dom!.tag).toBe('p')
+  })
+
+  it('deep', async () => {
+    const state = reactive({
+      nested: {
+        count: ref(0)
+      },
+      array: [1, 2, 3],
+      map: new Map([
+        ['a', 1],
+        ['b', 2]
+      ]),
+      set: new Set([1, 2, 3])
+    })
+
+    let dummy
+    watch(
+      () => state,
+      state => {
+        dummy = [
+          state.nested.count,
+          state.array[0],
+          state.map.get('a'),
+          state.set.has(1)
+        ]
+      },
+      { deep: true }
+    )
+
+    state.nested.count++
+    await nextTick()
+    expect(dummy).toEqual([1, 1, 1, true])
+
+    // nested array mutation
+    state.array[0] = 2
+    await nextTick()
+    expect(dummy).toEqual([1, 2, 1, true])
+
+    // nested map mutation
+    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])
+  })
+
+  it('watching deep ref', async () => {
+    const count = ref(0)
+    const double = computed(() => count.value * 2)
+    const state = reactive([count, double])
+
+    let dummy
+    watch(
+      () => state,
+      state => {
+        dummy = [state[0].value, state[1].value]
+      },
+      { deep: true }
+    )
+
+    count.value++
+    await nextTick()
+    expect(dummy).toEqual([1, 2])
+  })
+
+  it('immediate', async () => {
+    const count = ref(0)
+    const cb = vi.fn()
+    watch(count, cb, { immediate: true })
+    expect(cb).toHaveBeenCalledTimes(1)
+    count.value++
+    await nextTick()
+    expect(cb).toHaveBeenCalledTimes(2)
+  })
+
+  it('immediate: triggers when initial value is null', async () => {
+    const state = ref(null)
+    const spy = vi.fn()
+    watch(() => state.value, spy, { immediate: true })
+    expect(spy).toHaveBeenCalled()
+  })
+
+  it('immediate: triggers when initial value is undefined', async () => {
+    const state = ref()
+    const spy = vi.fn()
+    watch(() => state.value, spy, { immediate: true })
+    expect(spy).toHaveBeenCalled()
+    state.value = 3
+    await nextTick()
+    expect(spy).toHaveBeenCalledTimes(2)
+    // testing if undefined can trigger the watcher
+    state.value = undefined
+    await nextTick()
+    expect(spy).toHaveBeenCalledTimes(3)
+    // it shouldn't trigger if the same value is set
+    state.value = undefined
+    await nextTick()
+    expect(spy).toHaveBeenCalledTimes(3)
+  })
+
+  it('warn immediate option when using effect', async () => {
+    const count = ref(0)
+    let dummy
+    watchEffect(
+      () => {
+        dummy = count.value
+      },
+      // @ts-expect-error
+      { immediate: false }
+    )
+    expect(dummy).toBe(0)
+    expect(`"immediate" option is only respected`).toHaveBeenWarned()
+
+    count.value++
+    await nextTick()
+    expect(dummy).toBe(1)
+  })
+
+  it('warn and not respect deep option when using effect', async () => {
+    const arr = ref([1, [2]])
+    const spy = vi.fn()
+    watchEffect(
+      () => {
+        spy()
+        return arr
+      },
+      // @ts-expect-error
+      { deep: true }
+    )
+    expect(spy).toHaveBeenCalledTimes(1)
+    ;(arr.value[1] as Array<number>)[0] = 3
+    await nextTick()
+    expect(spy).toHaveBeenCalledTimes(1)
+    expect(`"deep" option is only respected`).toHaveBeenWarned()
+  })
+
+  // TODO
+  // it('onTrack', async () => {
+  //   const events: DebuggerEvent[] = []
+  //   let dummy
+  //   const onTrack = vi.fn((e: DebuggerEvent) => {
+  //     events.push(e)
+  //   })
+  //   const obj = reactive({ foo: 1, bar: 2 })
+  //   watchEffect(
+  //     () => {
+  //       dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
+  //     },
+  //     { onTrack }
+  //   )
+  //   await nextTick()
+  //   expect(dummy).toEqual([1, true, ['foo', 'bar']])
+  //   expect(onTrack).toHaveBeenCalledTimes(3)
+  //   expect(events).toMatchObject([
+  //     {
+  //       target: obj,
+  //       type: TrackOpTypes.GET,
+  //       key: 'foo'
+  //     },
+  //     {
+  //       target: obj,
+  //       type: TrackOpTypes.HAS,
+  //       key: 'bar'
+  //     },
+  //     {
+  //       target: obj,
+  //       type: TrackOpTypes.ITERATE,
+  //       key: ITERATE_KEY
+  //     }
+  //   ])
+  // })
+
+  // it('onTrigger', async () => {
+  //   const events: DebuggerEvent[] = []
+  //   let dummy
+  //   const onTrigger = vi.fn((e: DebuggerEvent) => {
+  //     events.push(e)
+  //   })
+  //   const obj = reactive<{ foo?: number }>({ foo: 1 })
+  //   watchEffect(
+  //     () => {
+  //       dummy = obj.foo
+  //     },
+  //     { onTrigger }
+  //   )
+  //   await nextTick()
+  //   expect(dummy).toBe(1)
+
+  //   obj.foo!++
+  //   await nextTick()
+  //   expect(dummy).toBe(2)
+  //   expect(onTrigger).toHaveBeenCalledTimes(1)
+  //   expect(events[0]).toMatchObject({
+  //     type: TriggerOpTypes.SET,
+  //     key: 'foo',
+  //     oldValue: 1,
+  //     newValue: 2
+  //   })
+
+  //   delete obj.foo
+  //   await nextTick()
+  //   expect(dummy).toBeUndefined()
+  //   expect(onTrigger).toHaveBeenCalledTimes(2)
+  //   expect(events[1]).toMatchObject({
+  //     type: TriggerOpTypes.DELETE,
+  //     key: 'foo',
+  //     oldValue: 2
+  //   })
+  // })
+
+  it('should work sync', () => {
+    const v = ref(1)
+    let calls = 0
+
+    watch(
+      v,
+      () => {
+        ++calls
+      },
+      {
+        flush: 'sync'
+      }
+    )
+
+    expect(calls).toBe(0)
+    v.value++
+    expect(calls).toBe(1)
+  })
+
+  test('should force trigger on triggerRef when watching a shallow ref', async () => {
+    const v = shallowRef({ a: 1 })
+    let sideEffect = 0
+    watch(v, obj => {
+      sideEffect = obj.a
+    })
+
+    v.value = v.value
+    await nextTick()
+    // should not trigger
+    expect(sideEffect).toBe(0)
+
+    v.value.a++
+    await nextTick()
+    // should not trigger
+    expect(sideEffect).toBe(0)
+
+    triggerRef(v)
+    await nextTick()
+    // should trigger now
+    expect(sideEffect).toBe(2)
+  })
+
+  test('should force trigger on triggerRef when watching multiple sources: shallow ref array', async () => {
+    const v = shallowRef([] as any)
+    const spy = vi.fn()
+    watch([v], () => {
+      spy()
+    })
+
+    v.value.push(1)
+    triggerRef(v)
+
+    await nextTick()
+    // should trigger now
+    expect(spy).toHaveBeenCalledTimes(1)
+  })
+
+  // #2125
+  test('watchEffect should not recursively trigger itself', async () => {
+    const spy = vi.fn()
+    const price = ref(10)
+    const history = ref<number[]>([])
+    watchEffect(() => {
+      history.value.push(price.value)
+      spy()
+    })
+    await nextTick()
+    expect(spy).toHaveBeenCalledTimes(1)
+  })
+
+  // #2231
+  test('computed refs should not trigger watch if value has no change', async () => {
+    const spy = vi.fn()
+    const source = ref(0)
+    const price = computed(() => source.value === 0)
+    watch(price, spy)
+    source.value++
+    await nextTick()
+    source.value++
+    await nextTick()
+    expect(spy).toHaveBeenCalledTimes(1)
+  })
+
+  // TODO
+  // https://github.com/vuejs/core/issues/2381
+  test.skip('$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()
+
+    const Comp = {
+      render() {},
+      created(this: any) {
+        instance = this
+        this.$watch(source, function () {})
+      }
+    }
+
+    const root = document.createElement('div')
+    new Vue(Comp).$mount(root)
+
+    expect(instance).toBeDefined()
+    expect(source).toHaveBeenCalledWith(instance)
+  })
+
+  test('should not leak `this.proxy` to setup()', () => {
+    const source = vi.fn()
+
+    const Comp = {
+      render() {},
+      setup() {
+        watch(source, () => {})
+      }
+    }
+
+    const root = document.createElement('div')
+    new Vue(Comp).$mount(root)
+    // should not have any arguments
+    expect(source.mock.calls[0]).toMatchObject([])
+  })
+
+  // #2728
+  test('pre watcher callbacks should not track dependencies', async () => {
+    const a = ref(0)
+    const b = ref(0)
+    const updated = vi.fn()
+
+    const Child = {
+      props: ['a'],
+      updated,
+      watch: {
+        a() {
+          b.value
+        }
+      },
+      render() {
+        return h('div', this.a)
+      }
+    }
+
+    const Parent = {
+      render() {
+        return h(Child, { a: a.value })
+      }
+    }
+
+    const root = document.createElement('div')
+    new Vue(Parent).$mount(root)
+
+    a.value++
+    await nextTick()
+    expect(updated).toHaveBeenCalledTimes(1)
+
+    b.value++
+    await nextTick()
+    // should not track b as dependency of Child
+    expect(updated).toHaveBeenCalledTimes(1)
+  })
+
+  test('watching keypath', async () => {
+    const spy = vi.fn()
+    const Comp = {
+      render() {},
+      data() {
+        return {
+          a: {
+            b: 1
+          }
+        }
+      },
+      watch: {
+        'a.b': spy
+      },
+      created(this: any) {
+        this.$watch('a.b', spy)
+      },
+      mounted(this: any) {
+        this.a.b++
+      }
+    }
+
+    const root = document.createElement('div')
+    new Vue(Comp).$mount(root)
+
+    await nextTick()
+    expect(spy).toHaveBeenCalledTimes(2)
+  })
+
+  it('watching sources: ref<any[]>', async () => {
+    const foo = ref([1])
+    const spy = vi.fn()
+    watch(foo, () => {
+      spy()
+    })
+    foo.value = foo.value.slice()
+    await nextTick()
+    expect(spy).toBeCalledTimes(1)
+  })
+
+  it('watching multiple sources: computed', async () => {
+    let count = 0
+    const value = ref('1')
+    const plus = computed(() => !!value.value)
+    watch([plus], () => {
+      count++
+    })
+    value.value = '2'
+    await nextTick()
+    expect(plus.value).toBe(true)
+    expect(count).toBe(0)
+  })
+
+  // #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)
+  })
+})

+ 3 - 3
test/unit/features/composition-api/reactivity/computed.spec.ts → test/unit/features/v3/reactivity/computed.spec.ts

@@ -6,10 +6,10 @@ import {
   // toRaw,
   WritableComputedRef
   // DebuggerEvent
-} from 'vca/index'
-import { effect } from 'vca/reactivity/effect'
+} from 'v3'
+import { effect } from 'v3/reactivity/effect'
 import { nextTick } from 'core/util'
-// import { TrackOpTypes, TriggerOpTypes } from 'vca/reactivity/operations'
+// import { TrackOpTypes, TriggerOpTypes } from 'v3/reactivity/operations'
 
 describe('reactivity/computed', () => {
   it('should return updated value', () => {

+ 2 - 10
test/unit/features/composition-api/reactivity/reactive.spec.ts → test/unit/features/v3/reactivity/reactive.spec.ts

@@ -1,14 +1,6 @@
-import {
-  ref,
-  isRef,
-  reactive,
-  isReactive,
-  toRaw,
-  markRaw,
-  computed
-} from 'vca/index'
+import { ref, isRef, reactive, isReactive, toRaw, markRaw, computed } from 'v3'
 import { set } from 'core/observer'
-import { effect } from 'vca/reactivity/effect'
+import { effect } from 'v3/reactivity/effect'
 
 describe('reactivity/reactive', () => {
   test('Object', () => {

+ 2 - 2
test/unit/features/composition-api/reactivity/readonly.spec.ts → test/unit/features/v3/reactivity/readonly.spec.ts

@@ -7,8 +7,8 @@ import {
   markRaw,
   ref,
   isProxy
-} from 'vca/index'
-import { effect } from 'vca/reactivity/effect'
+} from 'v3'
+import { effect } from 'v3/reactivity/effect'
 import { set, del } from 'core/observer'
 
 /**

+ 2 - 2
test/unit/features/composition-api/reactivity/ref.spec.ts → test/unit/features/v3/reactivity/ref.spec.ts

@@ -11,8 +11,8 @@ import {
   isReactive,
   isShallow,
   reactive
-} from 'vca/index'
-import { effect } from 'vca/reactivity/effect'
+} from 'v3'
+import { effect } from 'v3/reactivity/effect'
 
 describe('reactivity/ref', () => {
   it('should hold a value', () => {

+ 1 - 1
test/unit/features/composition-api/reactivity/shallowReactive.spec.ts → test/unit/features/v3/reactivity/shallowReactive.spec.ts

@@ -4,7 +4,7 @@ import {
   reactive,
   shallowReactive,
   shallowReadonly
-} from 'vca/index'
+} from 'v3'
 
 describe('shallowReactive', () => {
   test('should not make non-reactive properties reactive', () => {

+ 1 - 1
test/unit/features/composition-api/reactivity/shallowReadonly.spec.ts → test/unit/features/v3/reactivity/shallowReadonly.spec.ts

@@ -1,4 +1,4 @@
-import { isReactive, shallowReadonly, readonly, isReadonly } from 'vca/index'
+import { isReactive, shallowReadonly, readonly, isReadonly } from 'v3'
 
 describe('reactivity/shallowReadonly', () => {
   test('should be readonly', () => {

+ 2 - 4
tsconfig.json

@@ -23,14 +23,12 @@
     "paths": {
       "compiler/*": ["src/compiler/*"],
       "core/*": ["src/core/*"],
-
       "server/*": ["src/server/*"],
       "sfc/*": ["src/sfc/*"],
       "shared/*": ["src/shared/*"],
-
       "web/*": ["src/platforms/web/*"],
-      "vca/*": ["src/composition-api/*"],
-
+      "v3": ["src/v3/index"],
+      "v3/*": ["src/v3/*"],
       "vue": ["src/platforms/web/entry-runtime-with-compiler"]
     }
   },

+ 1 - 1
vitest.config.ts

@@ -12,7 +12,7 @@ export default defineConfig({
       sfc: resolve('src/sfc'),
       shared: resolve('src/shared'),
       web: resolve('src/platforms/web'),
-      vca: resolve('src/composition-api'),
+      v3: resolve('src/v3'),
       vue: resolve('src/platforms/web/entry-runtime-with-compiler')
     }
   },