mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
* Store list of contexts on the fiber Currently, context can only be read by a special type of component, ContextConsumer. We want to add support to all fibers, including classes and functional components. Each fiber may read from one or more contexts. To enable quick, mono- morphic access of this list, we'll store them on a fiber property. * Context.unstable_read unstable_read can be called anywhere within the render phase. That includes the render method, getDerivedStateFromProps, constructors, functional components, and context consumer render props. If it's called outside the render phase, an error is thrown. * Remove vestigial context cursor Wasn't being used. * Split fiber.expirationTime into two separate fields Currently, the `expirationTime` field represents the pending work of both the fiber itself — including new props, state, and context — and of any updates in that fiber's subtree. This commit adds a second field called `childExpirationTime`. Now `expirationTime` only represents the pending work of the fiber itself. The subtree's pending work is represented by `childExpirationTime`. The biggest advantage is it requires fewer checks to bailout on already finished work. For most types of work, if the `expirationTime` does not match the render expiration time, we can bailout immediately without any further checks. This won't work for fibers that have `shouldComponentUpdate` semantics (class components), for which we still need to check for props and state changes explicitly. * Performance nits Optimize `readContext` for most common case
624 lines
20 KiB
JavaScript
624 lines
20 KiB
JavaScript
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
// UpdateQueue is a linked list of prioritized updates.
|
|
//
|
|
// Like fibers, update queues come in pairs: a current queue, which represents
|
|
// the visible state of the screen, and a work-in-progress queue, which is
|
|
// can be mutated and processed asynchronously before it is committed — a form
|
|
// of double buffering. If a work-in-progress render is discarded before
|
|
// finishing, we create a new work-in-progress by cloning the current queue.
|
|
//
|
|
// Both queues share a persistent, singly-linked list structure. To schedule an
|
|
// update, we append it to the end of both queues. Each queue maintains a
|
|
// pointer to first update in the persistent list that hasn't been processed.
|
|
// The work-in-progress pointer always has a position equal to or greater than
|
|
// the current queue, since we always work on that one. The current queue's
|
|
// pointer is only updated during the commit phase, when we swap in the
|
|
// work-in-progress.
|
|
//
|
|
// For example:
|
|
//
|
|
// Current pointer: A - B - C - D - E - F
|
|
// Work-in-progress pointer: D - E - F
|
|
// ^
|
|
// The work-in-progress queue has
|
|
// processed more updates than current.
|
|
//
|
|
// The reason we append to both queues is because otherwise we might drop
|
|
// updates without ever processing them. For example, if we only add updates to
|
|
// the work-in-progress queue, some updates could be lost whenever a work-in
|
|
// -progress render restarts by cloning from current. Similarly, if we only add
|
|
// updates to the current queue, the updates will be lost whenever an already
|
|
// in-progress queue commits and swaps with the current queue. However, by
|
|
// adding to both queues, we guarantee that the update will be part of the next
|
|
// work-in-progress. (And because the work-in-progress queue becomes the
|
|
// current queue once it commits, there's no danger of applying the same
|
|
// update twice.)
|
|
//
|
|
// Prioritization
|
|
// --------------
|
|
//
|
|
// Updates are not sorted by priority, but by insertion; new updates are always
|
|
// appended to the end of the list.
|
|
//
|
|
// The priority is still important, though. When processing the update queue
|
|
// during the render phase, only the updates with sufficient priority are
|
|
// included in the result. If we skip an update because it has insufficient
|
|
// priority, it remains in the queue to be processed later, during a lower
|
|
// priority render. Crucially, all updates subsequent to a skipped update also
|
|
// remain in the queue *regardless of their priority*. That means high priority
|
|
// updates are sometimes processed twice, at two separate priorities. We also
|
|
// keep track of a base state, that represents the state before the first
|
|
// update in the queue is applied.
|
|
//
|
|
// For example:
|
|
//
|
|
// Given a base state of '', and the following queue of updates
|
|
//
|
|
// A1 - B2 - C1 - D2
|
|
//
|
|
// where the number indicates the priority, and the update is applied to the
|
|
// previous state by appending a letter, React will process these updates as
|
|
// two separate renders, one per distinct priority level:
|
|
//
|
|
// First render, at priority 1:
|
|
// Base state: ''
|
|
// Updates: [A1, C1]
|
|
// Result state: 'AC'
|
|
//
|
|
// Second render, at priority 2:
|
|
// Base state: 'A' <- The base state does not include C1,
|
|
// because B2 was skipped.
|
|
// Updates: [B2, C1, D2] <- C1 was rebased on top of B2
|
|
// Result state: 'ABCD'
|
|
//
|
|
// Because we process updates in insertion order, and rebase high priority
|
|
// updates when preceding updates are skipped, the final result is deterministic
|
|
// regardless of priority. Intermediate state may vary according to system
|
|
// resources, but the final state is always the same.
|
|
|
|
import type {Fiber} from './ReactFiber';
|
|
import type {ExpirationTime} from './ReactFiberExpirationTime';
|
|
|
|
import {NoWork} from './ReactFiberExpirationTime';
|
|
import {
|
|
Callback,
|
|
ShouldCapture,
|
|
DidCapture,
|
|
} from 'shared/ReactTypeOfSideEffect';
|
|
import {ClassComponent} from 'shared/ReactTypeOfWork';
|
|
|
|
import {
|
|
debugRenderPhaseSideEffects,
|
|
debugRenderPhaseSideEffectsForStrictMode,
|
|
} from 'shared/ReactFeatureFlags';
|
|
|
|
import {StrictMode} from './ReactTypeOfMode';
|
|
|
|
import invariant from 'shared/invariant';
|
|
import warningWithoutStack from 'shared/warningWithoutStack';
|
|
|
|
export type Update<State> = {
|
|
expirationTime: ExpirationTime,
|
|
|
|
tag: 0 | 1 | 2 | 3,
|
|
payload: any,
|
|
callback: (() => mixed) | null,
|
|
|
|
next: Update<State> | null,
|
|
nextEffect: Update<State> | null,
|
|
};
|
|
|
|
export type UpdateQueue<State> = {
|
|
baseState: State,
|
|
|
|
firstUpdate: Update<State> | null,
|
|
lastUpdate: Update<State> | null,
|
|
|
|
firstCapturedUpdate: Update<State> | null,
|
|
lastCapturedUpdate: Update<State> | null,
|
|
|
|
firstEffect: Update<State> | null,
|
|
lastEffect: Update<State> | null,
|
|
|
|
firstCapturedEffect: Update<State> | null,
|
|
lastCapturedEffect: Update<State> | null,
|
|
};
|
|
|
|
export const UpdateState = 0;
|
|
export const ReplaceState = 1;
|
|
export const ForceUpdate = 2;
|
|
export const CaptureUpdate = 3;
|
|
|
|
// Global state that is reset at the beginning of calling `processUpdateQueue`.
|
|
// It should only be read right after calling `processUpdateQueue`, via
|
|
// `checkHasForceUpdateAfterProcessing`.
|
|
let hasForceUpdate = false;
|
|
|
|
let didWarnUpdateInsideUpdate;
|
|
let currentlyProcessingQueue;
|
|
export let resetCurrentlyProcessingQueue;
|
|
if (__DEV__) {
|
|
didWarnUpdateInsideUpdate = false;
|
|
currentlyProcessingQueue = null;
|
|
resetCurrentlyProcessingQueue = () => {
|
|
currentlyProcessingQueue = null;
|
|
};
|
|
}
|
|
|
|
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
|
|
const queue: UpdateQueue<State> = {
|
|
baseState,
|
|
firstUpdate: null,
|
|
lastUpdate: null,
|
|
firstCapturedUpdate: null,
|
|
lastCapturedUpdate: null,
|
|
firstEffect: null,
|
|
lastEffect: null,
|
|
firstCapturedEffect: null,
|
|
lastCapturedEffect: null,
|
|
};
|
|
return queue;
|
|
}
|
|
|
|
function cloneUpdateQueue<State>(
|
|
currentQueue: UpdateQueue<State>,
|
|
): UpdateQueue<State> {
|
|
const queue: UpdateQueue<State> = {
|
|
baseState: currentQueue.baseState,
|
|
firstUpdate: currentQueue.firstUpdate,
|
|
lastUpdate: currentQueue.lastUpdate,
|
|
|
|
// TODO: With resuming, if we bail out and resuse the child tree, we should
|
|
// keep these effects.
|
|
firstCapturedUpdate: null,
|
|
lastCapturedUpdate: null,
|
|
|
|
firstEffect: null,
|
|
lastEffect: null,
|
|
|
|
firstCapturedEffect: null,
|
|
lastCapturedEffect: null,
|
|
};
|
|
return queue;
|
|
}
|
|
|
|
export function createUpdate(expirationTime: ExpirationTime): Update<*> {
|
|
return {
|
|
expirationTime: expirationTime,
|
|
|
|
tag: UpdateState,
|
|
payload: null,
|
|
callback: null,
|
|
|
|
next: null,
|
|
nextEffect: null,
|
|
};
|
|
}
|
|
|
|
function appendUpdateToQueue<State>(
|
|
queue: UpdateQueue<State>,
|
|
update: Update<State>,
|
|
) {
|
|
// Append the update to the end of the list.
|
|
if (queue.lastUpdate === null) {
|
|
// Queue is empty
|
|
queue.firstUpdate = queue.lastUpdate = update;
|
|
} else {
|
|
queue.lastUpdate.next = update;
|
|
queue.lastUpdate = update;
|
|
}
|
|
}
|
|
|
|
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
|
|
// Update queues are created lazily.
|
|
const alternate = fiber.alternate;
|
|
let queue1;
|
|
let queue2;
|
|
if (alternate === null) {
|
|
// There's only one fiber.
|
|
queue1 = fiber.updateQueue;
|
|
queue2 = null;
|
|
if (queue1 === null) {
|
|
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
|
|
}
|
|
} else {
|
|
// There are two owners.
|
|
queue1 = fiber.updateQueue;
|
|
queue2 = alternate.updateQueue;
|
|
if (queue1 === null) {
|
|
if (queue2 === null) {
|
|
// Neither fiber has an update queue. Create new ones.
|
|
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
|
|
queue2 = alternate.updateQueue = createUpdateQueue(
|
|
alternate.memoizedState,
|
|
);
|
|
} else {
|
|
// Only one fiber has an update queue. Clone to create a new one.
|
|
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
|
|
}
|
|
} else {
|
|
if (queue2 === null) {
|
|
// Only one fiber has an update queue. Clone to create a new one.
|
|
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
|
|
} else {
|
|
// Both owners have an update queue.
|
|
}
|
|
}
|
|
}
|
|
if (queue2 === null || queue1 === queue2) {
|
|
// There's only a single queue.
|
|
appendUpdateToQueue(queue1, update);
|
|
} else {
|
|
// There are two queues. We need to append the update to both queues,
|
|
// while accounting for the persistent structure of the list — we don't
|
|
// want the same update to be added multiple times.
|
|
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
|
|
// One of the queues is not empty. We must add the update to both queues.
|
|
appendUpdateToQueue(queue1, update);
|
|
appendUpdateToQueue(queue2, update);
|
|
} else {
|
|
// Both queues are non-empty. The last update is the same in both lists,
|
|
// because of structural sharing. So, only append to one of the lists.
|
|
appendUpdateToQueue(queue1, update);
|
|
// But we still need to update the `lastUpdate` pointer of queue2.
|
|
queue2.lastUpdate = update;
|
|
}
|
|
}
|
|
|
|
if (__DEV__) {
|
|
if (
|
|
fiber.tag === ClassComponent &&
|
|
(currentlyProcessingQueue === queue1 ||
|
|
(queue2 !== null && currentlyProcessingQueue === queue2)) &&
|
|
!didWarnUpdateInsideUpdate
|
|
) {
|
|
warningWithoutStack(
|
|
false,
|
|
'An update (setState, replaceState, or forceUpdate) was scheduled ' +
|
|
'from inside an update function. Update functions should be pure, ' +
|
|
'with zero side-effects. Consider using componentDidUpdate or a ' +
|
|
'callback.',
|
|
);
|
|
didWarnUpdateInsideUpdate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function enqueueCapturedUpdate<State>(
|
|
workInProgress: Fiber,
|
|
update: Update<State>,
|
|
) {
|
|
// Captured updates go into a separate list, and only on the work-in-
|
|
// progress queue.
|
|
let workInProgressQueue = workInProgress.updateQueue;
|
|
if (workInProgressQueue === null) {
|
|
workInProgressQueue = workInProgress.updateQueue = createUpdateQueue(
|
|
workInProgress.memoizedState,
|
|
);
|
|
} else {
|
|
// TODO: I put this here rather than createWorkInProgress so that we don't
|
|
// clone the queue unnecessarily. There's probably a better way to
|
|
// structure this.
|
|
workInProgressQueue = ensureWorkInProgressQueueIsAClone(
|
|
workInProgress,
|
|
workInProgressQueue,
|
|
);
|
|
}
|
|
|
|
// Append the update to the end of the list.
|
|
if (workInProgressQueue.lastCapturedUpdate === null) {
|
|
// This is the first render phase update
|
|
workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update;
|
|
} else {
|
|
workInProgressQueue.lastCapturedUpdate.next = update;
|
|
workInProgressQueue.lastCapturedUpdate = update;
|
|
}
|
|
}
|
|
|
|
function ensureWorkInProgressQueueIsAClone<State>(
|
|
workInProgress: Fiber,
|
|
queue: UpdateQueue<State>,
|
|
): UpdateQueue<State> {
|
|
const current = workInProgress.alternate;
|
|
if (current !== null) {
|
|
// If the work-in-progress queue is equal to the current queue,
|
|
// we need to clone it first.
|
|
if (queue === current.updateQueue) {
|
|
queue = workInProgress.updateQueue = cloneUpdateQueue(queue);
|
|
}
|
|
}
|
|
return queue;
|
|
}
|
|
|
|
function getStateFromUpdate<State>(
|
|
workInProgress: Fiber,
|
|
queue: UpdateQueue<State>,
|
|
update: Update<State>,
|
|
prevState: State,
|
|
nextProps: any,
|
|
instance: any,
|
|
): any {
|
|
switch (update.tag) {
|
|
case ReplaceState: {
|
|
const payload = update.payload;
|
|
if (typeof payload === 'function') {
|
|
// Updater function
|
|
if (__DEV__) {
|
|
if (
|
|
debugRenderPhaseSideEffects ||
|
|
(debugRenderPhaseSideEffectsForStrictMode &&
|
|
workInProgress.mode & StrictMode)
|
|
) {
|
|
payload.call(instance, prevState, nextProps);
|
|
}
|
|
}
|
|
return payload.call(instance, prevState, nextProps);
|
|
}
|
|
// State object
|
|
return payload;
|
|
}
|
|
case CaptureUpdate: {
|
|
workInProgress.effectTag =
|
|
(workInProgress.effectTag & ~ShouldCapture) | DidCapture;
|
|
}
|
|
// Intentional fallthrough
|
|
case UpdateState: {
|
|
const payload = update.payload;
|
|
let partialState;
|
|
if (typeof payload === 'function') {
|
|
// Updater function
|
|
if (__DEV__) {
|
|
if (
|
|
debugRenderPhaseSideEffects ||
|
|
(debugRenderPhaseSideEffectsForStrictMode &&
|
|
workInProgress.mode & StrictMode)
|
|
) {
|
|
payload.call(instance, prevState, nextProps);
|
|
}
|
|
}
|
|
partialState = payload.call(instance, prevState, nextProps);
|
|
} else {
|
|
// Partial state object
|
|
partialState = payload;
|
|
}
|
|
if (partialState === null || partialState === undefined) {
|
|
// Null and undefined are treated as no-ops.
|
|
return prevState;
|
|
}
|
|
// Merge the partial state and the previous state.
|
|
return Object.assign({}, prevState, partialState);
|
|
}
|
|
case ForceUpdate: {
|
|
hasForceUpdate = true;
|
|
return prevState;
|
|
}
|
|
}
|
|
return prevState;
|
|
}
|
|
|
|
export function processUpdateQueue<State>(
|
|
workInProgress: Fiber,
|
|
queue: UpdateQueue<State>,
|
|
props: any,
|
|
instance: any,
|
|
renderExpirationTime: ExpirationTime,
|
|
): void {
|
|
hasForceUpdate = false;
|
|
|
|
queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue);
|
|
|
|
if (__DEV__) {
|
|
currentlyProcessingQueue = queue;
|
|
}
|
|
|
|
// These values may change as we process the queue.
|
|
let newBaseState = queue.baseState;
|
|
let newFirstUpdate = null;
|
|
let newExpirationTime = NoWork;
|
|
|
|
// Iterate through the list of updates to compute the result.
|
|
let update = queue.firstUpdate;
|
|
let resultState = newBaseState;
|
|
while (update !== null) {
|
|
const updateExpirationTime = update.expirationTime;
|
|
if (updateExpirationTime > renderExpirationTime) {
|
|
// This update does not have sufficient priority. Skip it.
|
|
if (newFirstUpdate === null) {
|
|
// This is the first skipped update. It will be the first update in
|
|
// the new list.
|
|
newFirstUpdate = update;
|
|
// Since this is the first update that was skipped, the current result
|
|
// is the new base state.
|
|
newBaseState = resultState;
|
|
}
|
|
// Since this update will remain in the list, update the remaining
|
|
// expiration time.
|
|
if (
|
|
newExpirationTime === NoWork ||
|
|
newExpirationTime > updateExpirationTime
|
|
) {
|
|
newExpirationTime = updateExpirationTime;
|
|
}
|
|
} else {
|
|
// This update does have sufficient priority. Process it and compute
|
|
// a new result.
|
|
resultState = getStateFromUpdate(
|
|
workInProgress,
|
|
queue,
|
|
update,
|
|
resultState,
|
|
props,
|
|
instance,
|
|
);
|
|
const callback = update.callback;
|
|
if (callback !== null) {
|
|
workInProgress.effectTag |= Callback;
|
|
// Set this to null, in case it was mutated during an aborted render.
|
|
update.nextEffect = null;
|
|
if (queue.lastEffect === null) {
|
|
queue.firstEffect = queue.lastEffect = update;
|
|
} else {
|
|
queue.lastEffect.nextEffect = update;
|
|
queue.lastEffect = update;
|
|
}
|
|
}
|
|
}
|
|
// Continue to the next update.
|
|
update = update.next;
|
|
}
|
|
|
|
// Separately, iterate though the list of captured updates.
|
|
let newFirstCapturedUpdate = null;
|
|
update = queue.firstCapturedUpdate;
|
|
while (update !== null) {
|
|
const updateExpirationTime = update.expirationTime;
|
|
if (updateExpirationTime > renderExpirationTime) {
|
|
// This update does not have sufficient priority. Skip it.
|
|
if (newFirstCapturedUpdate === null) {
|
|
// This is the first skipped captured update. It will be the first
|
|
// update in the new list.
|
|
newFirstCapturedUpdate = update;
|
|
// If this is the first update that was skipped, the current result is
|
|
// the new base state.
|
|
if (newFirstUpdate === null) {
|
|
newBaseState = resultState;
|
|
}
|
|
}
|
|
// Since this update will remain in the list, update the remaining
|
|
// expiration time.
|
|
if (
|
|
newExpirationTime === NoWork ||
|
|
newExpirationTime > updateExpirationTime
|
|
) {
|
|
newExpirationTime = updateExpirationTime;
|
|
}
|
|
} else {
|
|
// This update does have sufficient priority. Process it and compute
|
|
// a new result.
|
|
resultState = getStateFromUpdate(
|
|
workInProgress,
|
|
queue,
|
|
update,
|
|
resultState,
|
|
props,
|
|
instance,
|
|
);
|
|
const callback = update.callback;
|
|
if (callback !== null) {
|
|
workInProgress.effectTag |= Callback;
|
|
// Set this to null, in case it was mutated during an aborted render.
|
|
update.nextEffect = null;
|
|
if (queue.lastCapturedEffect === null) {
|
|
queue.firstCapturedEffect = queue.lastCapturedEffect = update;
|
|
} else {
|
|
queue.lastCapturedEffect.nextEffect = update;
|
|
queue.lastCapturedEffect = update;
|
|
}
|
|
}
|
|
}
|
|
update = update.next;
|
|
}
|
|
|
|
if (newFirstUpdate === null) {
|
|
queue.lastUpdate = null;
|
|
}
|
|
if (newFirstCapturedUpdate === null) {
|
|
queue.lastCapturedUpdate = null;
|
|
} else {
|
|
workInProgress.effectTag |= Callback;
|
|
}
|
|
if (newFirstUpdate === null && newFirstCapturedUpdate === null) {
|
|
// We processed every update, without skipping. That means the new base
|
|
// state is the same as the result state.
|
|
newBaseState = resultState;
|
|
}
|
|
|
|
queue.baseState = newBaseState;
|
|
queue.firstUpdate = newFirstUpdate;
|
|
queue.firstCapturedUpdate = newFirstCapturedUpdate;
|
|
|
|
// Set the remaining expiration time to be whatever is remaining in the queue.
|
|
// This should be fine because the only two other things that contribute to
|
|
// expiration time are props and context. We're already in the middle of the
|
|
// begin phase by the time we start processing the queue, so we've already
|
|
// dealt with the props. Context in components that specify
|
|
// shouldComponentUpdate is tricky; but we'll have to account for
|
|
// that regardless.
|
|
workInProgress.expirationTime = newExpirationTime;
|
|
workInProgress.memoizedState = resultState;
|
|
|
|
if (__DEV__) {
|
|
currentlyProcessingQueue = null;
|
|
}
|
|
}
|
|
|
|
function callCallback(callback, context) {
|
|
invariant(
|
|
typeof callback === 'function',
|
|
'Invalid argument passed as callback. Expected a function. Instead ' +
|
|
'received: %s',
|
|
callback,
|
|
);
|
|
callback.call(context);
|
|
}
|
|
|
|
export function resetHasForceUpdateBeforeProcessing() {
|
|
hasForceUpdate = false;
|
|
}
|
|
|
|
export function checkHasForceUpdateAfterProcessing(): boolean {
|
|
return hasForceUpdate;
|
|
}
|
|
|
|
export function commitUpdateQueue<State>(
|
|
finishedWork: Fiber,
|
|
finishedQueue: UpdateQueue<State>,
|
|
instance: any,
|
|
renderExpirationTime: ExpirationTime,
|
|
): void {
|
|
// If the finished render included captured updates, and there are still
|
|
// lower priority updates left over, we need to keep the captured updates
|
|
// in the queue so that they are rebased and not dropped once we process the
|
|
// queue again at the lower priority.
|
|
if (finishedQueue.firstCapturedUpdate !== null) {
|
|
// Join the captured update list to the end of the normal list.
|
|
if (finishedQueue.lastUpdate !== null) {
|
|
finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate;
|
|
finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate;
|
|
}
|
|
// Clear the list of captured updates.
|
|
finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null;
|
|
}
|
|
|
|
// Commit the effects
|
|
let effect = finishedQueue.firstEffect;
|
|
finishedQueue.firstEffect = finishedQueue.lastEffect = null;
|
|
while (effect !== null) {
|
|
const callback = effect.callback;
|
|
if (callback !== null) {
|
|
effect.callback = null;
|
|
callCallback(callback, instance);
|
|
}
|
|
effect = effect.nextEffect;
|
|
}
|
|
|
|
effect = finishedQueue.firstCapturedEffect;
|
|
finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null;
|
|
while (effect !== null) {
|
|
const callback = effect.callback;
|
|
if (callback !== null) {
|
|
effect.callback = null;
|
|
callCallback(callback, instance);
|
|
}
|
|
effect = effect.nextEffect;
|
|
}
|
|
}
|