소스 검색

wip: renderTriggered/renderTracked

Evan You 3 년 전
부모
커밋
dae380dc24
6개의 변경된 파일150개의 추가작업 그리고 22개의 파일을 삭제
  1. 17 10
      src/core/instance/lifecycle.ts
  2. 20 9
      src/core/observer/watcher.ts
  3. 10 2
      src/v3/apiLifecycle.ts
  4. 77 1
      test/unit/features/v3/apiLifecycle.spec.ts
  5. 23 0
      types/options.d.ts
  6. 3 0
      typescript/options.d.ts

+ 17 - 10
src/core/instance/lifecycle.ts

@@ -1,5 +1,5 @@
 import config from '../config'
-import Watcher from '../observer/watcher'
+import Watcher, { WatcherOptions } from '../observer/watcher'
 import { mark, measure } from '../util/perf'
 import VNode, { createEmptyVNode } from '../vdom/vnode'
 import { updateComponentListeners } from './events'
@@ -192,6 +192,19 @@ export function mountComponent(
     }
   }
 
+  const watcherOptions: WatcherOptions = {
+    before() {
+      if (vm._isMounted && !vm._isDestroyed) {
+        callHook(vm, 'beforeUpdate')
+      }
+    }
+  }
+
+  if (__DEV__) {
+    watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
+    watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
+  }
+
   // we set this to vm._watcher inside the watcher's constructor
   // since the watcher's initial patch may call $forceUpdate (e.g. inside child
   // component's mounted hook), which relies on vm._watcher being already defined
@@ -199,13 +212,7 @@ export function mountComponent(
     vm,
     updateComponent,
     noop,
-    {
-      before() {
-        if (vm._isMounted && !vm._isDestroyed) {
-          callHook(vm, 'beforeUpdate')
-        }
-      }
-    },
+    watcherOptions,
     true /* isRenderWatcher */
   )
   hydrating = false
@@ -365,7 +372,7 @@ export function deactivateChildComponent(vm: Component, direct?: boolean) {
   }
 }
 
