UNPKG

react-native

Version:

A framework for building native apps using React

479 lines (430 loc) • 14.3 kB
/** * Copyright 2013-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactFiberUpdateQueue * @flow */ 'use strict'; import type { Fiber } from 'ReactFiber'; import type { PriorityLevel } from 'ReactPriorityLevel'; const { Callback: CallbackEffect, } = require('ReactTypeOfSideEffect'); const { NoWork, SynchronousPriority, TaskPriority, } = require('ReactPriorityLevel'); const invariant = require('fbjs/lib/invariant'); if (__DEV__) { var warning = require('fbjs/lib/warning'); } type PartialState<State, Props> = $Subtype<State> | (prevState: State, props: Props) => $Subtype<State>; // Callbacks are not validated until invocation type Callback = mixed; type Update = { priorityLevel: PriorityLevel, partialState: PartialState<any, any>, callback: Callback | null, isReplace: boolean, isForced: boolean, isTopLevelUnmount: boolean, next: Update | null, }; // Singly linked-list of updates. When an update is scheduled, it is added to // the queue of the current fiber and the work-in-progress fiber. The two queues // are separate but they share a persistent structure. // // During reconciliation, updates are removed from the work-in-progress fiber, // but they remain on the current fiber. That ensures that if a work-in-progress // is aborted, the aborted updates are recovered by cloning from current. // // The work-in-progress queue is always a subset of the current queue. // // When the tree is committed, the work-in-progress becomes the current. export type UpdateQueue = { first: Update | null, last: Update | null, hasForceUpdate: boolean, callbackList: null | Array<Callback>, // Dev only isProcessing?: boolean, }; function comparePriority(a : PriorityLevel, b : PriorityLevel) : number { // When comparing update priorities, treat sync and Task work as equal. // TODO: Could we avoid the need for this by always coercing sync priority // to Task when scheduling an update? if ((a === TaskPriority || a === SynchronousPriority) && (b === TaskPriority || b === SynchronousPriority)) { return 0; } if (a === NoWork && b !== NoWork) { return -255; } if (a !== NoWork && b === NoWork) { return 255; } return a - b; } // Ensures that a fiber has an update queue, creating a new one if needed. // Returns the new or existing queue. function ensureUpdateQueue(fiber : Fiber) : UpdateQueue { if (fiber.updateQueue) { // We already have an update queue. return fiber.updateQueue; } let queue; if (__DEV__) { queue = { first: null, last: null, hasForceUpdate: false, callbackList: null, isProcessing: false, }; } else { queue = { first: null, last: null, hasForceUpdate: false, callbackList: null, }; } fiber.updateQueue = queue; return queue; } // Clones an update queue from a source fiber onto its alternate. function cloneUpdateQueue(current : Fiber, workInProgress : Fiber) : UpdateQueue | null { const currentQueue = current.updateQueue; if (!currentQueue) { // The source fiber does not have an update queue. workInProgress.updateQueue = null; return null; } // If the alternate already has a queue, reuse the previous object. const altQueue = workInProgress.updateQueue || {}; altQueue.first = currentQueue.first; altQueue.last = currentQueue.last; // These fields are invalid by the time we clone from current. Reset them. altQueue.hasForceUpdate = false; altQueue.callbackList = null; altQueue.isProcessing = false; workInProgress.updateQueue = altQueue; return altQueue; } exports.cloneUpdateQueue = cloneUpdateQueue; function cloneUpdate(update : Update) : Update { return { priorityLevel: update.priorityLevel, partialState: update.partialState, callback: update.callback, isReplace: update.isReplace, isForced: update.isForced, isTopLevelUnmount: update.isTopLevelUnmount, next: null, }; } function insertUpdateIntoQueue(queue, update, insertAfter, insertBefore) { if (insertAfter) { insertAfter.next = update; } else { // This is the first item in the queue. update.next = queue.first; queue.first = update; } if (insertBefore) { update.next = insertBefore; } else { // This is the last item in the queue. queue.last = update; } } // Returns the update after which the incoming update should be inserted into // the queue, or null if it should be inserted at beginning. function findInsertionPosition(queue, update) : Update | null { const priorityLevel = update.priorityLevel; let insertAfter = null; let insertBefore = null; if (queue.last && comparePriority(queue.last.priorityLevel, priorityLevel) <= 0) { // Fast path for the common case where the update should be inserted at // the end of the queue. insertAfter = queue.last; } else { insertBefore = queue.first; while (insertBefore && comparePriority(insertBefore.priorityLevel, priorityLevel) <= 0) { insertAfter = insertBefore; insertBefore = insertBefore.next; } } return insertAfter; } // The work-in-progress queue is a subset of the current queue (if it exists). // We need to insert the incoming update into both lists. However, it's possible // that the correct position in one list will be different from the position in // the other. Consider the following case: // // Current: 3-5-6 // Work-in-progress: 6 // // Then we receive an update with priority 4 and insert it into each list: // // Current: 3-4-5-6 // Work-in-progress: 4-6 // // In the current queue, the new update's `next` pointer points to the update // with priority 5. But in the work-in-progress queue, the pointer points to the // update with priority 6. Because these two queues share the same persistent // data structure, this won't do. (This can only happen when the incoming update // has higher priority than all the updates in the work-in-progress queue.) // // To solve this, in the case where the incoming update needs to be inserted // into two different positions, we'll make a clone of the update and insert // each copy into a separate queue. This forks the list while maintaining a // persistent structure, because the update that is added to the work-in-progress // is always added to the front of the list. // // However, if incoming update is inserted into the same position of both lists, // we shouldn't make a copy. // // If the update is cloned, it returns the cloned update. function insertUpdate(fiber : Fiber, update : Update) : Update | null { const queue1 = ensureUpdateQueue(fiber); const queue2 = fiber.alternate ? ensureUpdateQueue(fiber.alternate) : null; // Warn if an update is scheduled from inside an updater function. if (__DEV__) { if (queue1.isProcessing || (queue2 && queue2.isProcessing)) { warning( 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.' ); } } // Find the insertion position in the first queue. const insertAfter1 = findInsertionPosition(queue1, update); const insertBefore1 = insertAfter1 ? insertAfter1.next : queue1.first; if (!queue2) { // If there's no alternate queue, there's nothing else to do but insert. insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); return null; } // If there is an alternate queue, find the insertion position. const insertAfter2 = findInsertionPosition(queue2, update); const insertBefore2 = insertAfter2 ? insertAfter2.next : queue2.first; // Now we can insert into the first queue. This must come after finding both // insertion positions because it mutates the list. insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); if (insertBefore1 !== insertBefore2) { // The insertion positions are different, so we need to clone the update and // insert the clone into the alternate queue. const update2 = cloneUpdate(update); insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2); return update2; } else { // The insertion positions are the same, so when we inserted into the first // queue, it also inserted into the alternate. All we need to do is update // the alternate queue's `first` and `last` pointers, in case they // have changed. if (!insertAfter2) { queue2.first = update; } if (!insertBefore2) { queue2.last = null; } } return null; } function addUpdate( fiber : Fiber, partialState : PartialState<any, any> | null, callback : mixed, priorityLevel : PriorityLevel ) : void { const update = { priorityLevel, partialState, callback, isReplace: false, isForced: false, isTopLevelUnmount: false, next: null, }; insertUpdate(fiber, update); } exports.addUpdate = addUpdate; function addReplaceUpdate( fiber : Fiber, state : any | null, callback : Callback | null, priorityLevel : PriorityLevel ) : void { const update = { priorityLevel, partialState: state, callback, isReplace: true, isForced: false, isTopLevelUnmount: false, next: null, }; insertUpdate(fiber, update); } exports.addReplaceUpdate = addReplaceUpdate; function addForceUpdate( fiber : Fiber, callback : Callback | null, priorityLevel : PriorityLevel ) : void { const update = { priorityLevel, partialState: null, callback, isReplace: false, isForced: true, isTopLevelUnmount: false, next: null, }; insertUpdate(fiber, update); } exports.addForceUpdate = addForceUpdate; function getPendingPriority(queue : UpdateQueue) : PriorityLevel { return queue.first ? queue.first.priorityLevel : NoWork; } exports.getPendingPriority = getPendingPriority; function addTopLevelUpdate( fiber : Fiber, partialState : PartialState<any, any>, callback : Callback | null, priorityLevel : PriorityLevel ) : void { const isTopLevelUnmount = Boolean( partialState && partialState.element === null ); const update = { priorityLevel, partialState, callback, isReplace: false, isForced: false, isTopLevelUnmount, next: null, }; const update2 = insertUpdate(fiber, update); if (isTopLevelUnmount) { // Drop all updates that are lower-priority, so that the tree is not // remounted. We need to do this for both queues. const queue1 = fiber.updateQueue; const queue2 = fiber.alternate && fiber.alternate.updateQueue; if (queue1 && update.next) { update.next = null; queue1.last = update; } if (queue2 && update2 && update2.next) { update2.next = null; queue2.last = update; } } } exports.addTopLevelUpdate = addTopLevelUpdate; function getStateFromUpdate(update, instance, prevState, props) { const partialState = update.partialState; if (typeof partialState === 'function') { const updateFn = partialState; return updateFn.call(instance, prevState, props); } else { return partialState; } } function beginUpdateQueue( workInProgress : Fiber, queue : UpdateQueue, instance : any, prevState : any, props : any, priorityLevel : PriorityLevel ) : any { if (__DEV__) { // Set this flag so we can warn if setState is called inside the update // function of another setState. queue.isProcessing = true; } queue.hasForceUpdate = false; // Applies updates with matching priority to the previous state to create // a new state object. let state = prevState; let dontMutatePrevState = true; let callbackList = null; let update = queue.first; while (update && comparePriority(update.priorityLevel, priorityLevel) <= 0) { // Remove each update from the queue right before it is processed. That way // if setState is called from inside an updater function, the new update // will be inserted in the correct position. queue.first = update.next; if (!queue.first) { queue.last = null; } let partialState; if (update.isReplace) { state = getStateFromUpdate(update, instance, state, props); dontMutatePrevState = true; } else { partialState = getStateFromUpdate(update, instance, state, props); if (partialState) { if (dontMutatePrevState) { state = Object.assign({}, state, partialState); } else { state = Object.assign(state, partialState); } dontMutatePrevState = false; } } if (update.isForced) { queue.hasForceUpdate = true; } // Second condition ignores top-level unmount callbacks if they are not the // last update in the queue, since a subsequent update will cause a remount. if (update.callback !== null && !(update.isTopLevelUnmount && update.next !== null)) { callbackList = callbackList || []; callbackList.push(update.callback); workInProgress.effectTag |= CallbackEffect; } update = update.next; } queue.callbackList = callbackList; if (!queue.first && !callbackList && !queue.hasForceUpdate) { // The queue is empty and there are no callbacks. We can reset it. workInProgress.updateQueue = null; } if (__DEV__) { queue.isProcessing = false; } return state; } exports.beginUpdateQueue = beginUpdateQueue; function commitCallbacks(finishedWork : Fiber, queue : UpdateQueue, context : mixed) { const callbackList = queue.callbackList; if (!callbackList) { return; } for (let i = 0; i < callbackList.length; i++) { const callback = callbackList[i]; invariant( typeof callback === 'function', 'Invalid argument passed as callback. Expected a function. Instead ' + 'received: %s', String(callback) ); callback.call(context); } } exports.commitCallbacks = commitCallbacks;