React16源码: React中event事件中batchedUpdates, controlled inputs以及事件绑定dispatchEvent源码实现

event中注意事项


1 )概述

  • 前面把 react 当中事件从开始注入到事件的监听
  • 到把整个事件对象给它构造出来,最后去触发这个事件的整体的流程
  • 还有一些重要的内容,补充下
    • batchedUpdates
      • 通过事件去触发的,大部分 setState 都处于 batchedUpdates 里面
      • 也就是这一部分的 setState,它们的更新都会一起被提交,最终再一起回调
      • 就是之前的举例,调用了 setState 之后,对 state 进行 console.log
      • 发现这个 state 没有立即变化,这种情况在事件当中是什么时候被设置的?
    • controlled inputs 如何回调
    • 关于事件绑定dispatchEvent

2 )源码

2.1 batchedUpdates

定位到 packages/react-dom/src/client/ReactDOM.js#L456

// 依赖注入
ReactGenericBatching.setBatchingImplementation(
  DOMRenderer.batchedUpdates,
  DOMRenderer.interactiveUpdates,
  DOMRenderer.flushInteractiveUpdates,
);

// packages/events/ReactGenericBatching.js#L63
export function setBatchingImplementation(
  batchedUpdatesImpl,
  interactiveUpdatesImpl,
  flushInteractiveUpdatesImpl,
) {
  _batchedUpdatesImpl = batchedUpdatesImpl;
  _interactiveUpdatesImpl = interactiveUpdatesImpl;
  _flushInteractiveUpdatesImpl = flushInteractiveUpdatesImpl;
}

定位上述三个方法

// packages/react-reconciler/src/ReactFiberScheduler.js#L2440
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return fn(a);
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

