diff --git a/packages/observer/src/index.ts b/packages/observer/src/index.ts index 836731ed..005b82d3 100644 --- a/packages/observer/src/index.ts +++ b/packages/observer/src/index.ts @@ -24,7 +24,7 @@ import { DebuggerEvent } from './autorun' -export { Autorun, DebuggerEvent } +export { Autorun, AutorunOptions, DebuggerEvent } export { OperationTypes } from './operations' export { computed, ComputedGetter } from './computed' export { lock, unlock } from './lock' diff --git a/packages/runtime-core/src/componentUtils.ts b/packages/runtime-core/src/componentUtils.ts index c7dda26b..0ad7caf9 100644 --- a/packages/runtime-core/src/componentUtils.ts +++ b/packages/runtime-core/src/componentUtils.ts @@ -21,7 +21,7 @@ import { createRenderProxy } from './componentProxy' import { handleError, ErrorTypes, - callLifecycleHookWithHandle + callLifecycleHookWithHandler } from './errorHandling' import { warn } from './warning' import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks' @@ -56,7 +56,7 @@ export function createComponentInstance( instance.$slots = currentVNode.slots || EMPTY_OBJ if (created) { - callLifecycleHookWithHandle(created, $proxy, ErrorTypes.CREATED) + callLifecycleHookWithHandler(created, $proxy, ErrorTypes.CREATED) } currentVNode = currentContextVNode = null @@ -100,7 +100,7 @@ export function initializeComponentInstance(instance: ComponentInstance) { // beforeCreate hook is called right in the constructor const { beforeCreate, props } = instance.$options if (beforeCreate) { - callLifecycleHookWithHandle(beforeCreate, proxy, ErrorTypes.BEFORE_CREATE) + callLifecycleHookWithHandler(beforeCreate, proxy, ErrorTypes.BEFORE_CREATE) } initializeProps(instance, props, (currentVNode as VNode).data) } @@ -222,18 +222,24 @@ export function shouldUpdateComponent( if (nextProps === null) { return prevProps !== null } - let shouldUpdate = true const nextKeys = Object.keys(nextProps) - if (nextKeys.length === Object.keys(prevProps).length) { - shouldUpdate = false - for (let i = 0; i < nextKeys.length; i++) { - const key = nextKeys[i] - if (nextProps[key] !== prevProps[key]) { - shouldUpdate = true - } + if (nextKeys.length !== Object.keys(prevProps).length) { + return true + } + for (let i = 0; i < nextKeys.length; i++) { + const key = nextKeys[i] + if (nextProps[key] !== prevProps[key]) { + return true } } - return shouldUpdate + return false +} + +export function getReasonForComponentUpdate( + prevVNode: VNode, + nextVNode: VNode +): any { + // TODO: extract more detailed information on why the component is updating } export function createComponentClassFromOptions( diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 41a68450..afa26caa 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -1,4 +1,10 @@ -import { autorun, stop, Autorun, immutable } from '@vue/observer' +import { + autorun, + stop, + Autorun, + immutable, + AutorunOptions +} from '@vue/observer' import { queueJob, handleSchedulerError, nextTick } from '@vue/scheduler' import { VNodeFlags, ChildrenFlags } from './flags' import { EMPTY_OBJ, reservedPropRE, isString } from '@vue/shared' @@ -15,7 +21,8 @@ import { renderFunctionalRoot, createComponentInstance, teardownComponentInstance, - shouldUpdateComponent + shouldUpdateComponent, + getReasonForComponentUpdate } from './componentUtils' import { KeepAliveSymbol } from './optional/keepAlive' import { pushWarningContext, popWarningContext, warn } from './warning' @@ -23,7 +30,7 @@ import { resolveProps } from './componentProps' import { handleError, ErrorTypes, - callLifecycleHookWithHandle + callLifecycleHookWithHandler } from './errorHandling' export interface NodeOps { @@ -561,6 +568,14 @@ export function createRenderer(options: RendererOptions) { instance.$parentVNode = nextVNode as MountedVNode if (shouldUpdateComponent(prevVNode, nextVNode)) { + if (__DEV__ && instance.$options.renderTriggered) { + callLifecycleHookWithHandler( + instance.$options.renderTriggered, + instance.$proxy, + ErrorTypes.RENDER_TRIGGERED, + getReasonForComponentUpdate(prevVNode, nextVNode) + ) + } instance.$forceUpdate() } else if (instance.$vnode.flags & VNodeFlags.COMPONENT) { instance.$vnode.contextVNode = nextVNode @@ -1194,58 +1209,72 @@ export function createRenderer(options: RendererOptions) { } = instance if (beforeMount) { - callLifecycleHookWithHandle(beforeMount, $proxy, ErrorTypes.BEFORE_MOUNT) + callLifecycleHookWithHandler(beforeMount, $proxy, ErrorTypes.BEFORE_MOUNT) } const queueUpdate = (instance.$forceUpdate = () => { queueJob(instance._updateHandle, flushHooks) }) - instance._updateHandle = autorun( - () => { - if (instance._unmounted) { - return - } - if (instance._mounted) { - updateComponentInstance(instance, isSVG) - } else { - // this will be executed synchronously right here - instance.$vnode = renderInstanceRoot(instance) as MountedVNode + const autorunOptions: AutorunOptions = { + scheduler: queueUpdate + } - queuePostCommitHook(() => { - vnode.el = instance.$vnode.el - if (__COMPAT__) { - // expose __vue__ for devtools - ;(vnode.el as any).__vue__ = instance - } - if (vnode.ref) { - vnode.ref($proxy) - } - // retrieve mounted value after initial render so that we get - // to inject effects in hooks - const { mounted } = instance.$options - if (mounted) { - callLifecycleHookWithHandle(mounted, $proxy, ErrorTypes.MOUNTED) - } - }) - - mount( - instance.$vnode, - container, - vnode as MountedVNode, - isSVG, - endNode + if (__DEV__) { + if (renderTracked) { + autorunOptions.onTrack = event => { + callLifecycleHookWithHandler( + renderTracked, + $proxy, + ErrorTypes.RENDER_TRACKED, + event ) - - instance._mounted = true } - }, - { - scheduler: queueUpdate, - onTrack: renderTracked, - onTrigger: renderTriggered } - ) + if (renderTriggered) { + autorunOptions.onTrigger = event => { + callLifecycleHookWithHandler( + renderTriggered, + $proxy, + ErrorTypes.RENDER_TRIGGERED, + event + ) + } + } + } + + instance._updateHandle = autorun(() => { + if (instance._unmounted) { + return + } + if (instance._mounted) { + updateComponentInstance(instance, isSVG) + } else { + // this will be executed synchronously right here + instance.$vnode = renderInstanceRoot(instance) as MountedVNode + + queuePostCommitHook(() => { + vnode.el = instance.$vnode.el + if (__COMPAT__) { + // expose __vue__ for devtools + ;(vnode.el as any).__vue__ = instance + } + if (vnode.ref) { + vnode.ref($proxy) + } + // retrieve mounted value after initial render so that we get + // to inject effects in hooks + const { mounted } = instance.$options + if (mounted) { + callLifecycleHookWithHandler(mounted, $proxy, ErrorTypes.MOUNTED) + } + }) + + mount(instance.$vnode, container, vnode as MountedVNode, isSVG, endNode) + + instance._mounted = true + } + }, autorunOptions) if (__DEV__) { popWarningContext() @@ -1267,7 +1296,7 @@ export function createRenderer(options: RendererOptions) { $options: { beforeUpdate } } = instance if (beforeUpdate) { - callLifecycleHookWithHandle( + callLifecycleHookWithHandler( beforeUpdate, $proxy, ErrorTypes.BEFORE_UPDATE, @@ -1297,7 +1326,7 @@ export function createRenderer(options: RendererOptions) { } const { updated } = instance.$options if (updated) { - callLifecycleHookWithHandle( + callLifecycleHookWithHandler( updated, $proxy, ErrorTypes.UPDATED, @@ -1334,7 +1363,7 @@ export function createRenderer(options: RendererOptions) { $options: { beforeUnmount, unmounted } } = instance if (beforeUnmount) { - callLifecycleHookWithHandle( + callLifecycleHookWithHandler( beforeUnmount, $proxy, ErrorTypes.BEFORE_UNMOUNT @@ -1347,7 +1376,7 @@ export function createRenderer(options: RendererOptions) { teardownComponentInstance(instance) instance._unmounted = true if (unmounted) { - callLifecycleHookWithHandle(unmounted, $proxy, ErrorTypes.UNMOUNTED) + callLifecycleHookWithHandler(unmounted, $proxy, ErrorTypes.UNMOUNTED) } } @@ -1391,7 +1420,7 @@ export function createRenderer(options: RendererOptions) { callActivatedHook($children[i], false) } if (activated) { - callLifecycleHookWithHandle(activated, $proxy, ErrorTypes.ACTIVATED) + callLifecycleHookWithHandler(activated, $proxy, ErrorTypes.ACTIVATED) } } } @@ -1416,7 +1445,11 @@ export function createRenderer(options: RendererOptions) { callDeactivateHook($children[i], false) } if (deactivated) { - callLifecycleHookWithHandle(deactivated, $proxy, ErrorTypes.DEACTIVATED) + callLifecycleHookWithHandler( + deactivated, + $proxy, + ErrorTypes.DEACTIVATED + ) } } } diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index 1205d5fa..301fa66b 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -16,6 +16,8 @@ export const enum ErrorTypes { DEACTIVATED, ERROR_CAPTURED, RENDER, + RENDER_TRACKED, + RENDER_TRIGGERED, WATCH_CALLBACK, NATIVE_EVENT_HANDLER, COMPONENT_EVENT_HANDLER, @@ -35,6 +37,8 @@ const ErrorTypeStrings: Record = { [ErrorTypes.DEACTIVATED]: 'in deactivated lifecycle hook', [ErrorTypes.ERROR_CAPTURED]: 'in errorCaptured lifecycle hook', [ErrorTypes.RENDER]: 'in render function', + [ErrorTypes.RENDER_TRACKED]: 'in renderTracked debug hook', + [ErrorTypes.RENDER_TRIGGERED]: 'in renderTriggered debug hook', [ErrorTypes.WATCH_CALLBACK]: 'in watcher callback', [ErrorTypes.NATIVE_EVENT_HANDLER]: 'in native event handler', [ErrorTypes.COMPONENT_EVENT_HANDLER]: 'in component event handler', @@ -42,7 +46,7 @@ const ErrorTypeStrings: Record = { 'when flushing updates. This may be a Vue internals bug.' } -export function callLifecycleHookWithHandle( +export function callLifecycleHookWithHandler( hook: Function, instanceProxy: ComponentInstance, type: ErrorTypes,