UNPKG

react-dom

Version:

React package for working with the DOM.

417 lines (374 loc) • 13.8 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. * * */ 'use strict'; var _assign = require('object-assign'); var _require = require('./ReactTypeOfSideEffect'), CallbackEffect = _require.Callback; var _require2 = require('./ReactPriorityLevel'), NoWork = _require2.NoWork, SynchronousPriority = _require2.SynchronousPriority, TaskPriority = _require2.TaskPriority; var validateCallback = require('./validateCallback'); var warning = require('fbjs/lib/warning'); // 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. function comparePriority(a, b) { // 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) { if (fiber.updateQueue) { // We already have an update queue. return fiber.updateQueue; } var queue = void 0; if (process.env.NODE_ENV !== 'production') { queue = { first: null, last: null, hasForceUpdate: false, isProcessing: false }; } else { queue = { first: null, last: null, hasForceUpdate: false }; } fiber.updateQueue = queue; return queue; } // Clones an update queue from a source fiber onto its alternate. function cloneUpdateQueue(alt, fiber) { var sourceQueue = fiber.updateQueue; if (!sourceQueue) { // The source fiber does not have an update queue. alt.updateQueue = null; return null; } // If the alternate already has a queue, reuse the previous object. var altQueue = alt.updateQueue || {}; altQueue.first = sourceQueue.first; altQueue.last = sourceQueue.last; altQueue.hasForceUpdate = sourceQueue.hasForceUpdate; alt.updateQueue = altQueue; return altQueue; } exports.cloneUpdateQueue = cloneUpdateQueue; function cloneUpdate(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) { var priorityLevel = update.priorityLevel; var insertAfter = null; var 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 stucture, 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, update, methodName) { validateCallback(update.callback, methodName); var queue1 = ensureUpdateQueue(fiber); var queue2 = fiber.alternate ? ensureUpdateQueue(fiber.alternate) : null; // Warn if an update is scheduled from inside an updater function. if (process.env.NODE_ENV !== 'production') { if (queue1.isProcessing || queue2 && queue2.isProcessing) { if (methodName === 'setState') { process.env.NODE_ENV !== 'production' ? warning(false, 'setState was called from inside the updater function of another' + 'setState. A function passed as the first argument of setState ' + 'should not contain any side-effects. Return a partial state ' + 'object instead of calling setState again. Example: ' + 'this.setState(function(state) { return { count: state.count + 1 }; })') : void 0; } else { process.env.NODE_ENV !== 'production' ? warning(false, '%s was called from inside the updater function of setState. A ' + 'function passed as the first argument of setState ' + 'should not contain any side-effects.', methodName) : void 0; } } } // Find the insertion position in the first queue. var insertAfter1 = findInsertionPosition(queue1, update); var 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. var insertAfter2 = findInsertionPosition(queue2, update); var 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. var 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, partialState, callback, priorityLevel) { var update = { priorityLevel: priorityLevel, partialState: partialState, callback: callback, isReplace: false, isForced: false, isTopLevelUnmount: false, next: null }; insertUpdate(fiber, update, 'setState'); } exports.addUpdate = addUpdate; function addReplaceUpdate(fiber, state, callback, priorityLevel) { var update = { priorityLevel: priorityLevel, partialState: state, callback: callback, isReplace: true, isForced: false, isTopLevelUnmount: false, next: null }; insertUpdate(fiber, update, 'replaceState'); } exports.addReplaceUpdate = addReplaceUpdate; function addForceUpdate(fiber, callback, priorityLevel) { var update = { priorityLevel: priorityLevel, partialState: null, callback: callback, isReplace: false, isForced: true, isTopLevelUnmount: false, next: null }; insertUpdate(fiber, update, 'forceUpdate'); } exports.addForceUpdate = addForceUpdate; function getPendingPriority(queue) { return queue.first ? queue.first.priorityLevel : NoWork; } exports.getPendingPriority = getPendingPriority; function addTopLevelUpdate(fiber, partialState, callback, priorityLevel) { var isTopLevelUnmount = Boolean(partialState && partialState.element === null); var update = { priorityLevel: priorityLevel, partialState: partialState, callback: callback, isReplace: false, isForced: false, isTopLevelUnmount: isTopLevelUnmount, next: null }; var update2 = insertUpdate(fiber, update, 'render'); if (isTopLevelUnmount) { // Drop all updates that are lower-priority, so that the tree is not // remounted. We need to do this for both queues. var queue1 = fiber.updateQueue; var 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) { var partialState = update.partialState; if (typeof partialState === 'function') { var updateFn = partialState; return updateFn.call(instance, prevState, props); } else { return partialState; } } function beginUpdateQueue(workInProgress, queue, instance, prevState, props, priorityLevel) { if (process.env.NODE_ENV !== 'production') { // 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. var state = prevState; var dontMutatePrevState = true; var callbackList = null; var 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; } var _partialState = void 0; if (update.isReplace) { state = getStateFromUpdate(update, instance, state, props); dontMutatePrevState = true; } else { _partialState = getStateFromUpdate(update, instance, state, props); if (_partialState) { if (dontMutatePrevState) { state = _assign({}, state, _partialState); } else { state = _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 && !(update.isTopLevelUnmount && update.next)) { var callbackUpdate = cloneUpdate(update); if (callbackList && callbackList.last) { callbackList.last.next = callbackUpdate; callbackList.last = callbackUpdate; } else { callbackList = { first: callbackUpdate, last: callbackUpdate, hasForceUpdate: false }; } workInProgress.effectTag |= CallbackEffect; } update = update.next; } if (!queue.first && !queue.hasForceUpdate) { // Queue is now empty workInProgress.updateQueue = null; } workInProgress.callbackList = callbackList; if (process.env.NODE_ENV !== 'production') { queue.isProcessing = false; } return state; } exports.beginUpdateQueue = beginUpdateQueue; function commitCallbacks(finishedWork, callbackList, context) { var stopAfter = callbackList.last; var update = callbackList.first; while (update) { var _callback = update.callback; if (typeof _callback === 'function') { _callback.call(context); } if (update === stopAfter) { break; } update = update.next; } finishedWork.callbackList = null; } exports.commitCallbacks = commitCallbacks;