feat(reactivity): new effectScope API (#2195)

This commit is contained in:
Anthony Fu
2021-07-07 21:07:19 +08:00
committed by Evan You
parent 87f69fd0bb
commit f5617fc3bb
16 changed files with 400 additions and 89 deletions

View File

@@ -848,15 +848,16 @@ describe('api: watch', () => {
render(h(Comp), nodeOps.createElement('div'))
expect(instance!).toBeDefined()
expect(instance!.effects).toBeInstanceOf(Array)
expect(instance!.effects!.length).toBe(1)
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!.effects![0].active).toBe(false)
expect(instance!.scope.effects![0].active).toBe(false)
})
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {

View File

@@ -1,20 +0,0 @@
import {
computed as _computed,
ComputedRef,
WritableComputedOptions,
WritableComputedRef,
ComputedGetter
} from '@vue/reactivity'
import { recordInstanceBoundEffect } from './component'
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
const c = _computed(getterOrOptions as any)
recordInstanceBoundEffect(c.effect)
return c
}

View File

@@ -3,7 +3,8 @@ import {
currentInstance,
isInSSRComponentSetup,
LifecycleHooks,
setCurrentInstance
setCurrentInstance,
unsetCurrentInstance
} from './component'
import { ComponentPublicInstance } from './componentPublicInstance'
import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
@@ -38,7 +39,7 @@ export function injectHook(
// can only be false when the user does something really funky.
setCurrentInstance(target)
const res = callWithAsyncErrorHandling(hook, target, type, args)
setCurrentInstance(null)
unsetCurrentInstance()
resetTracking()
return res
})

View File

