1 )概述
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;
}
// 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); // 这里是注入的方法
}
dispatchInteractiveEvent
和 dispatchEvent