-export function callHook(vm: Component, hook: string) {
+export function callHook(vm: Component, hook: string, args?: any[]) {
   // #7573 disable dep collection when invoking lifecycle hooks
   pushTarget()
   const prev = currentInstance
@@ -374,7 +381,7 @@ export function callHook(vm: Component, hook: string) {
   const info = `${hook} hook`
   if (handlers) {
     for (let i = 0, j = handlers.length; i < j; i++) {
-      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
+      invokeWithErrorHandling(handlers[i], vm, args || null, vm, info)
     }
   }
   if (vm._hasHookEvent) {

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

@@ -12,7 +12,13 @@ import {
 
 import { traverse } from './traverse'
 import { queueWatcher } from './scheduler'
-import Dep, { pushTarget, popTarget, DepTarget, DebuggerEvent } from './dep'
+import Dep, {
+  pushTarget,
+  popTarget,
+  DepTarget,
+  DebuggerEvent,
+  DebuggerOptions
+} from './dep'
 
 import type { SimpleSet } from '../util/index'
 import type { Component } from 'typescript/component'
@@ -23,6 +29,14 @@ import {
 
 let uid = 0
 
+export interface WatcherOptions extends DebuggerOptions {
+  deep?: boolean
+  user?: boolean
+  lazy?: boolean
+  sync?: boolean
+  before?: Function
+}
+
 /**
  * A watcher parses an expression, collects dependencies,
  * and fires callback when the expression value changes.
@@ -57,14 +71,7 @@ export default class Watcher implements DepTarget {
     vm: Component | null,
     expOrFn: string | (() => any),
     cb: Function,
-    options?: {
-      deep?: boolean
-      user?: boolean
-      lazy?: boolean
-      sync?: boolean
-      before?: Function
-      scheduler?: Function
-    } | null,
+    options?: WatcherOptions | null,
     isRenderWatcher?: boolean
   ) {
     recordEffectScope(this, activeEffectScope || (vm ? vm._scope : undefined))
@@ -80,6 +87,10 @@ export default class Watcher implements DepTarget {
       this.lazy = !!options.lazy
       this.sync = !!options.sync
       this.before = options.before
+      if (__DEV__) {
+        this.onTrack = options.onTrack
+        this.onTrigger = options.onTrigger
+      }
     } else {
       this.deep = this.user = this.lazy = this.sync = false
     }

+ 10 - 2
src/v3/apiLifecycle.ts

@@ -1,9 +1,12 @@
+import { DebuggerEvent } from '.'
 import { Component } from '../../typescript/component'
 import { mergeLifecycleHook, warn } from '../core/util'
 import { currentInstance } from './currentInstance'
 
-function createLifeCycle(hookName: string) {
-  return (fn: () => void, target: Component | null = currentInstance) => {
+function createLifeCycle<T extends (...args: any[]) => any = () => void>(
+  hookName: string
+) {
+  return (fn: T, target: Component | null = currentInstance) => {
     if (!target) {
       __DEV__ &&
         warn(
@@ -43,3 +46,8 @@ export const onErrorCaptured = createLifeCycle('errorCaptured')
 export const onActivated = createLifeCycle('activated')
 export const onDeactivated = createLifeCycle('deactivated')
 export const onServerPrefetch = createLifeCycle('serverPrefetch')
+
+export const onRenderTracked =
+  createLifeCycle<(e: DebuggerEvent) => any>('renderTracked')
+export const onRenderTriggered =
+  createLifeCycle<(e: DebuggerEvent) => any>('renderTriggered')

+ 77 - 1
test/unit/features/v3/apiLifecycle.spec.ts

@@ -4,10 +4,16 @@ import {
   onBeforeMount,
   onMounted,
   ref,
+  reactive,
   onBeforeUpdate,
   onUpdated,
   onBeforeUnmount,
-  onUnmounted
+  onUnmounted,
+  onRenderTracked,
+  onRenderTriggered,
+  DebuggerEvent,
+  TrackOpTypes,
+  TriggerOpTypes
 } from 'v3'
 import { nextTick } from 'core/util'
 
@@ -281,4 +287,74 @@ describe('api: lifecycle hooks', () => {
       'root onUnmounted'
     ])
   })
+
+  it('onRenderTracked', () => {
+    const events: DebuggerEvent[] = []
+    const onTrack = vi.fn((e: DebuggerEvent) => {
+      events.push(e)
+    })
+    const obj = reactive({ foo: 1, bar: 2 })
+
+    const Comp = {
+      setup() {
+        onRenderTracked(onTrack)
+        return () => h('div', [obj.foo + obj.bar])
+      }
+    }
+
+    new Vue(Comp).$mount()
+    expect(onTrack).toHaveBeenCalledTimes(2)
+    expect(events).toMatchObject([
+      {
+        target: obj,
+        type: TrackOpTypes.GET,
+        key: 'foo'
+      },
+      {
+        target: obj,
+        type: TrackOpTypes.GET,
+        key: 'bar'
+      }
+    ])
+  })
+
+  it('onRenderTriggered', async () => {
+    const events: DebuggerEvent[] = []
+    const onTrigger = vi.fn((e: DebuggerEvent) => {
+      events.push(e)
+    })
+    const obj = reactive<{
+      foo: number
+      bar: number
+    }>({ foo: 1, bar: 2 })
+
+    const Comp = {
+      setup() {
+        onRenderTriggered(onTrigger)
+        return () => h('div', [obj.foo + obj.bar])
+      }
+    }
+
+    new Vue(Comp).$mount()
+
+    obj.foo++
+    await nextTick()
+    expect(onTrigger).toHaveBeenCalledTimes(1)
+    expect(events[0]).toMatchObject({
+      type: TriggerOpTypes.SET,
+      key: 'foo',
+      oldValue: 1,
+      newValue: 2
+    })
+
+    obj.bar++
+    await nextTick()
+    expect(onTrigger).toHaveBeenCalledTimes(2)
+    expect(events[1]).toMatchObject({
+      type: TriggerOpTypes.SET,
+      key: 'bar',
+      oldValue: 2,
+      newValue: 3
+    })
+  })
 })

+ 23 - 0
types/options.d.ts

@@ -189,6 +189,8 @@ export interface ComponentOptions<
   deactivated?(): void
   errorCaptured?(err: Error, vm: Vue, info: string): boolean | void
   serverPrefetch?(this: V): Promise<void>
+  renderTracked?(e: DebuggerEvent): void
+  renderTriggerd?(e: DebuggerEvent): void
 
   directives?: { [key: string]: DirectiveFunction | DirectiveOptions }
   components?: {
@@ -314,3 +316,24 @@ export type InjectOptions =
       [key: string]: InjectKey | { from?: InjectKey; default?: any }
     }
   | string[]
+
+export type DebuggerEvent = {
+  target: object
+  type: TrackOpTypes | TriggerOpTypes
+  key?: any
+  newValue?: any
+  oldValue?: any
+  oldTarget?: Map<any, any> | Set<any>
+}
+
+export const enum TrackOpTypes {
+  GET = 'get',
+  TOUCH = 'touch'
+}
+
+export const enum TriggerOpTypes {
+  SET = 'set',
+  ADD = 'add',
+  DELETE = 'delete',
+  ARRAY_MUTATION = 'array mutation'
+}

+ 3 - 0
typescript/options.d.ts

@@ -1,4 +1,5 @@
 import VNode from '../src/core/vdom/vnode'
+import { DebuggerEvent } from '../src/v3'
 import { Component } from './component'
 
 declare type InternalComponentOptions = {
@@ -61,6 +62,8 @@ declare type ComponentOptions = {
   destroyed?: Function
   errorCaptured?: () => boolean | void
   serverPrefetch?: Function
+  renderTracked?(e: DebuggerEvent): void
+  renderTriggerd?(e: DebuggerEvent): void
 
   // assets
   directives?: { [key: string]: object }