@@ -3,7 +3,8 @@ import {
getCurrentInstance,
setCurrentInstance,
SetupContext,
createSetupContext
createSetupContext,
unsetCurrentInstance
} from './component'
import { EmitFn, EmitsOptions } from './componentEmits'
import {
@@ -248,9 +249,15 @@ export function mergeDefaults(
* @internal
*/
export function withAsyncContext(getAwaitable: () => any) {
const ctx = getCurrentInstance()
const ctx = getCurrentInstance()!
if (__DEV__ && !ctx) {
warn(
`withAsyncContext called without active current instance. ` +
`This is likely a bug.`
)
}
let awaitable = getAwaitable()
setCurrentInstance(null)
unsetCurrentInstance()
if (isPromise(awaitable)) {
awaitable = awaitable.catch(e => {
setCurrentInstance(ctx)

View File

@@ -25,8 +25,7 @@ import {
import {
currentInstance,
ComponentInternalInstance,
isInSSRComponentSetup,
recordInstanceBoundEffect
isInSSRComponentSetup
} from './component'
import {
ErrorCodes,
@@ -326,15 +325,14 @@ function doWatch(
}
}
const effect = new ReactiveEffect(getter, scheduler)
const scope = instance && instance.scope
const effect = new ReactiveEffect(getter, scheduler, scope)
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
recordInstanceBoundEffect(effect, instance)
// initial run
if (cb) {
if (immediate) {
@@ -353,8 +351,8 @@ function doWatch(
return () => {
effect.stop()
if (instance) {
remove(instance.effects!, effect)
if (scope) {
remove(scope.effects!, effect)
}
}
}

View File

@@ -563,7 +563,7 @@ function installCompatMount(
}
delete app._container.__vue_app__
} else {
const { bum, effects, um } = instance
const { bum, scope, um } = instance
// beforeDestroy hooks
if (bum) {
invokeArrayFns(bum)
@@ -572,10 +572,8 @@ function installCompatMount(
instance.emit('hook:beforeDestroy')
}
// stop effects
if (effects) {
for (let i = 0; i < effects.length; i++) {
effects[i].stop()
}
if (scope) {
scope.stop()
}
// unmounted hook
if (um) {

View File

@@ -1,10 +1,10 @@
import { VNode, VNodeChild, isVNode } from './vnode'
import {
ReactiveEffect,
pauseTracking,
resetTracking,
shallowReadonly,
proxyRefs,
EffectScope,
markRaw
} from '@vue/reactivity'
import {
@@ -217,11 +217,6 @@ export interface ComponentInternalInstance {
* Root vnode of this component's own vdom tree
*/
subTree: VNode
/**
* Main update effect
* @internal
*/
effect: ReactiveEffect
/**
* Bound effect runner to be passed to schedulers
*/
@@ -246,7 +241,7 @@ export interface ComponentInternalInstance {
* so that they can be automatically stopped on component unmount
* @internal
*/
effects: ReactiveEffect[] | null
scope: EffectScope
/**
* cache for proxy access type to avoid hasOwnProperty calls
* @internal
@@ -451,14 +446,13 @@ export function createComponentInstance(
root: null!, // to be immediately set
next: null,
subTree: null!, // will be set synchronously right after creation
effect: null!, // will be set synchronously right after creation
update: null!, // will be set synchronously right after creation
scope: new EffectScope(),
render: null,
proxy: null,
exposed: null,
exposeProxy: null,
withProxy: null,
effects: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
accessCache: null!,
renderCache: [],
@@ -533,10 +527,14 @@ export let currentInstance: ComponentInternalInstance | null = null
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance || currentRenderingInstance
export const setCurrentInstance = (
instance: ComponentInternalInstance | null
) => {
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
currentInstance = instance
instance.scope.on()
}
export const unsetCurrentInstance = () => {
currentInstance && currentInstance.scope.off()
currentInstance = null
}
const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component')
@@ -618,7 +616,7 @@ function setupStatefulComponent(
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
currentInstance = instance
setCurrentInstance(instance)
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
@@ -627,13 +625,10 @@ function setupStatefulComponent(
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
currentInstance = null
unsetCurrentInstance()
if (isPromise(setupResult)) {
const unsetInstance = () => {
currentInstance = null
}
setupResult.then(unsetInstance, unsetInstance)
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
@@ -801,11 +796,11 @@ export function finishComponentSetup(
// support for 2.x options
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
currentInstance = instance
setCurrentInstance(instance)
pauseTracking()
applyOptions(instance)
resetTracking()
currentInstance = null
unsetCurrentInstance()
}
// warn missing template/render
@@ -900,17 +895,6 @@ export function getExposeProxy(instance: ComponentInternalInstance) {
}
}
// record effects created during a component's setup() so that they can be
// stopped when the component unmounts
export function recordInstanceBoundEffect(
effect: ReactiveEffect,
instance = currentInstance
) {
if (instance) {
;(instance.effects || (instance.effects = [])).push(effect)
}
}
const classifyRE = /(?:^|[-_])(\w)/g
const classify = (str: string): string =>
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')

View File

@@ -17,7 +17,7 @@ import {
NOOP,
isPromise
} from '@vue/shared'
import { computed } from './apiComputed'
import { computed } from '@vue/reactivity'
import {
watch,
WatchOptions,

View File

@@ -29,7 +29,8 @@ import {
ComponentInternalInstance,
ComponentOptions,
ConcreteComponent,
setCurrentInstance
setCurrentInstance,
unsetCurrentInstance
} from './component'
import { isEmitListener } from './componentEmits'
import { InternalObjectKey } from './vnode'
@@ -411,7 +412,7 @@ function resolvePropValue(
: null,
props
)
setCurrentInstance(null)
unsetCurrentInstance()
}
} else {
value = defaultValue

View File

@@ -3,6 +3,7 @@
export const version = __VERSION__
export {
// core
computed,
reactive,
ref,
readonly,
@@ -22,9 +23,17 @@ export {
shallowReactive,
shallowReadonly,
markRaw,
toRaw
toRaw,
// effect
effect,
stop,
ReactiveEffect,
// effect scope
effectScope,
EffectScope,
getCurrentScope,
onScopeDispose
} from '@vue/reactivity'
export { computed } from './apiComputed'
export { watch, watchEffect } from './apiWatch'
export {
onBeforeMount,
@@ -137,7 +146,6 @@ declare module '@vue/reactivity' {
}
export {
ReactiveEffect,
ReactiveEffectOptions,
DebuggerEvent,
TrackOpTypes,

View File

@@ -1622,11 +1622,12 @@ function baseCreateRenderer(
}
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope, // track it in component's effect scope
true /* allowRecurse */
))
)
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
update.id = instance.uid
@@ -2285,12 +2286,13 @@ function baseCreateRenderer(
unregisterHMR(instance)
}
const { bum, effect, effects, update, subTree, um } = instance
const { bum, scope, update, subTree, um } = instance
// beforeUnmount hook
if (bum) {
invokeArrayFns(bum)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
@@ -2298,15 +2300,13 @@ function baseCreateRenderer(
instance.emit('hook:beforeDestroy')
}
if (effects) {
for (let i = 0; i < effects.length; i++) {
effects[i].stop()
}
if (scope) {
scope.stop()
}
// update may be null if a component is unmounted before its async
// setup has resolved.
if (effect) {
effect.stop()
if (update) {
// so that scheduler will no longer invoke it
update.active = false
unmount(subTree, instance, parentSuspense, doRemove)