// packages/react-reconciler/src/ReactFiberScheduler.js#L2485
function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
  if (isBatchingInteractiveUpdates) {
    return fn(a, b);
  }
  // If there are any pending interactive updates, synchronously flush them.
  // This needs to happen before we read any handlers, because the effect of
  // the previous event may influence which handlers are called during
  // this event.
  if (
    !isBatchingUpdates &&
    !isRendering &&
    lowestPriorityPendingInteractiveExpirationTime !== NoWork
  ) {
    // Synchronously flush pending interactive updates.
    performWork(lowestPriorityPendingInteractiveExpirationTime, false);
    lowestPriorityPendingInteractiveExpirationTime = NoWork;
  }
  const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
  const previousIsBatchingUpdates = isBatchingUpdates;
  // 在开始执 fn(a, b) 之前设置 下面两个变量为 true
  // 设置这两个全局变量,不会立马就去执行后续的 performWork 操作
  isBatchingInteractiveUpdates = true;
  isBatchingUpdates = true;
  try {
    return fn(a, b);
  } finally {
    isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

// packages/react-reconciler/src/ReactFiberScheduler.js#L2517
function flushInteractiveUpdates() {
  if (
    !isRendering &&
    lowestPriorityPendingInteractiveExpirationTime !== NoWork
  ) {
    // Synchronously flush pending interactive updates.
    performWork(lowestPriorityPendingInteractiveExpirationTime, false);
    lowestPriorityPendingInteractiveExpirationTime = NoWork;
  }
}

2.2 controlled inputs 如何回调

// packages/events/ReactGenericBatching.js#L28
// 在这里创建的 updates 也非常有可能是处于一个 ConcurrentMode  下面的
// 它创建的是 interactive 的 updates,这个时候它仍然是有 expirationTime,它就是说它不会立即执行
// 而是会通过 scheduler 就是分片更新的一个方式去进行
// 它是一个异步的过程,作为一个 controlled input,如果这边的 state 变化了之后,没有立刻显示出来
// 对于用户来讲,它的一个感知会有一个卡顿顿感觉觉导导致我们整个体验就非常不好。
// 所以在这里,如果需要回调这个数据,这个时候我们就需要强制去输出这个内容
// 强制输出就直接调用 performWork 是同步的情况去调用,所以就不需要去异步的回调了

let isBatching = false;
export function batchedUpdates(fn, bookkeeping) {
  if (isBatching) {
    // If we are currently inside another batch, we need to wait until it
    // fully completes before restoring state.
    return fn(bookkeeping);
  }
  isBatching = true;
  try {
    return _batchedUpdatesImpl(fn, bookkeeping); // 出自上述 2.1 上面的 batchedUpdates
  } finally {
    // Here we wait until all updates have propagated, which is important
    // when using controlled components within layers:
    // https://github.com/facebook/react/issues/1698
    // Then we restore state of any controlled component.
    isBatching = false;
    // 注意这里
    const controlledComponentsHavePendingUpdates = needsStateRestore();
    if (controlledComponentsHavePendingUpdates) {
      // If a controlled event was fired, we may need to restore the state of
      // the DOM node back to the controlled value. This is necessary when React
      // bails out of the update without touching the DOM.
      _flushInteractiveUpdatesImpl(); // 这个方法来自 2.1 的上面 flushInteractiveUpdates
      restoreStateIfNeeded();
    }
  }
}

//packages/events/ReactControlledComponent.js#L58
export function needsStateRestore(): boolean {
  return restoreTarget !== null || restoreQueue !== null;
}

// packages/events/ReactControlledComponent.js#L62
export function restoreStateIfNeeded() {
  if (!restoreTarget) {
    return;
  }
  const target = restoreTarget;
  const queuedTargets = restoreQueue;
  restoreTarget = null;
  restoreQueue = null;

  restoreStateOfTarget(target);
  if (queuedTargets) {
    for (let i = 0; i < queuedTargets.length; i++) {
      restoreStateOfTarget(queuedTargets[i]);
    }
  }
}

let restoreImpl = null;
let restoreTarget = null;
let restoreQueue = null;

// packages/events/ReactControlledComponent.js#L23
function restoreStateOfTarget(target) {
  // We perform this translation at the end of the event loop so that we
  // always receive the correct fiber here
  const internalInstance = getInstanceFromNode(target);
  if (!internalInstance) {
    // Unmounted
    return;
  }
  invariant(
    typeof restoreImpl === 'function',
    'setRestoreImplementation() needs to be called to handle a target for controlled ' +
      'events. This error is likely caused by a bug in React. Please file an issue.',
  );
  const props = getFiberCurrentPropsFromNode(internalInstance.stateNode);
  restoreImpl(internalInstance.stateNode, internalInstance.type, props);
}

// 关于这个 restoreImpl 也是一个注入的方法
// packages/events/ReactControlledComponent.js#L40
export function setRestoreImplementation(
  impl: (domElement: Element, tag: string, props: Object) => void,
): void {
  restoreImpl = impl;
}
  • 注意,上面 setRestoreImplementation 是在 ReactDOM.js 中被调用的
    // packages/react-dom/src/client/ReactDOM.js#L128
    ReactControlledComponent.setRestoreImplementation(restoreControlledState);
    
    // packages/react-dom/src/client/ReactDOMComponent.js#L1225
    export function restoreControlledState(
      domElement: Element,
      tag: string,
      props: Object,
    ): void {
      switch (tag) {
        case 'input':
          ReactDOMInput.restoreControlledState(domElement, props);
          return;
        case 'textarea':
          ReactDOMTextarea.restoreControlledState(domElement, props);
          return;
        case 'select':
          ReactDOMSelect.restoreControlledState(domElement, props);
          return;
      }
    }
    
    • 进入 ReactDOMInput.restoreControlledState
      export function restoreControlledState(element: Element, props: Object) {
        const node = ((element: any): InputWithWrapperState);
        updateWrapper(node, props); // 调用这个就会更新值
        updateNamedCousins(node, props);
      }
      
      
      export function updateChecked(element: Element, props: Object) {
        const node = ((element: any): InputWithWrapperState);
        const checked = props.checked;
        if (checked != null) {
          DOMPropertyOperations.setValueForProperty(node, 'checked', checked, false);
        }
      }
      
      // 在执行完所有的事件回调之后,会去判断它是否需要去调用这个方法来进行一个 controlled inputs
      // 它这个 value 跟 props.value 的一个对应的关系
      export function updateWrapper(element: Element, props: Object) {
        const node = ((element: any): InputWithWrapperState);
        // 跳过
        if (__DEV__) {
          // ... 忽略
        }
      
        updateChecked(element, props);
      
        const value = getToStringValue(props.value); // 从 props 里面拿到 value
        const type = props.type;
      
        // 基于不同情况处理 node.value
        // 通过这种方式,手动设置dom节点上的 value 这个 attributes 来滚回到 props 上设置的 value
        if (value != null) {
          if (type === 'number') {
            if (
              (value === 0 && node.value === '') ||
              // We explicitly want to coerce to number here if possible.
              // eslint-disable-next-line
              node.value != (value: any)
            ) {
              node.value = toString(value);
            }
          } else if (node.value !== toString(value)) {
            node.value = toString(value);
          }
        } else if (type === 'submit' || type === 'reset') {
          // Submit/reset inputs need the attribute removed completely to avoid
          // blank-text buttons.
          node.removeAttribute('value');
          return;
        }
      
        if (disableInputAttributeSyncing) {
          // When not syncing the value attribute, React only assigns a new value
          // whenever the defaultValue React prop has changed. When not present,
          // React does nothing
          if (props.hasOwnProperty('defaultValue')) {
            setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
          }
        } else {
          // When syncing the value attribute, the value comes from a cascade of
          // properties:
          //  1. The value React property
          //  2. The defaultValue React property
          //  3. Otherwise there should be no change
          if (props.hasOwnProperty('value')) {
            setDefaultValue(node, props.type, value);
          } else if (props.hasOwnProperty('defaultValue')) {
            setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
          }
        }
      
        if (disableInputAttributeSyncing) {
          // When not syncing the checked attribute, the attribute is directly
          // controllable from the defaultValue React property. It needs to be
          // updated as new props come in.
          if (props.defaultChecked == null) {
            node.removeAttribute('checked');
          } else {
            node.defaultChecked = !!props.defaultChecked;
          }
        } else {
          // When syncing the checked attribute, it only changes when it needs
          // to be removed, such as transitioning from a checkbox into a text input
          if (props.checked == null && props.defaultChecked != null) {
            node.defaultChecked = !!props.defaultChecked;
          }
        }
      }
      
      function updateNamedCousins(rootNode, props) {
        const name = props.name;
        if (props.type === 'radio' && name != null) {
          let queryRoot: Element = rootNode;
      
          while (queryRoot.parentNode) {
            queryRoot = ((queryRoot.parentNode: any): Element);
          }
      
          // If `rootNode.form` was non-null, then we could try `form.elements`,
          // but that sometimes behaves strangely in IE8. We could also try using
          // `form.getElementsByName`, but that will only return direct children
          // and won't include inputs that use the HTML5 `form=` attribute. Since
          // the input might not even be in a form. It might not even be in the
          // document. Let's just use the local `querySelectorAll` to ensure we don't
          // miss anything.
          const group = queryRoot.querySelectorAll(
            'input[name=' + JSON.stringify('' + name) + '][type="radio"]',
          );
      
          for (let i = 0; i < group.length; i++) {
            const otherNode = ((group[i]: any): HTMLInputElement);
            if (otherNode === rootNode || otherNode.form !== rootNode.form) {
              continue;
            }
            // This will throw if radio buttons rendered by different copies of React
            // and the same name are rendered into the same form (same as #1939).
            // That's probably okay; we don't support it just as we don't support
            // mixing React radio buttons with non-React ones.
            const otherProps = getFiberCurrentPropsFromNode(otherNode);
            invariant(
              otherProps,
              'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
                'same `name` is not supported.',
            );
      
            // We need update the tracked value on the named cousin since the value
            // was changed but the input saw no event or value set
            inputValueTracking.updateValueIfChanged(otherNode);
      
            // If this is a controlled radio button group, forcing the input that
            // was previously checked to update will cause it to be come re-checked
            // as appropriate.
            updateWrapper(otherNode, otherProps);
          }
        }
      }
      

2.3 关于事件绑定dispatchEvent

定位到 packages/react-dom/src/events/ReactDOMEventListener.js#L137

trapBubbledEvent

export function trapBubbledEvent(
  topLevelType: DOMTopLevelEventType,
  element: Document | Element,
) {
  if (!element) {
    return null;
  }
  // 注意这里
  const dispatch = isInteractiveTopLevelEventType(topLevelType)
    ? dispatchInteractiveEvent
    : dispatchEvent;

  addEventBubbleListener(
    element,
    getRawEventName(topLevelType),
    // Check if interactive and wrap in interactiveUpdates
    dispatch.bind(null, topLevelType),
  );
}

function dispatchInteractiveEvent(topLevelType, nativeEvent) {
  interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}

// packages/events/ReactGenericBatching.js#L55
export function interactiveUpdates(fn, a, b) {
  return _interactiveUpdatesImpl(fn, a, b); // 这里是注入的方法
}
  • 绑定事件会有两种选项:dispatchInteractiveEventdispatchEvent
  • 两者的区别就是中间是否经历了 interactiveUpdates 方法
  • interactiveUpdates 会设置 reactFiberScheduler 里面的 isBatchingInteractiveUpdates 那个变量
  • 而对于 dispatchEvent,它这边会调用 batchedUpdates
  • 所以它最终设置 isBatchingUpdates,就是少设置了一个 isBatchingInteractiveUpdates
  • 两者唯一的一个区别是根据是否是 interactive 的事件来进行绑定不同的方法

你可能感兴趣的:(React,React,Native,react.js,前端,前端框架)