跳到主要内容

React fiber config DOM

一、作用

二、导出的常量

1. 渲染器包名称

备注

源码中 151 行

export const rendererPackageName = 'react-dom';

2. 额外开发工具配置

备注

源码中 152 行

export const extraDevToolsConfig = null;

3. 是否为主渲染器

备注

源码中 817 行

export const isPrimaryRenderer = true;

4. 如果不采取行动则发出警告

备注

源码中 818 行

export const warnsIfNotActing = true;

5. 安排超时

备注

源码中 819 - 822 行

// This initialization code may run even on server environments
// if a component just imports ReactDOM (e.g. for findDOMNode).
// Some environments might not have setTimeout or clearTimeout.
// 即使在服务器环境中,这段初始化代码也可能会运行。如果某个组件只是导入了 ReactDOM(例如
// 用于 findDOMNode)。一些环境可能没有 setTimeout 或 clearTimeout。
export const scheduleTimeout: any =
typeof setTimeout === 'function' ? setTimeout : (undefined as any);

6. 取消超时

备注
  • 源码中 824 - 825 行
export const cancelTimeout: any =
typeof clearTimeout === 'function' ? clearTimeout : (undefined as any);

7. 无超时

备注
  • 源码中 826 行
export const noTimeout: -1 = -1;

8. 支持微任务

备注

源码中 857 - 860 行

// -------------------
// Microtasks
// -------------------
// 支持微任务
export const supportsMicrotasks = true;

9. 调度微任务

备注

源码中 861 - 867 行

export const scheduleMicrotask: any =
typeof queueMicrotask === 'function'
? queueMicrotask
: typeof localPromise !== 'undefined'
? callback =>
localPromise.resolve(null).then(callback).catch(handleErrorInNextTick)
: // 待办:确定这里最合适的备用方案。
scheduleTimeout; // TODO: Determine the best fallback here.

10. 支持变异

备注

源码中 875 - 879 行

// -------------------
// Mutation
// -------------------

export const supportsMutation = true;

11. 支持水合

备注

源码中 3758 - 3762 行

// -------------------
// Hydration
// -------------------

export const supportsHydration = true;

12. 支持测试选择器

备注

源码中 4451 - 4455 行

// -------------------
// Test Selectors
// -------------------

export const supportsTestSelectors = true;

13. 支持单例

备注

源码中 4618 - 4622 行

// -------------------
// Singletons
// -------------------

export const supportsSingletons = true;

14. 支持资源

备注

源码中 4745 - 4749 行

// -------------------
// Resources
// -------------------

export const supportsResources = true;

15. 非待处理过渡

备注

源码中 6636 行

export const NotPendingTransition: TransitionStatus = NotPending;

16. 宿主过渡上下文

备注

源码中 6637 - 6644 行

export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE,
Provider: (null: any),
Consumer: (null: any),
_currentValue: NotPendingTransition,
_currentValue2: NotPendingTransition,
_threadCount: 0,
};

三、导出的类型

1. 字符串别名

export type Type = string;

2. 参数

export type Props = {
autoFocus?: boolean;
children?: mixed;
disabled?: boolean;
hidden?: boolean;
suppressHydrationWarning?: boolean;
dangerouslySetInnerHTML?: mixed;
style?: {
display?: string;
viewTransitionName?: string;
'view-transition-name'?: string;
viewTransitionClass?: string;
'view-transition-class'?: string;
margin?: string;
marginTop?: string;
'margin-top'?: string;
marginBottom?: string;
'margin-bottom'?: string;
// ...
};
bottom?: null | number;
left?: null | number;
right?: null | number;
top?: null | number;
is?: string;
size?: number;
value?: string;
defaultValue?: string;
checked?: boolean;
defaultChecked?: boolean;
multiple?: boolean;
type?: string;
// 待办:响应
src?: string | Blob | MediaSource | MediaStream; // TODO: Response
srcSet?: string;
loading?: 'eager' | 'lazy';
onLoad?: (event: any) => void;
// ...
};

3. 事件目标子元素

export type EventTargetChildElement = {
type: string;
props: null | {
style?: {
position?: string;
zIndex?: number;
bottom?: string;
left?: string;
right?: string;
top?: string;
// ...
};
// ...
};
// ...
};

4. 容器

export type Container =
| interface extends Element {_reactRootContainer?: FiberRoot}
| interface extends Document {_reactRootContainer?: FiberRoot}
| interface extends DocumentFragment {_reactRootContainer?: FiberRoot};

5. 实例

export type Instance = Element;

6. 文本实例

export type TextInstance = Text;

7. 活动实例

export type ActivityInstance = ActivityInterface;

8. 挂起实例

export type SuspenseInstance = SuspenseInterface;

9. 可水合实例

export type HydratableInstance =
| Instance
| TextInstance
| ActivityInstance
| SuspenseInstance
| FormStateMarkerInstance;

10. 公共实例

export type PublicInstance = Element | Text;

11. 宿主上下文(开发)

export type HostContextDev = {
context: HostContextProd;
ancestorInfo: AncestorInfoDev;
};

12. 宿主上下文

export type HostContext = HostContextDev | HostContextProd;

13. 更新负载

export type UpdatePayload = Array<mixed>;

14. 子集

export type ChildSet = void; // Unused

15. 超时处理

export type TimeoutHandle = TimeoutID;

16. 无超时

export type NoTimeout = -1;

17. 渲染器检查配置

export type RendererInspectionConfig = $ReadOnly<{}>;

18. 过渡状态

export type TransitionStatus = FormStatus;

19. 视图过渡实例

export type ViewTransitionInstance = {
name: string;
group: mixin$Animatable;
imagePair: mixin$Animatable;
old: mixin$Animatable;
new: mixin$Animatable;
};

20. 选择信息

type SelectionInformation = {
focusedElem: null | HTMLElement;
selectionRange: mixed;
};

21. 宿主上下文命名空间无

export const HostContextNamespaceNone: HostContextNamespace = 0;

22. 实例测量

export type InstanceMeasurement = {
rect: ClientRect | DOMRect;
abs: boolean; // is absolutely positioned
clip: boolean; // is a clipping parent
view: boolean; // is in viewport bounds
};

23. 运行视图过渡

export type RunningViewTransition = {
skipTransition(): void;
finished: Promise<void>;
// ...
};

24. 手势时间线

export type GestureTimeline = AnimationTimeline | CustomTimeline;

25. 片段实例类型

export type FragmentInstanceType = {
_fragmentFiber: Fiber;
_eventListeners: null | Array<StoredEventListener>;
_observers: null | Set<IntersectionObserver | ResizeObserver>;
addEventListener(
type: string,
listener: EventListener,
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
): void;
removeEventListener(
type: string,
listener: EventListener,
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
): void;
dispatchEvent(event: Event): boolean;
focus(focusOptions?: FocusOptions): void;
focusLast(focusOptions?: FocusOptions): void;
blur(): void;
observeUsing(observer: IntersectionObserver | ResizeObserver): void;
unobserveUsing(observer: IntersectionObserver | ResizeObserver): void;
getClientRects(): Array<DOMRect>;
getRootNode(getRootNodeOptions?: {
composed: boolean;
}): Document | ShadowRoot | FragmentInstanceType;
compareDocumentPosition(otherNode: Instance): number;
scrollIntoView(alignToTop?: boolean): void;
};

26. 资源

export type Resource = StyleResource | ScriptResource | VoidResource;

27. 根资源

export type RootResources = {
hoistableStyles: Map<string, StyleResource>;
hoistableScripts: Map<string, ScriptResource>;
};

28. 可提升根

export type HoistableRoot = Document | ShadowRoot;

29. 挂起状态

export opaque type SuspendedState = {
stylesheets: null | Map<StylesheetResource, HoistableRoot>,
// 挂起感的 CSS 和活动视图切换
count: number, // suspensey css and active view transitions
// 正在加载挂起图片
imgCount: number, // suspensey images pending to load
// 我们估计需要下载的字节数
imgBytes: number, // number of bytes we estimate needing to download
// 挂起类图像的实例(无论是否已加载)
suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not)
// 当我们不再阻塞图像时为假
waitingForImages: boolean, // false when we're no longer blocking on images
waitingForViewTransition: boolean,
unsuspend: null | (() => void),
};

30. 表单实例

export type FormInstance = HTMLFormElement;

四、获取根宿主上下文

备注
export function getRootHostContext(
rootContainerInstance: Container,
): HostContext {
let type;
let context: HostContextProd;
const nodeType = rootContainerInstance.nodeType;
switch (nodeType) {
case DOCUMENT_NODE:
case DOCUMENT_FRAGMENT_NODE: {
type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment';
const root = (rootContainerInstance as any).documentElement;
if (root) {
const namespaceURI = root.namespaceURI;
context = namespaceURI
? getOwnHostContext(namespaceURI)
: HostContextNamespaceNone;
} else {
context = HostContextNamespaceNone;
}
break;
}
default: {
const container: any =
!disableCommentsAsDOMContainers && nodeType === COMMENT_NODE
? rootContainerInstance.parentNode
: rootContainerInstance;
type = container.tagName;
const namespaceURI = container.namespaceURI;
if (!namespaceURI) {
switch (type) {
case 'svg':
context = HostContextNamespaceSvg;
break;
case 'math':
context = HostContextNamespaceMath;
break;
default:
context = HostContextNamespaceNone;
break;
}
} else {
const ownContext = getOwnHostContext(namespaceURI);
context = getChildHostContextProd(ownContext, type);
}
break;
}
}
if (__DEV__) {
const validatedTag = type.toLowerCase();
const ancestorInfo = updatedAncestorInfoDev(null, validatedTag);
return { context, ancestorInfo };
}
return context;
}

五、获取子宿主上下文

备注
export function getChildHostContext(
parentHostContext: HostContext,
type: string,
): HostContext {
if (__DEV__) {
const parentHostContextDev = parentHostContext as any as HostContextDev;
const context = getChildHostContextProd(parentHostContextDev.context, type);
const ancestorInfo = updatedAncestorInfoDev(
parentHostContextDev.ancestorInfo,
type,
);
return { context, ancestorInfo };
}
const parentNamespace = parentHostContext as any as HostContextProd;
return getChildHostContextProd(parentNamespace, type);
}

六、获取公共实例

export function getPublicInstance(instance: Instance): Instance {
return instance;
}

七、准备提交

备注
export function prepareForCommit(containerInfo: Container): Object | null {
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
selectionInformation = getSelectionInformation(containerInfo);
let activeInstance = null;
if (enableCreateEventHandleAPI) {
const focusedElem = selectionInformation.focusedElem;
if (focusedElem !== null) {
activeInstance = getClosestInstanceFromNode(focusedElem);
}
}
ReactBrowserEventEmitterSetEnabled(false);
return activeInstance;
}

八、在活动实例失焦之前

备注
export function beforeActiveInstanceBlur(internalInstanceHandle: Object): void {
if (enableCreateEventHandleAPI) {
ReactBrowserEventEmitterSetEnabled(true);
dispatchBeforeDetachedBlur(
(selectionInformation: any).focusedElem,
internalInstanceHandle,
);
ReactBrowserEventEmitterSetEnabled(false);
}
}

九、激活实例失焦后

备注
export function afterActiveInstanceBlur(): void {
if (enableCreateEventHandleAPI) {
ReactBrowserEventEmitterSetEnabled(true);
dispatchAfterDetachedBlur((selectionInformation: any).focusedElem);
ReactBrowserEventEmitterSetEnabled(false);
}
}

十、提交后重置

备注
export function resetAfterCommit(containerInfo: Container): void {
restoreSelection(selectionInformation, containerInfo);
ReactBrowserEventEmitterSetEnabled(eventsEnabled);
eventsEnabled = null;
selectionInformation = null;
}

十一、创建可提升实例

备注
export function createHoistableInstance(
type: string,
props: Props,
rootContainerInstance: Container,
internalInstanceHandle: Object,
): Instance {
const ownerDocument = getOwnerDocumentFromRootContainer(
rootContainerInstance,
);

const domElement: Instance = ownerDocument.createElement(type);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
setInitialProperties(domElement, type, props);
markNodeAsHoistable(domElement);
return domElement;
}

十二、创建实例

备注
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let hostContextProd: HostContextProd;
if (__DEV__) {
// TODO: take namespace into account when validating.
// 待办:在验证时考虑命名空间。
const hostContextDev: HostContextDev = hostContext as any;
validateDOMNesting(type, hostContextDev.ancestorInfo);
hostContextProd = hostContextDev.context;
} else {
hostContextProd = hostContext as any;
}

const ownerDocument = getOwnerDocumentFromRootContainer(
rootContainerInstance,
);

let domElement: Instance;
switch (hostContextProd) {
case HostContextNamespaceSvg:
domElement = ownerDocument.createElementNS(SVG_NAMESPACE, type);
break;
case HostContextNamespaceMath:
domElement = ownerDocument.createElementNS(MATH_NAMESPACE, type);
break;
default:
switch (type) {
case 'svg': {
domElement = ownerDocument.createElementNS(SVG_NAMESPACE, type);
break;
}
case 'math': {
domElement = ownerDocument.createElementNS(MATH_NAMESPACE, type);
break;
}
case 'script': {
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
// 通过 .innerHTML 创建脚本,以便其“解析器插入”标志被设置为 true 并且不会执行
const div = ownerDocument.createElement('div');
if (__DEV__) {
if (
enableTrustedTypesIntegration &&
!didWarnScriptTags &&
// Data block scripts are not executed by UAs anyway so
// we don't need to warn:
// 数据块脚本无论如何都不会被 UA 执行,所以我们不需要警告:
// https://html.spec.whatwg.org/multipage/scripting.html#attr-script-type
!isScriptDataBlock(props)
) {
console.error(
'Encountered a script tag while rendering React component. ' +
'Scripts inside React components are never executed when rendering ' +
'on the client. Consider using template tag instead ' +
'(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
);
didWarnScriptTags = true;
}
}
div.innerHTML = '<script><' + '/script>';
// This is guaranteed to yield a script element.
// 这保证会生成一个 script 元素。
const firstChild = div.firstChild as any as HTMLScriptElement;
domElement = div.removeChild(firstChild);
break;
}
case 'select': {
if (typeof props.is === 'string') {
domElement = ownerDocument.createElement('select', {
is: props.is,
});
} else {
// Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
// 分开 else 分支,而不是在上面使用 `props.is || undefined`,因为 Firefox 的一个错误。
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
domElement = ownerDocument.createElement('select');
}
if (props.multiple) {
domElement.multiple = true;
} else if (props.size) {
// Setting a size greater than 1 causes a select to behave like `multiple=true`, where
// it is possible that no option is selected.
// 设置大于 1 的大小会使 select 表现得像 `multiple=true`,此时可能没有选项被选中。
//
// This is only necessary when a select in "single selection mode".
// 只有在单选模式下的 select 才需要这样设置。
domElement.size = props.size;
}
break;
}
default: {
if (typeof props.is === 'string') {
domElement = ownerDocument.createElement(type, { is: props.is });
} else {
// Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
// 分开 else 分支,而不是在上面使用 `props.is || undefined`,因为 Firefox 的一个错误。
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
domElement = ownerDocument.createElement(type);
}

if (__DEV__) {
if (type.indexOf('-') === -1) {
// We're not SVG/MathML and we don't have a dash, so we're not a custom element
// Even if you use `is`, these should be of known type and lower case.
// 我们不是 SVG/MathML,并且我们没有连字符,所以我们不是自定义元素
// 即使你使用 `is`,这些也应该是已知类型且小写。
if (type !== type.toLowerCase()) {
console.error(
'<%s /> is using incorrect casing. ' +
'Use PascalCase for React components, ' +
'or lowercase for HTML elements.',
type,
);
}
if (
Object.prototype.toString.call(domElement) ===
'[object HTMLUnknownElement]' &&
!hasOwnProperty.call(warnedUnknownTags, type)
) {
warnedUnknownTags[type] = true;
console.error(
'The tag <%s> is unrecognized in this browser. ' +
'If you meant to render a React component, start its name with ' +
'an uppercase letter.',
type,
);
}
}
}
}
}
}
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}

十三、克隆可变实例

export function cloneMutableInstance(
instance: Instance,
keepChildren: boolean,
): Instance {
if (__DEV__) {
// Warn for problematic
// 警告有问题的
const tagName = instance.tagName;
switch (tagName) {
case 'VIDEO':
case 'IFRAME':
if (!didWarnForClone) {
didWarnForClone = true;
// TODO: Once we have the ability to avoid cloning the root, suggest an absolutely
// positioned ViewTransition instead as the solution.
// 待办事项:一旦我们具备避免克隆根节点的能力,请改为建议使用绝对定位的
// ViewTransition 作为解决方案。
console.warn(
'startGestureTransition() required cloning a <%s> element since it exists in ' +
'both states of the gesture. This can be problematic since it will load it twice ' +
'Try removing or hiding it with <Activity mode="offscreen"> in the optimistic state.',
tagName.toLowerCase(),
);
}
break;
}
}
return instance.cloneNode(keepChildren);
}

十四、追加初始子节点

export function appendInitialChild(
parentInstance: Instance,
child: Instance | TextInstance,
): void {
// Note: This should not use moveBefore() because initial are appended while disconnected.
// 注意:这里不应该使用 moveBefore(),因为初始元素是在断开连接的情况下追加的。
parentInstance.appendChild(child);
}

十五、完成初始子元素

export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
hostContext: HostContext,
): boolean {
setInitialProperties(domElement, type, props);
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea':
return !!props.autoFocus;
case 'img':
return true;
default:
return false;
}
}

十六、完成已水化的子节点

备注
export function finalizeHydratedChildren(
domElement: Instance,
type: string,
props: Props,
hostContext: HostContext,
): boolean {
// TOOD: Consider unifying this with hydrateInstance.
// TODO:考虑将其与 hydrateInstance 统一。
if (!enableHydrationChangeEvent) {
return false;
}
switch (type) {
case 'input':
case 'select':
case 'textarea':
case 'img':
return true;
default:
return false;
}
}

十七、应该设置文本内容

export function shouldSetTextContent(type: string, props: Props): boolean {
return (
type === 'textarea' ||
type === 'noscript' ||
typeof props.children === 'string' ||
typeof props.children === 'number' ||
typeof props.children === 'bigint' ||
(typeof props.dangerouslySetInnerHTML === 'object' &&
props.dangerouslySetInnerHTML !== null &&
props.dangerouslySetInnerHTML.__html != null)
);
}

十八、创建文本实例

备注
export function createTextInstance(
text: string,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): TextInstance {
if (__DEV__) {
const hostContextDev = hostContext as any as HostContextDev;
const ancestor = hostContextDev.ancestorInfo.current;
if (ancestor != null) {
validateTextNesting(
text,
ancestor.tag,
hostContextDev.ancestorInfo.implicitRootScope,
);
}
}
const textNode: TextInstance = getOwnerDocumentFromRootContainer(
rootContainerInstance,
).createTextNode(text);
precacheFiberNode(internalInstanceHandle, textNode);
return textNode;
}

十九、克隆可变文本实例

export function cloneMutableTextInstance(
textInstance: TextInstance,
): TextInstance {
return textInstance.cloneNode(false);
}

廿、是否应尝试主动过渡

export function shouldAttemptEagerTransition(): boolean {
const event = window.event;
if (event && event.type === 'popstate') {
// This is a popstate event. Attempt to render any transition during this
// event synchronously. Unless we already attempted during this event.
// 这是一个 popstate 事件。尝试在此事件期间同步渲染任何过渡。除非我们已经在此事件期
// 间尝试过。
if (event === currentPopstateTransitionEvent) {
// We already attempted to render this popstate transition synchronously.
// Any subsequent attempts must have happened as the result of a derived
// update, like startTransition inside useEffect, or useDV. Switch back to
// the default behavior for all remaining transitions during the current
// popstate event.
// 我们已经尝试过同步渲染这个 popstate 转换。任何后续尝试都必须是在派生更新的结
// 果下发生的,比如在 useEffect 内部的 startTransition,或者 useDV。在当前
// popstate 事件期间,将所有剩余转换切换回默认行为。
return false;
} else {
// Cache the current event in case a derived transition is scheduled.
// (Refer to previous branch.)
// 缓存当前事件,以防计划了派生转换。(参考之前的分支。)
currentPopstateTransitionEvent = event;
return true;
}
}
// We're not inside a popstate event.
// 我们不在 popstate 事件内部。
currentPopstateTransitionEvent = null;
return false;
}

廿一、跟踪调度事件

export function trackSchedulerEvent(): void {
schedulerEvent = window.event;
}

廿二、解决事件类型

export function resolveEventType(): null | string {
const event = window.event;
return event && event !== schedulerEvent ? event.type : null;
}

廿三、解析事件时间戳

export function resolveEventTimeStamp(): number {
const event = window.event;
return event && event !== schedulerEvent ? event.timeStamp : -1.1;
}

廿四、准备传送门安装

备注
export function preparePortalMount(portalInstance: Instance): void {
listenToAllSupportedEvents(portalInstance);
}

廿五、准备范围更新

备注
export function prepareScopeUpdate(
scopeInstance: ReactScopeInstance,
internalInstanceHandle: Object,
): void {
if (enableScopeAPI) {
precacheFiberNode(internalInstanceHandle, scopeInstance);
}
}

廿六、从范围获取实例

备注
export function getInstanceFromScope(
scopeInstance: ReactScopeInstance,
): null | Object {
if (enableScopeAPI) {
return getFiberFromScopeInstance(scopeInstance);
}
return null;
}

廿七、提交挂载

备注
export function commitMount(
domElement: Instance,
type: string,
newProps: Props,
internalInstanceHandle: Object,
): void {
// Despite the naming that might imply otherwise, this method only
// fires if there is an `Update` effect scheduled during mounting.
// This happens if `finalizeInitialChildren` returns `true` (which it
// does to implement the `autoFocus` attribute on the client). But
// there are also other cases when this might happen (such as patching
// up text content during hydration mismatch). So we'll check this again.
// 尽管名字可能会让人有其他联想,但这个方法只有在挂载期间调度了 `Update` 效果时才会触发。
// 当 `finalizeInitialChildren` 返回 `true` 时会发生这种情况(它会在客户端实现 `autoFocus` 属性)。
// 但也有其他情况可能会发生这种情况(例如在水合不匹配期间修补文本内容)。所以我们会再次检查这一点。
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea':
if (newProps.autoFocus) {
(
domElement as any as
| HTMLButtonElement
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement
).focus();
}
return;
case 'img': {
// The technique here is to assign the src or srcSet property to cause the browser
// to issue a new load event. If it hasn't loaded yet it'll fire whenever the load actually completes.
// If it has already loaded we missed it so the second load will still be the first one that executes
// any associated onLoad props.
// Even if we have srcSet we prefer to reassign src. The reason is that Firefox does not trigger a new
// load event when only srcSet is assigned. Chrome will trigger a load event if either is assigned so we
// only need to assign one. And Safari just never triggers a new load event which means this technique
// is already a noop regardless of which properties are assigned. We should revisit if browsers update
// this heuristic in the future.
//
// 这里的技巧是分配 src 或 srcSet 属性,以便浏览器触发新的加载事件。如果它还没有
// 加载完成,当加载实际完成时就会触发事件。如果它已经加载完成,我们就错过了它,因此
// 第二次加载仍将是执行任何相关 onLoad 属性的第一次。即使我们有 srcSet,我们也更
// 倾向于重新分配 src。原因是 Firefox 在仅分配 srcSet 时不会触发新的加载事件。
// Chrome 如果分配了任意一个属性都会触发加载事件,所以我们只需要分配其中一个。
// Safari 根本不会触发新的加载事件,这意味着无论分配哪个属性,这个技巧已经无效。
// 如果未来浏览器更新了这个行为,我们应当重新考虑这个方法。
if (newProps.src) {
const src = (newProps as any).src;
if (enableSrcObject && typeof src === 'object') {
// For object src, we can't just set the src again to the same blob URL because it might have
// already revoked if it loaded before this. However, we can create a new blob URL and set that.
// This is relatively cheap since the blob is already in memory but this might cause some
// duplicated work.
// TODO: We could maybe detect if load hasn't fired yet and if so reuse the URL.
// 对于对象 src,我们不能只是再次将 src 设置为相同的 blob URL,因为它可能已
// 经被撤销,如果它之前已经加载过的话。不过,我们可以创建一个新的 blob URL
// 并设置它。
// 这相对来说开销不大,因为 blob 已经在内存中,但这可能会导致一些重复工作。
// TODO: 我们或许可以检测加载事件是否尚未触发,如果是的话可以重用 URL。
try {
setSrcObject(domElement, type, src);
return;
} catch (x) {
// If URL.createObjectURL() errors, it was probably some other object type
// that should be toString:ed instead, so we just fall-through to the normal
// path.
// 如果 URL.createObjectURL() 出错,可能是其他类型的对象
// 应该调用 toString 方法,因此我们就直接执行正常的路径。
}
}
(domElement as any as HTMLImageElement).src = src;
} else if (newProps.srcSet) {
(domElement as any as HTMLImageElement).srcset = (
newProps as any
).srcSet;
}
return;
}
}
}

廿八、提交已水化实例

备注
export function commitHydratedInstance(
domElement: Instance,
type: string,
props: Props,
internalInstanceHandle: Object,
): void {
if (!enableHydrationChangeEvent) {
return;
}
// This fires in the commit phase if a hydrated instance needs to do further
// work in the commit phase. Similar to commitMount. However, this should not
// do things that would've already happened such as set auto focus since that
// would steal focus. It's only scheduled if finalizeHydratedChildren returns
// true.
// 如果一个已水合的实例在提交阶段需要进一步工作,这个会在提交阶段触发。
// 类似于 commitMount。然而,这不应做已经完成的操作,例如设置自动聚焦,
// 因为那样会抢走焦点。只有当 finalizeHydratedChildren 返回 true 时才会调度。
switch (type) {
case 'input': {
hydrateInput(
domElement,
props.value,
props.defaultValue,
props.checked,
props.defaultChecked,
);
break;
}
case 'select': {
hydrateSelect(
domElement,
props.value,
props.defaultValue,
props.multiple,
);
break;
}
case 'textarea':
hydrateTextarea(domElement, props.value, props.defaultValue);
break;
case 'img':
// TODO: Should we replay onLoad events?
// 待办:我们是否应该重放 onLoad 事件?
break;
}
}

廿九、提交更新

备注
export function commitUpdate(
domElement: Instance,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// Diff and update the properties.
// 比较并更新属性。
updateProperties(domElement, type, oldProps, newProps);

// Update the props handle so that we know which props are the ones with
// with current event handlers.
// 更新 props 处理方法,这样我们就知道哪些 props 是当前事件处理程序对应的。
updateFiberProps(domElement, newProps);
}

卅、 重置文本内容

备注
export function resetTextContent(domElement: Instance): void {
setTextContent(domElement, '');
}

卅一、提交文本更新

export function commitTextUpdate(
textInstance: TextInstance,
oldText: string,
newText: string,
): void {
textInstance.nodeValue = newText;
}

卅二、添加子元素

export function appendChild(
parentInstance: Instance,
child: Instance | TextInstance,
): void {
if (supportsMoveBefore && child.parentNode !== null) {
parentInstance.moveBefore(child, null);
} else {
parentInstance.appendChild(child);
}
}

卅三、将子节点添加到容器

备注
export function appendChildToContainer(
container: Container,
child: Instance | TextInstance,
): void {
if (__DEV__) {
warnForReactChildrenConflict(container);
}
let parentNode: DocumentFragment | Element;
if (container.nodeType === DOCUMENT_NODE) {
parentNode = (container as any).body;
} else if (
!disableCommentsAsDOMContainers &&
container.nodeType === COMMENT_NODE
) {
parentNode = container.parentNode as any;
if (supportsMoveBefore && child.parentNode !== null) {
parentNode.moveBefore(child, container);
} else {
parentNode.insertBefore(child, container);
}
return;
} else if (container.nodeName === 'HTML') {
parentNode = container.ownerDocument.body as any;
} else {
parentNode = container as any;
}
if (supportsMoveBefore && child.parentNode !== null) {
parentNode.moveBefore(child, null);
} else {
parentNode.appendChild(child);
}

// This container might be used for a portal.
// If something inside a portal is clicked, that click should bubble
// through the React tree. However, on Mobile Safari the click would
// never bubble through the *DOM* tree unless an ancestor with onclick
// event exists. So we wouldn't see it and dispatch it.
// This is why we ensure that non React root containers have inline onclick
// defined.
// 这个容器可能用于门户(portal)。如果点击门户内的某个元素,该点击应该在 React 树中
// 冒泡。然而,在移动版 Safari 上,点击事件不会通过 *DOM* 树冒泡,除非存在带有
// onclick 事件的祖先元素。因此,我们无法看到它并触发它。这就是为什么我们要确保非
// React 根容器定义了内联的 onclick。
// https://github.com/facebook/react/issues/11918
const reactRootContainer = container._reactRootContainer;
if (
(reactRootContainer === null || reactRootContainer === undefined) &&
parentNode.onclick === null
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
// TODO:对于 SVG、MathML 或自定义元素,这种类型转换可能不安全。
trapClickOnNonInteractiveElement(parentNode as any as HTMLElement);
}
}

卅四、插入到之前

export function insertBefore(
parentInstance: Instance,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance | ActivityInstance,
): void {
if (supportsMoveBefore && child.parentNode !== null) {
parentInstance.moveBefore(child, beforeChild);
} else {
parentInstance.insertBefore(child, beforeChild);
}
}

卅五、在容器中插入到之前

备注
export function insertInContainerBefore(
container: Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance | ActivityInstance,
): void {
if (__DEV__) {
warnForReactChildrenConflict(container);
}
let parentNode: DocumentFragment | Element;
if (container.nodeType === DOCUMENT_NODE) {
parentNode = (container as any).body;
} else if (
!disableCommentsAsDOMContainers &&
container.nodeType === COMMENT_NODE
) {
parentNode = container.parentNode as any;
} else if (container.nodeName === 'HTML') {
parentNode = container.ownerDocument.body as any;
} else {
parentNode = container as any;
}
if (supportsMoveBefore && child.parentNode !== null) {
parentNode.moveBefore(child, beforeChild);
} else {
parentNode.insertBefore(child, beforeChild);
}
}

卅六、判定单例范围

export function isSingletonScope(type: string): boolean {
return type === 'head';
}

卅七、移除子节点

export function removeChild(
parentInstance: Instance,
child: Instance | TextInstance | SuspenseInstance | ActivityInstance,
): void {
parentInstance.removeChild(child);
}

卅八、从容器中移除子元素

备注
export function removeChildFromContainer(
container: Container,
child: Instance | TextInstance | SuspenseInstance | ActivityInstance,
): void {
let parentNode: DocumentFragment | Element;
if (container.nodeType === DOCUMENT_NODE) {
parentNode = (container as any).body;
} else if (
!disableCommentsAsDOMContainers &&
container.nodeType === COMMENT_NODE
) {
parentNode = container.parentNode as any;
} else if (container.nodeName === 'HTML') {
parentNode = container.ownerDocument.body as any;
} else {
parentNode = container as any;
}
parentNode.removeChild(child);
}

卅九、清除活动边界

export function clearActivityBoundary(
parentInstance: Instance,
activityInstance: ActivityInstance,
): void {
clearHydrationBoundary(parentInstance, activityInstance);
}

卌、清除悬而未决的边界

export function clearSuspenseBoundary(
parentInstance: Instance,
suspenseInstance: SuspenseInstance,
): void {
clearHydrationBoundary(parentInstance, suspenseInstance);
}

卌一、从容器清除活动边界

export function clearActivityBoundaryFromContainer(
container: Container,
activityInstance: ActivityInstance,
): void {
clearHydrationBoundaryFromContainer(container, activityInstance);
}

卌二、从容器中清除挂起边界

export function clearSuspenseBoundaryFromContainer(
container: Container,
suspenseInstance: SuspenseInstance,
): void {
clearHydrationBoundaryFromContainer(container, suspenseInstance);
}

卌三、隐藏脱水边界

export function hideDehydratedBoundary(
suspenseInstance: SuspenseInstance,
): void {
hideOrUnhideDehydratedBoundary(suspenseInstance, true);
}

卌四、隐藏实例

export function hideInstance(instance: Instance): void {
// TODO: Does this work for all element types? What about MathML? Should we
// pass host context to this method?
// 待办:这对所有元素类型都适用吗?MathML 呢?我们是否应该将宿主上下文传递给此方法?
instance = instance as any as HTMLElement;
const style = instance.style;
if (typeof style.setProperty === 'function') {
style.setProperty('display', 'none', 'important');
} else {
style.display = 'none';
}
}

卌五、隐藏文本实例

export function hideTextInstance(textInstance: TextInstance): void {
textInstance.nodeValue = '';
}

卌六、显示已脱水边界

export function unhideDehydratedBoundary(
dehydratedInstance: SuspenseInstance | ActivityInstance,
): void {
hideOrUnhideDehydratedBoundary(dehydratedInstance, false);
}

卌七、显示实例

export function unhideInstance(instance: Instance, props: Props): void {
instance = instance as any as HTMLElement;
const styleProp = props[STYLE];
const display =
styleProp !== undefined &&
styleProp !== null &&
styleProp.hasOwnProperty('display')
? styleProp.display
: null;
instance.style.display =
display == null || typeof display === 'boolean'
? ''
: // The value would've errored already if it wasn't safe.
// 如果不安全,该值早就会出错了。
('' + display).trim();
}

卌八、显示文本实例

export function unhideTextInstance(
textInstance: TextInstance,
text: string,
): void {
textInstance.nodeValue = text;
}

卌九、应用视图过渡名称

export function applyViewTransitionName(
instance: Instance,
name: string,
className: ?string,
): void {
instance = instance as any as HTMLElement;
// If the name isn't valid CSS identifier, base64 encode the name instead.
// This doesn't let you select it in custom CSS selectors but it does work in current
// browsers.
// 如果名称不是有效的 CSS 标识符,则改为使用 Base64 编码名称。
// 这不能让你在自定义 CSS 选择器中选择它,但在当前浏览器中可以正常工作。
const escapedName =
CSS.escape(name) !== name ? 'r-' + btoa(name).replace(/=/g, '') : name;
instance.style.viewTransitionName = escapedName;
if (className != null) {
instance.style.viewTransitionClass = className;
}
const computedStyle = getComputedStyle(instance);
if (computedStyle.display === 'inline') {
// WebKit has a bug where assigning a name to display: inline elements errors
// if they have display: block children. We try to work around this bug in the
// simple case by converting it automatically to display: inline-block.
// WebKit 有一个错误,当为 display: inline 的元素分配名称时,如果它们有
// display: block 的子元素就会出错。我们尝试在简单情况下通过自动将其转换为
// display: inline-block 来解决这个错误。
// https://bugs.webkit.org/show_bug.cgi?id=290923
const rects = instance.getClientRects();
if (countClientRects(rects) === 1) {
// If the instance has a single client rect, that means that it can be
// expressed as a display: inline-block or block.
// This will cause layout thrash but we live with it since inline view transitions
// are unusual.
// 如果实例只有一个客户端矩形,这意味着它可以表示为 display: inline-block 或
// block。这会导致布局抖动,但我们可以接受,因为内联视图过渡并不常见。
const style = instance.style;
// If there's literally only one rect, then it's likely on a single line like an
// inline-block. If it's multiple rects but all but one of them are empty it's
// likely because it's a single block that caused a line break.
//
// 如果只有一个矩形,那么它很可能在单行上,就像一个内联块一样。如果有多个矩形,但除
// 了一个之外其它都是空的,很可能是因为是一个块元素导致换行。
style.display = rects.length === 1 ? 'inline-block' : 'block';
// Margin doesn't apply to inline so should be zero. However, padding top/bottom
// applies to inline-block positioning which we can offset by setting the margin
// to the negative padding to get it back into original position.
// 边距不适用于内联元素,因此应为零。然而,上下内边距适用于内联块定位,我们可以通过
// 设置边距为负内边距来抵消以将其恢复到原始位置。
style.marginTop = '-' + computedStyle.paddingTop;
style.marginBottom = '-' + computedStyle.paddingBottom;
} else {
// This case cannot be easily fixed if it has blocks but it's also fine if
// it doesn't have blocks. So we only warn in DEV about this being an issue.
// 如果它有块,这种情况不容易修复,但如果没有块也没关系。所以我们只在开发环境中警告
// 这是一个问题。
warnForBlockInsideInline(instance);
}
}
}

伍零、恢复视图过渡名称

export function restoreViewTransitionName(
instance: Instance,
props: Props,
): void {
instance = instance as any as HTMLElement;
const style = instance.style;
const styleProp = props[STYLE];
const viewTransitionName =
styleProp != null
? styleProp.hasOwnProperty('viewTransitionName')
? styleProp.viewTransitionName
: styleProp.hasOwnProperty('view-transition-name')
? styleProp['view-transition-name']
: null
: null;
style.viewTransitionName =
viewTransitionName == null || typeof viewTransitionName === 'boolean'
? ''
: // The value would've errored already if it wasn't safe.
// 如果不安全,该值早就会出错了。
('' + viewTransitionName).trim();
const viewTransitionClass =
styleProp != null
? styleProp.hasOwnProperty('viewTransitionClass')
? styleProp.viewTransitionClass
: styleProp.hasOwnProperty('view-transition-class')
? styleProp['view-transition-class']
: null
: null;
style.viewTransitionClass =
viewTransitionClass == null || typeof viewTransitionClass === 'boolean'
? ''
: // The value would've errored already if it wasn't safe.
// 如果不安全,该值早就会出错了。
('' + viewTransitionClass).trim();
if (style.display === 'inline-block') {
// We might have overridden the style. Reset it to what it should be.
// 我们可能覆盖了样式。将其重置为应有的样式。
if (styleProp == null) {
style.display = style.margin = '';
} else {
const display = styleProp.display;
style.display =
display == null || typeof display === 'boolean' ? '' : display;
const margin = styleProp.margin;
if (margin != null) {
style.margin = margin;
} else {
const marginTop = styleProp.hasOwnProperty('marginTop')
? styleProp.marginTop
: styleProp['margin-top'];
style.marginTop =
marginTop == null || typeof marginTop === 'boolean' ? '' : marginTop;
const marginBottom = styleProp.hasOwnProperty('marginBottom')
? styleProp.marginBottom
: styleProp['margin-bottom'];
style.marginBottom =
marginBottom == null || typeof marginBottom === 'boolean'
? ''
: marginBottom;
}
}
}
}

伍一、取消视图过渡名称

export function cancelViewTransitionName(
instance: Instance,
oldName: string,
props: Props,
): void {
// To cancel the "new" state and paint this instance as part of the parent, all we have to do
// is remove the view-transition-name before we exit startViewTransition.
// 要取消 “new” 状态并将此实例作为父级的一部分绘制,
// 我们所需要做的只是退出 startViewTransition 之前移除 view-transition-name。
restoreViewTransitionName(instance, props);
// There isn't a way to cancel an "old" state but what we can do is hide it by animating it.
// Since it is already removed from the old state of the parent, this technique only works
// if the parent also isn't transitioning. Therefore we should only cancel the root most
// ViewTransitions.
// 没有办法取消“旧”的状态,但我们可以通过动画将其隐藏。
// 由于它已经从父组件的旧状态中移除,这种方法仅在父组件没有进行过渡时有效。
// 因此我们应该只取消最根部的 ViewTransitions。
const documentElement = instance.ownerDocument.documentElement;
if (documentElement !== null) {
documentElement.animate(
{ opacity: [0, 0], pointerEvents: ['none', 'none'] },
{
duration: 0,
fill: 'forwards',
pseudoElement: '::view-transition-group(' + oldName + ')',
},
);
}
}

伍二、取消根视图过渡名称

备注
export function cancelRootViewTransitionName(rootContainer: Container): void {
const documentElement: null | HTMLElement =
rootContainer.nodeType === DOCUMENT_NODE
? (rootContainer as any).documentElement
: rootContainer.ownerDocument.documentElement;

if (
!disableCommentsAsDOMContainers &&
rootContainer.nodeType === COMMENT_NODE
) {
if (__DEV__) {
console.warn(
'Cannot cancel root view transition on a comment node. All view transitions will be globally scoped.',
);
}
return;
}

if (
documentElement !== null &&
documentElement.style.viewTransitionName === ''
) {
documentElement.style.viewTransitionName = 'none';
documentElement.animate(
{ opacity: [0, 0], pointerEvents: ['none', 'none'] },
{
duration: 0,
fill: 'forwards',
pseudoElement: '::view-transition-group(root)',
},
);
// By default the root ::view-transition selector captures all pointer events,
// which means nothing gets interactive. We want to let whatever is not animating
// remain interactive during the transition. To do that, we set the size to nothing
// so that the transition doesn't capture any clicks. We don't set pointer-events
// on this one as that would apply to all running transitions. This lets animations
// that are running to block clicks so that they don't end up incorrectly hitting
// whatever is below the animation.
// 默认情况下,根 ::view-transition 选择器会捕获所有指针事件,
// 这意味着没有任何内容可以被交互。我们希望在过渡期间,
// 任何没有动画的内容仍然可以被交互。为此,我们将其大小设置为零,
// 这样过渡就不会捕获任何点击。我们不会在这个元素上设置 pointer-events,
// 因为那样会影响所有正在运行的过渡。这样可以让正在运行的动画
// 阻止点击,以防点击错误地作用到动画下面的内容。
documentElement.animate(
{ width: [0, 0], height: [0, 0] },
{
duration: 0,
fill: 'forwards',
pseudoElement: '::view-transition',
},
);
}
}

伍三、恢复根视图过渡名称

export function restoreRootViewTransitionName(rootContainer: Container): void {
let containerInstance: Instance;
if (rootContainer.nodeType === DOCUMENT_NODE) {
containerInstance = (rootContainer as any).body;
} else if (rootContainer.nodeName === 'HTML') {
containerInstance = rootContainer.ownerDocument.body as any;
} else {
// If the container is not the whole document, then we ideally should probably
// clone the whole document outside of the React too.
// 如果容器不是整个文档,那么我们理想情况下可能也应该在 React 外部克隆整个文档。
containerInstance = rootContainer as any;
}
if (
!disableCommentsAsDOMContainers &&
containerInstance.nodeType === COMMENT_NODE
) {
return;
}
if (containerInstance.style.viewTransitionName === 'root') {
// If we moved the root view transition name to the container in a gesture
// we need to restore it now.
// 如果我们在手势中将根视图的转换名称移动到容器,我们现在需要将其恢复。
containerInstance.style.viewTransitionName = '';
}
const documentElement: null | HTMLElement =
containerInstance.ownerDocument.documentElement;
if (
documentElement !== null &&
documentElement.style.viewTransitionName === 'none'
) {
documentElement.style.viewTransitionName = '';
}
}

伍四、克隆根视图过渡容器

备注
export function cloneRootViewTransitionContainer(
rootContainer: Container,
): Instance {
// This implies that we're not going to animate the root document but instead
// the clone so we first clear the name of the root container.
// 这意味着我们不会对根文档进行动画处理,而是对克隆进行动画处理,所以我们首先清除根容器
// 的名称。
const documentElement: null | HTMLElement =
rootContainer.nodeType === DOCUMENT_NODE
? (rootContainer as any).documentElement
: rootContainer.ownerDocument.documentElement;
if (
documentElement !== null &&
documentElement.style.viewTransitionName === ''
) {
documentElement.style.viewTransitionName = 'none';
}

let containerInstance: HTMLElement;
if (rootContainer.nodeType === DOCUMENT_NODE) {
containerInstance = (rootContainer as any).body;
} else if (rootContainer.nodeName === 'HTML') {
containerInstance = rootContainer.ownerDocument.body as any;
} else if (
!disableCommentsAsDOMContainers &&
rootContainer.nodeType === COMMENT_NODE
) {
throw new Error(
'Cannot use a startGestureTransition() with a comment node root.',
);
} else {
// If the container is not the whole document, then we ideally should probably
// clone the whole document outside of the React too.
// 如果容器不是整个文档,那么我们理想情况下可能也应该在 React 外部克隆整个文档。
containerInstance = rootContainer as any;
}

const containerParent = containerInstance.parentNode;
if (containerParent === null) {
throw new Error(
'Cannot use a startGestureTransition() on a detached root.',
);
}

const clone: HTMLElement = containerInstance.cloneNode(false);

const computedStyle = getComputedStyle(containerInstance);

if (
computedStyle.position === 'absolute' ||
computedStyle.position === 'fixed'
) {
// If the style is already absolute, we don't have to do anything because it'll appear
// in the same place.
// 如果样式已经是绝对定位,我们就不需要做任何操作,因为它会出现在同一个位置。
} else {
// Otherwise we need to absolutely position the clone in the same location as the original.
// 否则我们需要将克隆体绝对定位在与原始位置相同的位置。
let positionedAncestor: HTMLElement = containerParent;
while (
positionedAncestor.parentNode != null &&
positionedAncestor.parentNode.nodeType !== DOCUMENT_NODE
) {
if (getComputedStyle(positionedAncestor).position !== 'static') {
break;
}
positionedAncestor = positionedAncestor.parentNode;
}

const positionedAncestorStyle: any = positionedAncestor.style;
const containerInstanceStyle: any = containerInstance.style;
// Clear the transform while we're measuring since it affects the bounding client rect.
// 在测量时清除变换,因为它会影响边界矩形。
const prevAncestorTranslate = positionedAncestorStyle.translate;
const prevAncestorScale = positionedAncestorStyle.scale;
const prevAncestorRotate = positionedAncestorStyle.rotate;
const prevAncestorTransform = positionedAncestorStyle.transform;
const prevTranslate = containerInstanceStyle.translate;
const prevScale = containerInstanceStyle.scale;
const prevRotate = containerInstanceStyle.rotate;
const prevTransform = containerInstanceStyle.transform;
positionedAncestorStyle.translate = 'none';
positionedAncestorStyle.scale = 'none';
positionedAncestorStyle.rotate = 'none';
positionedAncestorStyle.transform = 'none';
containerInstanceStyle.translate = 'none';
containerInstanceStyle.scale = 'none';
containerInstanceStyle.rotate = 'none';
containerInstanceStyle.transform = 'none';

const ancestorRect = positionedAncestor.getBoundingClientRect();
const rect = containerInstance.getBoundingClientRect();

const cloneStyle = clone.style;
cloneStyle.position = 'absolute';
cloneStyle.top = rect.top - ancestorRect.top + 'px';
cloneStyle.left = rect.left - ancestorRect.left + 'px';
cloneStyle.width = rect.width + 'px';
cloneStyle.height = rect.height + 'px';
cloneStyle.margin = '0px';
cloneStyle.boxSizing = 'border-box';

positionedAncestorStyle.translate = prevAncestorTranslate;
positionedAncestorStyle.scale = prevAncestorScale;
positionedAncestorStyle.rotate = prevAncestorRotate;
positionedAncestorStyle.transform = prevAncestorTransform;
containerInstanceStyle.translate = prevTranslate;
containerInstanceStyle.scale = prevScale;
containerInstanceStyle.rotate = prevRotate;
containerInstanceStyle.transform = prevTransform;
}

// For this transition the container will act as the root. Nothing outside of it should
// be affected anyway. This lets us transition from the cloned container to the original.
// 对于此过渡,容器将作为根。外部的任何内容都不应受到影响。
// 这使我们可以从克隆的容器过渡到原始容器。
clone.style.viewTransitionName = 'root';

// Move out of the viewport so that it's still painted for the snapshot but is not visible
// for the frame where the snapshot happens.
// 移出视口,以便它仍然可以被快照绘制,但在快照发生的帧中不可见。
moveOutOfViewport(computedStyle, clone);

// Insert the clone after the root container as a sibling. This may inject a body
// as the next sibling of an existing body. document.body will still point to the
// first one and any id selectors will still find the first one. That's why it's
// important that it's after the existing node.
// 将克隆体插入到根容器之后作为兄弟节点。这可能会将一个新的 body 注入到现有的 body 之
// 后。document.body 仍然会指向第一个 body,任何 id 选择器仍会找到第一个 body。因
// 此,重要的是它必须位于现有节点之后。
containerInstance.parentNode.insertBefore(
clone,
containerInstance.nextSibling,
);

return clone;
}

伍五、移除根视图过渡克隆

备注
export function removeRootViewTransitionClone(
rootContainer: Container,
clone: Instance,
): void {
let containerInstance: Instance;
if (rootContainer.nodeType === DOCUMENT_NODE) {
containerInstance = (rootContainer as any).body;
} else if (rootContainer.nodeName === 'HTML') {
containerInstance = rootContainer.ownerDocument.body as any;
} else {
// If the container is not the whole document, then we ideally should probably
// clone the whole document outside of the React too.
// 如果容器不是整个文档,那么我们理想情况下可能也应该在 React 外部克隆整个文档。
containerInstance = rootContainer as any;
}
const containerParent = containerInstance.parentNode;
if (containerParent === null) {
throw new Error(
'Cannot use a startGestureTransition() on a detached root.',
);
}
// We assume that the clone is still within the same parent.
// 我们假设克隆仍然在同一个父元素内。
containerParent.removeChild(clone);

// Now the root is on the containerInstance itself until we call restoreRootViewTransitionName.
// 现在根视图在 containerInstance 本身上,直到我们调用
// restoreRootViewTransitionName。
containerInstance.style.viewTransitionName = 'root';
}

伍六、测量实例

export function measureInstance(instance: Instance): InstanceMeasurement {
const rect = instance.getBoundingClientRect();
const computedStyle = getComputedStyle(instance);
return createMeasurement(rect, computedStyle, instance);
}

伍七、测量克隆实例

export function measureClonedInstance(instance: Instance): InstanceMeasurement {
const measuredRect = instance.getBoundingClientRect();
// Adjust the DOMRect based on the translate that put it outside the viewport.
// TODO: This might not be completely correct if the parent also has a transform.
// 根据将 DOMRect 移出视口的位移调整它。
// 待办:如果父元素也有变换,这可能不完全正确。
const rect = new DOMRect(
measuredRect.x + 20000,
measuredRect.y + 20000,
measuredRect.width,
measuredRect.height,
);
const computedStyle = getComputedStyle(instance);
return createMeasurement(rect, computedStyle, instance);
}

伍八、曾在视口内

export function wasInstanceInViewport(
measurement: InstanceMeasurement,
): boolean {
return measurement.view;
}

伍九、实例是否已更改

export function hasInstanceChanged(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
// Note: This is not guaranteed from the same instance in the case that the Instance of the
// ViewTransition swaps out but it's still the same ViewTransition instance.
// 注意:在 ViewTransition 的实例被替换的情况下,这并不保证来自同一个实例,但它仍然是
// 相同的 ViewTransition 实例。
if (newMeasurement.clip) {
// If we're a clipping parent, we always animate if any of our children do so that we can clip
// them. This doesn't yet until browsers implement layered capture and nested view transitions.
// 如果我们是一个裁剪父级,只要我们的任何子元素有动画,我们也会进行动画,以便对它们进
// 行裁剪。 目前还不能实现,直到浏览器实现分层捕获和嵌套视图过渡。
return true;
}
const oldRect = oldMeasurement.rect;
const newRect = newMeasurement.rect;
return (
oldRect.y !== newRect.y ||
oldRect.x !== newRect.x ||
oldRect.height !== newRect.height ||
oldRect.width !== newRect.width
);
}

陸零、实例影响了父对象

export function hasInstanceAffectedParent(
oldMeasurement: InstanceMeasurement,
newMeasurement: InstanceMeasurement,
): boolean {
// Note: This is not guaranteed from the same instance in the case that the Instance of the
// ViewTransition swaps out but it's still the same ViewTransition instance.
// If the instance has resized, it might have affected the parent layout.
// 注意:在 ViewTransition 实例被替换的情况下,这并不保证来自同一个实例,但它仍然是相同的 ViewTransition 实例。
// 如果实例已经调整了大小,可能会影响父布局。
if (newMeasurement.abs) {
// Absolutely positioned elements don't affect the parent layout, unless they
// previously were not absolutely positioned.
// 绝对定位的元素不会影响父元素的布局,除非它们之前不是绝对定位的。
return !oldMeasurement.abs;
}
const oldRect = oldMeasurement.rect;
const newRect = newMeasurement.rect;
return oldRect.height !== newRect.height || oldRect.width !== newRect.width;
}

陸一、开始视图过渡

export function startViewTransition(
suspendedState: null | SuspendedState,
rootContainer: Container,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
layoutCallback: () => void,
afterMutationCallback: () => void,
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: (mixed: mixed) => void,
// 仅供性能分析用
blockedCallback: (str: string) => void, // Profiling-only
// 仅供性能分析用
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
const ownerDocument: Document =
rootContainer.nodeType === DOCUMENT_NODE
? (rootContainer as any)
: rootContainer.ownerDocument;
try {
const transition = ownerDocument.startViewTransition({
update() {
// Note: We read the existence of a pending navigation before we apply the
// mutations. That way we're not waiting on a navigation that we spawned
// from this update. Only navigations that started before this commit.
// 注意:我们在应用变更之前会先检查是否存在待处理的导航。这样做是为了避免等待由此
// 次更新触发的导航。仅处理在此次提交之前开始的导航。
const ownerWindow = ownerDocument.defaultView;
const pendingNavigation =
ownerWindow.navigation && ownerWindow.navigation.transition;
const previousFontLoadingStatus = ownerDocument.fonts.status;
mutationCallback();
const blockingPromises: Array<Promise<any>> = [];
if (previousFontLoadingStatus === 'loaded') {
// Force layout calculation to trigger font loading.
// 强制布局计算以触发字体加载。
forceLayout(ownerDocument);
if (ownerDocument.fonts.status === 'loading') {
// The mutation lead to new fonts being loaded. We should wait on them before continuing.
// This avoids waiting for potentially unrelated fonts that were already loading before.
// Either in an earlier transition or as part of a sync optimistic state. This doesn't
// include preloads that happened earlier.
// 这一变更导致加载了新的字体。我们应该在字体加载完成后再继续。
// 这样可以避免等待可能无关的字体,这些字体可能已经在之前加载。
// 可能是在早期的转换中,或者作为同步乐观状态的一部分。这不包括之前已经进行的预加载。
blockingPromises.push(ownerDocument.fonts.ready);
}
}
const blockingIndexSnapshot = blockingPromises.length;
if (suspendedState !== null) {
// Suspend on any images that still haven't loaded and are in the viewport.
// 暂停任何仍未加载且在视口内的图片。
const suspenseyImages = suspendedState.suspenseyImages;
let imgBytes = 0;
for (let i = 0; i < suspenseyImages.length; i++) {
const suspenseyImage = suspenseyImages[i];
if (!suspenseyImage.complete) {
const rect = suspenseyImage.getBoundingClientRect();
const inViewport =
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < ownerWindow.innerHeight &&
rect.left < ownerWindow.innerWidth;
if (inViewport) {
imgBytes += estimateImageBytes(suspenseyImage);
if (imgBytes > estimatedBytesWithinLimit) {
// We don't think we'll be able to download all the images within
// the timeout. Give up. Rewind to only block on fonts, if any.
// 我们认为无法在超时时间内下载所有图像。放弃。回退到仅阻塞字体,如果有的话。
blockingPromises.length = blockingIndexSnapshot;
break;
}
const loadingImage = new Promise(
waitForImageToLoad.bind(suspenseyImage),
);
blockingPromises.push(loadingImage);
}
}
}
}
if (blockingPromises.length > 0) {
if (enableProfilerTimer) {
const blockedReason =
blockingIndexSnapshot > 0
? blockingPromises.length > blockingIndexSnapshot
? 'Waiting on Fonts and Images'
: 'Waiting on Fonts'
: 'Waiting on Images';
blockedCallback(blockedReason);
}
const blockingReady = Promise.race([
Promise.all(blockingPromises),
new Promise(resolve =>
setTimeout(resolve, SUSPENSEY_FONT_AND_IMAGE_TIMEOUT),
),
]).then(layoutCallback, layoutCallback);
const allReady = pendingNavigation
? Promise.allSettled([pendingNavigation.finished, blockingReady])
: blockingReady;
return allReady.then(afterMutationCallback, afterMutationCallback);
}
layoutCallback();
if (pendingNavigation) {
return pendingNavigation.finished.then(
afterMutationCallback,
afterMutationCallback,
);
} else {
afterMutationCallback();
}
},
types: transitionTypes,
});
ownerDocument.__reactViewTransition = transition;

const viewTransitionAnimations: Array<Animation> = [];

const readyCallback = () => {
const documentElement: Element = ownerDocument.documentElement as any;
// Loop through all View Transition Animations.
// 遍历所有视图过渡动画。
const animations = documentElement.getAnimations({ subtree: true });
for (let i = 0; i < animations.length; i++) {
const animation = animations[i];
const effect: KeyframeEffect = animation.effect as any;
const pseudoElement: ?string = effect.pseudoElement;
if (
pseudoElement != null &&
pseudoElement.startsWith('::view-transition')
) {
viewTransitionAnimations.push(animation);
const keyframes = effect.getKeyframes();
// Next, we're going to try to optimize this animation in case the auto-generated
// width/height keyframes are unnecessary.
// 接下来,我们将尝试优化此动画,以防自动生成的宽度/高度关键帧不必要。
let width;
let height;
let unchangedDimensions = true;
for (let j = 0; j < keyframes.length; j++) {
const keyframe = keyframes[j];
const w = keyframe.width;
if (width === undefined) {
width = w;
} else if (width !== w) {
unchangedDimensions = false;
break;
}
const h = keyframe.height;
if (height === undefined) {
height = h;
} else if (height !== h) {
unchangedDimensions = false;
break;
}
// We're clearing the keyframes in case we are going to apply the optimization.
// 我们正在清除关键帧,以防我们要应用优化。
delete keyframe.width;
delete keyframe.height;
if (keyframe.transform === 'none') {
delete keyframe.transform;
}
}
if (
unchangedDimensions &&
width !== undefined &&
height !== undefined
) {
// Replace the keyframes with ones that don't animate the width/height.
// 将关键帧替换为不会动画化宽度/高度的关键帧。
effect.setKeyframes(keyframes);
// Read back the new animation to see what the underlying width/height of the pseudo-element was.
// 回放新的动画以查看伪元素的实际宽度/高度。
const computedStyle = getComputedStyle(
effect.target,
effect.pseudoElement,
);
if (
computedStyle.width !== width ||
computedStyle.height !== height
) {
// Oops. Turns out that the pseudo-element had a different width/height so we need to let it
// be overridden. Add it back.
// 哎呀。原来这个伪元素有不同的宽度/高度,所以我们需要允许它被覆盖。把它加回来。
const first = keyframes[0];
first.width = width;
first.height = height;
const last = keyframes[keyframes.length - 1];
last.width = width;
last.height = height;
effect.setKeyframes(keyframes);
}
}
}
}
spawnedWorkCallback();
};
const handleError = (error: mixed) => {
if (ownerDocument.__reactViewTransition === transition) {
ownerDocument.__reactViewTransition = null;
}
try {
error = customizeViewTransitionError(error, false);
if (error !== null) {
errorCallback(error);
}
} finally {
// Continue the reset of the work.
// If the error happened in the snapshot phase before the update callback
// was invoked, then we need to first finish the mutation and layout phases.
// If they're already invoked it's still safe to call them due the status check.
// 继续重置工作。
// 如果错误发生在更新回调被调用之前的快照阶段,
// 那么我们需要先完成变更和布局阶段。
// 如果它们已经被调用,由于状态检查,调用它们仍然是安全的。
mutationCallback();
layoutCallback();
// Skip afterMutationCallback() since we're not animating.
// 跳过 afterMutationCallback(),因为我们没有做动画。
spawnedWorkCallback();
if (enableProfilerTimer) {
finishedAnimation();
}
}
};
transition.ready.then(readyCallback, handleError);
transition.finished.finally(() => {
for (let i = 0; i < viewTransitionAnimations.length; i++) {
// In Safari, we need to manually cancel all manually started animations
// or it'll block or interfer with future transitions.
// We can't use getAnimations() due to #35336 so we collect them in an array.
// 在 Safari 中,我们需要手动取消所有手动启动的动画
// 否则它会阻塞或干扰未来的过渡。
// 由于 #35336 我们不能使用 getAnimations(),所以我们把它们收集到一个数组中。
viewTransitionAnimations[i].cancel();
}
if (ownerDocument.__reactViewTransition === transition) {
ownerDocument.__reactViewTransition = null;
}
if (enableProfilerTimer) {
finishedAnimation();
}
passiveCallback();
});
return transition;
} catch (x) {
// We use the error as feature detection.
// The only thing that should throw is if startViewTransition is missing
// or if it doesn't accept the object form. Other errors are async.
// I.e. it's before the View Transitions v2 spec. We only support View
// Transitions v2 otherwise we fallback to not animating to ensure that
// we're not animating with the wrong animation mapped.
// Flush remaining work synchronously.
// 我们使用错误来进行功能检测。
// 唯一应该抛出的错误是 startViewTransition 缺失
// 或者它不接受对象形式。其他错误都是异步的。
// 例如,它是在 View Transitions v2 规范之前。否则我们只支持 View
// Transitions v2,否则回退到不动画的方式,以确保
// 不会使用错误的动画映射进行动画。
// 同步刷新剩余的工作。
mutationCallback();
layoutCallback();
// Skip afterMutationCallback(). We don't need it since we're not animating.
// 跳过 afterMutationCallback()。我们不需要它,因为我们没有做动画。
if (enableProfilerTimer) {
finishedAnimation();
}
spawnedWorkCallback();
// Skip passiveCallback(). Spawned work will schedule a task.
// 跳过 passiveCallback()。生成的工作将安排一个任务。
return null;
}
}

陸二、开始手势过渡

备注
export function startGestureTransition(
suspendedState: null | SuspendedState,
rootContainer: Container,
timeline: GestureTimeline,
rangeStart: number,
rangeEnd: number,
transitionTypes: null | TransitionTypes,
mutationCallback: () => void,
animateCallback: () => void,
errorCallback: (mixed: mixed) => void,
// 仅供性能分析用
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
const ownerDocument: Document =
rootContainer.nodeType === DOCUMENT_NODE
? (rootContainer as any)
: rootContainer.ownerDocument;
try {
// Force layout before we start the Transition. This works around a bug in Safari
// if one of the clones end up being a stylesheet that isn't loaded or uncached.
// 在我们开始过渡之前强制布局。这可以解决 Safari 中的一个漏洞
// 如果其中一个克隆最终变成一个未加载或未缓存的样式表。
// https://bugs.webkit.org/show_bug.cgi?id=290146
forceLayout(ownerDocument);
const transition = ownerDocument.startViewTransition({
update: mutationCallback,
types: transitionTypes,
});
ownerDocument.__reactViewTransition = transition;
// Cleanup Animations started in a CustomTimeline
// Cleanup Animations started in a CustomTimeline
const customTimelineCleanup: Array<() => void> = [];
const viewTransitionAnimations: Array<Animation> = [];
const readyCallback = () => {
const documentElement: Element = ownerDocument.documentElement as any;
// Loop through all View Transition Animations.
// 遍历所有视图过渡动画。
const animations = documentElement.getAnimations({ subtree: true });
// First do a pass to collect all known group and new items so we can look
// up if they exist later.
// 首先进行一次遍历,收集所有已知组和新项目,这样我们以后可以查找它们是否存在。
const foundGroups: Set<string> = new Set();
const foundNews: Set<string> = new Set();
// Collect the longest duration of any view-transition animation including delay.
// 收集任何视图切换动画的最长持续时间,包括延迟。
let longestDuration = 0;
for (let i = 0; i < animations.length; i++) {
const effect: KeyframeEffect = animations[i].effect as any;
const pseudoElement: ?string = effect.pseudoElement;
if (pseudoElement == null) {
} else if (
pseudoElement.startsWith('::view-transition') &&
effect.target === documentElement
) {
// TODO: ++++ 为什么原文找不到下面这行代码了,我也不能瞎加呀。。。
// viewTransitionAnimations.push(animations[i]);
const timing = effect.getTiming();
const duration =
typeof timing.duration === 'number' ? timing.duration : 0;
// TODO: Consider interation count higher than 1.
// 待办:考虑迭代次数大于 1。
const durationWithDelay = timing.delay + duration;
if (durationWithDelay > longestDuration) {
longestDuration = durationWithDelay;
}
if (pseudoElement.startsWith('::view-transition-group')) {
foundGroups.add(pseudoElement.slice(23));
} else if (pseudoElement.startsWith('::view-transition-new')) {
// TODO: This is not really a sufficient detection because if the new
// pseudo element might exist but have animations disabled on it.
// TODO: 这实际上并不是一个足够的检测,因为新伪元素可能存在,但动画被禁用了。
foundNews.add(pseudoElement.slice(21));
}
}
}
const durationToRangeMultipler =
(rangeEnd - rangeStart) / longestDuration;
for (let i = 0; i < animations.length; i++) {
const anim = animations[i];
if (anim.playState !== 'running') {
continue;
}
const effect: KeyframeEffect = anim.effect as any;
const pseudoElement: ?string = effect.pseudoElement;
if (
pseudoElement != null &&
pseudoElement.startsWith('::view-transition') &&
effect.target === documentElement
) {
// Ideally we could mutate the existing animation but unfortunately
// the mutable APIs seem less tested and therefore are lacking or buggy.
// Therefore we create a new animation instead.
// 理想情况下我们可以修改现有动画,但不幸的是。可变的 API 似乎测试较少,因此
// 缺少或存在错误。因此我们改为创建一个新动画。
anim.cancel();
let isGeneratedGroupAnim = false;
let isExitGroupAnim = false;
if (pseudoElement.startsWith('::view-transition-group')) {
const groupName = pseudoElement.slice(23);
if (foundNews.has(groupName)) {
// If this has both "new" and "old" state we expect this to be an auto-generated
// animation that started outside the viewport. We need to adjust this first frame
// to be inside the viewport.
// 如果这同时有“新”和“旧”状态,我们预计这是一个在视口外开始的自动生成动
// 画。我们需要将这一帧调整到视口内。
const animationName: ?string = anim.animationName;
isGeneratedGroupAnim =
animationName != null &&
animationName.startsWith('-ua-view-transition-group-anim-');
} else {
// If this has only an "old" state then the pseudo element will be outside
// the viewport. If any keyframes don't override "transform" we need to
// adjust them.
// 如果这只有一个“旧”的状态,那么伪元素将位于视口之外。
// 如果任何关键帧没有覆盖“transform”,我们需要对它们进行调整。
isExitGroupAnim = true;
}
// TODO: If this has only an old state and no new state,
// TODO:如果这只包含旧状态而没有新状态,
}
// Adjust the range based on how long the animation would've ran as time based.
// Since we're running animations in reverse from how they normally would run,
// therefore the timing is from the rangeEnd to the start.
// 根据动画本应运行的时间长度来调整范围。由于我们是从动画正常运行的反向来播放
// 动画,所以时间是从 rangeEnd 到开始。
const timing = effect.getTiming();
const duration =
typeof timing.duration === 'number' ? timing.duration : 0;
let adjustedRangeStart =
rangeEnd - (duration + timing.delay) * durationToRangeMultipler;
let adjustedRangeEnd =
rangeEnd - timing.delay * durationToRangeMultipler;
if (
timing.direction === 'reverse' ||
timing.direction === 'alternate-reverse'
) {
// This animation was originally in reverse so we have to play it in flipped range.
// 这个动画最初是反向的,所以我们必须在翻转的范围内播放它。
const temp = adjustedRangeStart;
adjustedRangeStart = adjustedRangeEnd;
adjustedRangeEnd = temp;
}
animateGesture(
effect.getKeyframes(),
effect.target,
pseudoElement,
timeline,
viewTransitionAnimations,
customTimelineCleanup,
adjustedRangeStart,
adjustedRangeEnd,
isGeneratedGroupAnim,
isExitGroupAnim,
);
if (pseudoElement.startsWith('::view-transition-old')) {
const groupName = pseudoElement.slice(21);
if (!foundGroups.has(groupName) && !foundNews.has(groupName)) {
foundGroups.add(groupName);
// We haven't seen any group animation with this name. Since the old
// state was outside the viewport we need to put it back. Since we
// can't programmatically target the element itself, we use an
// animation to adjust it.
// This usually happens for exit animations where the element has
// the old position.
// If we also have a "new" state then we skip this because it means
// someone manually disabled the auto-generated animation. We need to
// treat the old state as having the position of the "new" state which
// will happen by default.
// 我们没有看到任何具有此名称的组动画。由于旧状态位于视口之外,因此我们需
// 要将其放回去。由于我们无法以编程方式直接定位元素本身,所以我们使用动画
// 来调整它。这种情况通常发生在元素具有旧位置的退出动画中。如果我们还有一
// 个“新”状态,则会跳过此步骤,因为这意味着有人手动禁用了自动生成的动画。
// 我们需要将旧状态视为具有“新”状态的位置,这将会默认发生。
const pseudoElementName = '::view-transition-group' + groupName;
animateGesture(
[{}, {}],
effect.target,
pseudoElementName,
timeline,
viewTransitionAnimations,
customTimelineCleanup,
rangeStart,
rangeEnd,
false,
// 我们让助手来进行翻译
true, // We let the helper apply the translate
);
}
}
}
}
// View Transitions with ScrollTimeline has a quirk where they end if the
// ScrollTimeline ever reaches 100% but that doesn't mean we're done because
// you can swipe back again. We can prevent this by adding a paused Animation
// that never stops. This seems to keep all running Animations alive until
// we explicitly abort (or something forces the View Transition to cancel).
// 使用 ScrollTimeline 的视图过渡有一个特殊情况,当 ScrollTimeline 达到
// 100% 时,过渡会结束,但这并不意味着我们完成了,因为你可以再次向回滑动。我们可以
// 通过添加一个从不停止的暂停动画来防止这种情况。这似乎可以让所有运行中的动画保持活
// 跃,直到我们明确中止(或者某些情况强制取消视图过渡)。
const blockingAnim = documentElement.animate([{}, {}], {
pseudoElement: '::view-transition',
duration: 1,
});
blockingAnim.pause();
viewTransitionAnimations.push(blockingAnim);
animateCallback();
};
// In Chrome, "new" animations are not ready in the ready callback. We have to wait
// until requestAnimationFrame before we can observe them through getAnimations().
// However, in Safari, that would cause a flicker because we're applying them late.
// TODO: Think of a feature detection for this instead.
// 在 Chrome 中,“新”动画在 ready 回调中还未准备好。我们必须等到
// requestAnimationFrame 才能通过 getAnimations() 观察它们。然而,在 Safari
// 中,这会导致闪烁,因为我们应用它们得太晚。
// TODO: 考虑为此想一个特性检测方法。
const readyForAnimations =
navigator.userAgent.indexOf('Chrome') !== -1
? () => requestAnimationFrame(readyCallback)
: readyCallback;
const handleError = (error: mixed) => {
if (ownerDocument.__reactViewTransition === transition) {
ownerDocument.__reactViewTransition = null;
}
try {
error = customizeViewTransitionError(error, true);
if (error !== null) {
errorCallback(error);
}
} finally {
// Continue the reset of the work.
// If the error happened in the snapshot phase before the update callback
// was invoked, then we need to first finish the mutation and layout phases.
// If they're already invoked it's still safe to call them due the status check.
// 继续重置工作。如果错误发生在更新回调被调用之前的快照阶段,那么我们需要先完成变
// 更和布局阶段。如果它们已经被调用,由于状态检查,调用它们仍然是安全的。
mutationCallback();
// Skip readyCallback() and go straight to animateCallbck() since we're not animating.
// animateCallback() is still required to restore states.
// 跳过 readyCallback(),直接进入 animateCallback(),因为我们没有进行动
// 画。仍然需要 animateCallback() 来恢复状态。
animateCallback();
if (enableProfilerTimer) {
finishedAnimation();
}
}
};
transition.ready.then(readyForAnimations, handleError);
transition.finished.finally(() => {
for (let i = 0; i < viewTransitionAnimations.length; i++) {
// In Safari, we need to manually cancel all manually started animations
// or it'll block or interfer with future transitions.
// We can't use getAnimations() due to #35336 so we collect them in an array.
// 在 Safari 中,我们需要手动取消所有手动启动的动画。否则它会阻塞或干扰未来的过
// 渡。由于 #35336 我们不能使用 getAnimations(),所以我们把它们收集到一个数
// 组中。
viewTransitionAnimations[i].cancel();
}
for (let i = 0; i < customTimelineCleanup.length; i++) {
const cleanup = customTimelineCleanup[i];
cleanup();
}
if (ownerDocument.__reactViewTransition === transition) {
ownerDocument.__reactViewTransition = null;
}
if (enableProfilerTimer) {
// Signal that the Transition was unable to continue. We do that here
// instead of when we stop the running View Transition to ensure that
// we cover cases when something else stops it early.
// 表示转换无法继续。我们在这里处理这一情况。而不是在停止正在运行的视图转换时处
// 理,以确保我们覆盖了其他原因导致它提前停止的情况。
finishedAnimation();
}
});
return transition;
} catch (x) {
// We use the error as feature detection.
// The only thing that should throw is if startViewTransition is missing
// or if it doesn't accept the object form. Other errors are async.
// I.e. it's before the View Transitions v2 spec. We only support View
// Transitions v2 otherwise we fallback to not animating to ensure that
// we're not animating with the wrong animation mapped.
// Run through the sequence to put state back into a consistent state.
// 我们使用错误来进行功能检测。
// 唯一应该抛出的错误是 startViewTransition 缺失
// 或者它不接受对象形式。其他错误都是异步的。
// 即,它发生在 View Transitions v2 规范之前。我们只支持 View
// Transitions v2,否则我们会回退为不执行动画,以确保
// 不会使用错误的动画映射进行动画。
// 运行整个序列以将状态恢复到一致状态。
mutationCallback();
animateCallback();
if (enableProfilerTimer) {
finishedAnimation();
}
return null;
}
}

陸三、停止视图过渡

export function stopViewTransition(transition: RunningViewTransition) {
transition.skipTransition();
}

陸四、添加视图过渡完成监听器

export function addViewTransitionFinishedListener(
transition: RunningViewTransition,
callback: () => void,
) {
transition.finished.finally(callback);
}

陸五、创建视图过渡实例

export function createViewTransitionInstance(
name: string,
): ViewTransitionInstance {
return {
name: name,
group: new (ViewTransitionPseudoElement as any)('group', name),
imagePair: new (ViewTransitionPseudoElement as any)('image-pair', name),
old: new (ViewTransitionPseudoElement as any)('old', name),
new: new (ViewTransitionPseudoElement as any)('new', name),
};
}

陸六、获取当前手势偏移

export function getCurrentGestureOffset(timeline: GestureTimeline): number {
const time = timeline.currentTime;
if (time === null) {
throw new Error(
'Cannot start a gesture with a disconnected AnimationTimeline.',
);
}
return typeof time === 'number' ? time : time.value;
}

陸七、创建片段实例

备注
export function createFragmentInstance(
fragmentFiber: Fiber,
): FragmentInstanceType {
const fragmentInstance = new (FragmentInstance as any)(fragmentFiber);
if (enableFragmentRefsInstanceHandles) {
traverseFragmentInstance(
fragmentFiber,
addFragmentHandleToFiber,
fragmentInstance,
);
}
return fragmentInstance;
}

陸八、更新片段实例纤维

export function updateFragmentInstanceFiber(
fragmentFiber: Fiber,
instance: FragmentInstanceType,
): void {
instance._fragmentFiber = fragmentFiber;
}

陸九、将新子项提交到片段实例

备注
export function commitNewChildToFragmentInstance(
childInstance: InstanceWithFragmentHandles | Text,
fragmentInstance: FragmentInstanceType,
): void {
if (childInstance.nodeType === TEXT_NODE) {
return;
}
const instance: InstanceWithFragmentHandles = childInstance as any;
const eventListeners = fragmentInstance._eventListeners;
if (eventListeners !== null) {
for (let i = 0; i < eventListeners.length; i++) {
const { type, listener, optionsOrUseCapture } = eventListeners[i];
instance.addEventListener(type, listener, optionsOrUseCapture);
}
}
if (fragmentInstance._observers !== null) {
fragmentInstance._observers.forEach(observer => {
observer.observe(instance);
});
}
if (enableFragmentRefsInstanceHandles) {
addFragmentHandleToInstance(instance, fragmentInstance);
}
}

㰟零、从片段实例中删除子项

备注
export function deleteChildFromFragmentInstance(
childInstance: InstanceWithFragmentHandles | Text,
fragmentInstance: FragmentInstanceType,
): void {
if (childInstance.nodeType === TEXT_NODE) {
return;
}
const instance: InstanceWithFragmentHandles = childInstance as any;
const eventListeners = fragmentInstance._eventListeners;
if (eventListeners !== null) {
for (let i = 0; i < eventListeners.length; i++) {
const { type, listener, optionsOrUseCapture } = eventListeners[i];
instance.removeEventListener(type, listener, optionsOrUseCapture);
}
}
if (enableFragmentRefsInstanceHandles) {
if (instance.reactFragments != null) {
instance.reactFragments.delete(fragmentInstance);
}
}
}

㰟一、清空容器

export function clearContainer(container: Container): void {
const nodeType = container.nodeType;
if (nodeType === DOCUMENT_NODE) {
clearContainerSparingly(container);
} else if (nodeType === ELEMENT_NODE) {
switch (container.nodeName) {
case 'HEAD':
case 'HTML':
case 'BODY':
clearContainerSparingly(container);
return;
default: {
container.textContent = '';
}
}
}
}

㰟二、绑定实例

备注
// Making this so we can eventually move all of the instance caching to the commit phase.
// Currently this is only used to associate fiber and props to instances for hydrating
// HostSingletons. The reason we need it here is we only want to make this binding on commit
// because only one fiber can own the instance at a time and render can fail/restart
// 这样做是为了我们最终能够将所有实例缓存移动到提交阶段。
// 目前,这只用于将 fiber 和 props 与实例关联,以便为 HostSingletons 进行 hydration。
// 我们之所以需要在这里这样做,是因为我们只想在提交阶段进行这种绑定,
// 因为一次只能有一个 fiber 拥有该实例,并且渲染可能会失败或重新启动
export function bindInstance(
instance: Instance,
props: Props,
internalInstanceHandle: mixed,
) {
precacheFiberNode(internalInstanceHandle as any, instance);
updateFiberProps(instance, props);
}

㰟三、可以水合实例

备注
export function canHydrateInstance(
instance: HydratableInstance,
type: string,
props: Props,
inRootOrSingleton: boolean,
): null | Instance {
while (instance.nodeType === ELEMENT_NODE) {
const element: Element = instance as any;
const anyProps = props as any;
if (element.nodeName.toLowerCase() !== type.toLowerCase()) {
if (!inRootOrSingleton) {
// Usually we error for mismatched tags.
// 通常我们会因为标签不匹配而出错。
if (
element.nodeName === 'INPUT' &&
(element as any).type === 'hidden'
) {
// If we have extra hidden inputs, we don't mismatch. This allows us to embed
// extra form data in the original form.
// 如果我们有额外的隐藏输入,就不会出现不匹配。这允许我们在原始表单中嵌入额外的表单数据。
} else {
return null;
}
}
// In root or singleton parents we skip past mismatched instances.
// 在根节点或单例父节点中,我们会跳过不匹配的实例。
} else if (!inRootOrSingleton) {
// Match
// 匹配
if (type === 'input' && (element as any).type === 'hidden') {
if (__DEV__) {
checkAttributeStringCoercion(anyProps.name, 'name');
}
const name = anyProps.name == null ? null : '' + anyProps.name;
if (
anyProps.type !== 'hidden' ||
element.getAttribute('name') !== name
) {
// Skip past hidden inputs unless that's what we're looking for. This allows us
// embed extra form data in the original form.
// 跳过隐藏的输入,除非我们正好需要它们。这允许我们在原始表单中嵌入额外的表单
// 数据。
} else {
return element;
}
} else {
return element;
}
} else if (isMarkedHoistable(element)) {
// We've already claimed this as a hoistable which isn't hydrated this way so we skip past it.
// 我们已经将其声明为可提升的,但它不会以这种方式被水化,所以我们跳过它。
} else {
// We have an Element with the right type.

// We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
// we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
// using high entropy attributes for certain types. This technique will fail for strange insertions like
// extension prepending <div> in the <body> but that already breaks before and that is an edge case.
// 我们有一个类型正确的元素。

// 如果我们能够明确地将其识别为提升的节点,或者
// 我们可以推测该节点很可能是被提升的,或者是由第三方脚本或浏览器扩展
// 使用高熵属性插入的特定类型的节点,我们将尝试将其排除。对于奇怪的插入情况,
// 例如扩展在<body>中添加<div>,这种方法会失败,但这种情况之前就已经会出错,这是一个极端例子。
switch (type) {
// case 'title':
//We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
// and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop.
// case 'title':
// 我们假设所有标题都是可以匹配的。在文档中你应该只有一个标题,至少在可提升的
// 作用域内是这样。如果你是一个 type 为 title 的 HostComponent,我们必须
// 要么处在 <svg> 环境中,要么这个标题必须有一个 `itemProp` 属性。
case 'meta': {
// The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
// not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
// are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
// implications are minimal
// 唯一避免提升 meta 标签的方法是为其提供 itemprop 属性。我们假设不会有被预
// 置的第三方 meta 标签,并接受这种情况不成立的情况,因为 meta 标签通常只对
// SSR 有作用,因此即使在罕见情况下我们确实绑定到注入的标签,运行时的影响也是
// 最小的
if (!element.hasAttribute('itemprop')) {
// This is a Hoistable
// 这是一个可提升的
break;
}
return element;
}
case 'link': {
// Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
// and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
// matches.
// 链接有多种形式,我们确实期望第三方将它们注入到 <head> / <body> 中。我们
// 排除已知资源,然后使用高熵属性,比如 href,这些属性几乎总是使用并且几乎总
// 是唯一的,以过滤掉不太可能的匹配。
const rel = element.getAttribute('rel');
if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
// This is a stylesheet resource
// 这是一个样式表资源
break;
} else if (
rel !== anyProps.rel ||
element.getAttribute('href') !==
(anyProps.href == null || anyProps.href === ''
? null
: anyProps.href) ||
element.getAttribute('crossorigin') !==
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
element.getAttribute('title') !==
(anyProps.title == null ? null : anyProps.title)
) {
// rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
// and title could vary for rel alternate
// rel href 通常足以唯一标识一个链接,但对于 rel preconnect,
// crossOrigin 可能不同;
// 对于 rel alternate,title 可能会不同
break;
}
return element;
}
case 'style': {
// Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
// in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles
// 样式很难正确匹配。我们可以排除已知资源,但除此之外,我们要接受这样一个事
// 实:非提升的样式标签
// 在<head>或<body>中,由于它们在文档中的位置以及它们可能包含全局样式的事实,
// 很可能永远不会被卸载
if (element.hasAttribute('data-precedence')) {
// This is a style resource
// 这是一个样式资源
break;
}
return element;
}
case 'script': {
// Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
// to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
// in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
// Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
// if we learn it is problematic
// 脚本有点棘手,我们排除已知资源,然后类似处理链接,尝试使用高熵属性来拒绝不
// 匹配的项。
// 脚本的一个挑战是内联脚本。我们不会尝试检查文本内容,这理论上可能在第三方在
// React 渲染节点之前注入内联脚本的情况下,导致后续的 hydration 错误。
// 如果发生这种情况,退回到客户端渲染应该是无缝的,因此我们会尝试这个启发式方
// 法,如果发现有问题,会在以后重新评估。
const srcAttr = element.getAttribute('src');
if (
srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
element.getAttribute('type') !==
(anyProps.type == null ? null : anyProps.type) ||
element.getAttribute('crossorigin') !==
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
) {
// This script is for a different src/type/crossOrigin. It may be a script resource
// or it may just be a mistmatch
// 这个脚本用于不同的 src/type/crossOrigin。它可能是一个脚本资源,也可能
// 只是一个不匹配
if (
srcAttr &&
element.hasAttribute('async') &&
!element.hasAttribute('itemprop')
) {
// This is an async script resource
// 这是一个异步脚本资源
break;
}
}
return element;
}
default: {
// We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
// and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
// that should work in the vast majority of cases.
// 我们已经排除了可提升标签、第三方脚本插入的标签以及浏览器扩展插入的标签之间
// 最可能的不匹配情况。虽然这有可能不是完全正确的匹配,但这是一个相当合理的启
// 发式方法,应适用于绝大多数情况。
return element;
}
}
}
const nextInstance = getNextHydratableSibling(element);
if (nextInstance === null) {
break;
}
instance = nextInstance;
}
// This is a suspense boundary or Text node or we got the end.
// Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
// and this is a hydration error.
// Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
// but it seems reasonable and conservative to reject this as a hydration error as well
// 这是一个 Suspense 边界或文本节点,或者我们已经到达了末尾。
// Suspense 边界从不应由第三方注入。如果我们看到一个,它应该被匹配
// 否则就是一个水合错误。
// 文本节点也不应由第三方注入。这对于 <body> 来说保证性较低,
// 但将其作为水合错误拒绝似乎是合理且谨慎的做法
return null;
}

㰟四、可以水化文本实例

export function canHydrateTextInstance(
instance: HydratableInstance,
text: string,
inRootOrSingleton: boolean,
): null | TextInstance {
// Empty strings are not parsed by HTML so there won't be a correct match here.
// 空字符串不会被 HTML 解析,所以这里不会有正确的匹配。
if (text === '') return null;

while (instance.nodeType !== TEXT_NODE) {
if (
instance.nodeType === ELEMENT_NODE &&
instance.nodeName === 'INPUT' &&
(instance as any).type === 'hidden'
) {
// If we have extra hidden inputs, we don't mismatch. This allows us to
// embed extra form data in the original form.
// 如果我们有额外的隐藏输入,就不会出现不匹配。这允许我们
// 在原始表单中嵌入额外的表单数据。
} else if (!inRootOrSingleton) {
return null;
}
const nextInstance = getNextHydratableSibling(instance);
if (nextInstance === null) {
return null;
}
instance = nextInstance;
}
// This has now been refined to a text node.
// 现在已经被精炼为一个文本节点。
return instance as any as TextInstance;
}

㰟五、可以水化活动实例

export function canHydrateActivityInstance(
instance: HydratableInstance,
inRootOrSingleton: boolean,
): null | ActivityInstance {
const hydratableInstance = canHydrateHydrationBoundary(
instance,
inRootOrSingleton,
);
if (
hydratableInstance !== null &&
hydratableInstance.data === ACTIVITY_START_DATA
) {
return hydratableInstance as any;
}
return null;
}

㰟六、可以水化挂起实例

export function canHydrateSuspenseInstance(
instance: HydratableInstance,
inRootOrSingleton: boolean,
): null | SuspenseInstance {
const hydratableInstance = canHydrateHydrationBoundary(
instance,
inRootOrSingleton,
);
if (
hydratableInstance !== null &&
hydratableInstance.data !== ACTIVITY_START_DATA
) {
return hydratableInstance as any;
}
return null;
}

㰟七、判定挂起的实例是否未处理

export function isSuspenseInstancePending(instance: SuspenseInstance): boolean {
return (
instance.data === SUSPENSE_PENDING_START_DATA ||
instance.data === SUSPENSE_QUEUED_START_DATA
);
}

㰟八、判定挂起的实例是否回退

export function isSuspenseInstanceFallback(
instance: SuspenseInstance,
): boolean {
return (
instance.data === SUSPENSE_FALLBACK_START_DATA ||
(instance.data === SUSPENSE_PENDING_START_DATA &&
instance.ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING)
);
}

㰟九、获取挂起实例回退错误详情

export function getSuspenseInstanceFallbackErrorDetails(
instance: SuspenseInstance,
): {
digest: ?string;
message?: string;
stack?: string;
componentStack?: string;
} {
const dataset =
instance.nextSibling &&
(instance.nextSibling as any as HTMLElement).dataset;
let digest, message, stack, componentStack;
if (dataset) {
digest = dataset.dgst;
if (__DEV__) {
message = dataset.msg;
stack = dataset.stck;
componentStack = dataset.cstck;
}
}
if (__DEV__) {
return {
message,
digest,
stack,
componentStack,
};
} else {
// Object gets DCE'd if constructed in tail position and matches callsite destructuring
// 如果对象在尾部位置构造并且与调用点的解构匹配,则会被 DCE(死代码消除)。
return {
digest,
};
}
}

仈零、注册挂起实例重试

export function registerSuspenseInstanceRetry(
instance: SuspenseInstance,
callback: () => void,
) {
const ownerDocument = instance.ownerDocument;
if (instance.data === SUSPENSE_QUEUED_START_DATA) {
// The Fizz runtime has already queued this boundary for reveal. We wait for it
// to be revealed and then retries.
// Fizz 运行时已经将此边界排队以进行显示。我们等待它被显示,然后再重试。
instance._reactRetry = callback;
} else if (
// The Fizz runtime must have put this boundary into client render or complete
// state after the render finished but before it committed. We need to call the
// callback now rather than wait
// Fizz 运行时必须在渲染完成但提交之前,将此边界放入客户端渲染或完成状态。
// 我们现在需要调用回调,而不是等待
instance.data !== SUSPENSE_PENDING_START_DATA ||
// The boundary is still in pending status but the document has finished loading
// before we could register the event handler that would have scheduled the retry
// on load so we call teh callback now.
// 边界仍处于待定状态,但文档已完成加。在我们能够注册会调度重试的事件处理程序之前
// 所以我们现在调用回调。
ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING
) {
callback();
} else {
// We're still in pending status and the document is still loading so we attach
// a listener to the document load even and expose the retry on the instance for
// the Fizz runtime to trigger if it ends up resolving this boundary
// 我们仍处于挂起状态,文档仍在加载,因此我们为文档加载事件附加了一个监听器,并在实例
// 上公开了重试,以便 Fizz 运行时在最终解析此边界时触发
const listener = () => {
callback();
ownerDocument.removeEventListener('DOMContentLoaded', listener);
};
ownerDocument.addEventListener('DOMContentLoaded', listener);
instance._reactRetry = listener;
}
}

仈一、可以水化表单状态标记

export function canHydrateFormStateMarker(
instance: HydratableInstance,
inRootOrSingleton: boolean,
): null | FormStateMarkerInstance {
while (instance.nodeType !== COMMENT_NODE) {
if (!inRootOrSingleton) {
return null;
}
const nextInstance = getNextHydratableSibling(instance);
if (nextInstance === null) {
return null;
}
instance = nextInstance;
}
const nodeData = (instance as any).data;
if (
nodeData === FORM_STATE_IS_MATCHING ||
nodeData === FORM_STATE_IS_NOT_MATCHING
) {
const markerInstance: FormStateMarkerInstance = instance as any;
return markerInstance;
}
return null;
}

仈二、判定表单状态是否匹配标识

export function isFormStateMarkerMatching(
markerInstance: FormStateMarkerInstance,
): boolean {
return markerInstance.data === FORM_STATE_IS_MATCHING;
}

仈三、获取下一个可水化的兄弟节点

export function getNextHydratableSibling(
instance: HydratableInstance,
): null | HydratableInstance {
return getNextHydratable(instance.nextSibling);
}

仈四、获取第一个可水化子元素

export function getFirstHydratableChild(
parentInstance: Instance,
): null | HydratableInstance {
return getNextHydratable(parentInstance.firstChild);
}

仈五、获取容器内的第一个可水化子元素

export function getFirstHydratableChildWithinContainer(
parentContainer: Container,
): null | HydratableInstance {
let parentElement: Element;
switch (parentContainer.nodeType) {
case DOCUMENT_NODE:
parentElement = (parentContainer as any).body;
break;
default: {
if (parentContainer.nodeName === 'HTML') {
parentElement = (parentContainer as any).ownerDocument.body;
} else {
parentElement = parentContainer as any;
}
}
}
return getNextHydratable(parentElement.firstChild);
}

仈六、获取活动实例中的第一个可水化子元素

export function getFirstHydratableChildWithinActivityInstance(
parentInstance: ActivityInstance,
): null | HydratableInstance {
return getNextHydratable(parentInstance.nextSibling);
}

仈七、获取悬挂实例内的第一个可水合子节点

export function getFirstHydratableChildWithinSuspenseInstance(
parentInstance: SuspenseInstance,
): null | HydratableInstance {
return getNextHydratable(parentInstance.nextSibling);
}

仈八、获取单例内的首个可水化子元素

export function getFirstHydratableChildWithinSingleton(
type: string,
singletonInstance: Instance,
currentHydratableInstance: null | HydratableInstance,
): null | HydratableInstance {
if (isSingletonScope(type)) {
previousHydratableOnEnteringScopedSingleton = currentHydratableInstance;
return getNextHydratable(singletonInstance.firstChild);
} else {
return currentHydratableInstance;
}
}

仈九、获取单例后的下一个可水合兄弟元素

export function getNextHydratableSiblingAfterSingleton(
type: string,
currentHydratableInstance: null | HydratableInstance,
): null | HydratableInstance {
if (isSingletonScope(type)) {
const previousHydratableInstance =
previousHydratableOnEnteringScopedSingleton;
previousHydratableOnEnteringScopedSingleton = null;
return previousHydratableInstance;
} else {
return currentHydratableInstance;
}
}

玖零、为开发警告描述可水合实例

备注

export function describeHydratableInstanceForDevWarnings(
instance: HydratableInstance,
): string | {type: string, props: $ReadOnly<Props>} {
// Reverse engineer a pseudo react-element from hydratable instance
// 从可水化实例反向工程伪 React 元素
if (instance.nodeType === ELEMENT_NODE) {
// Reverse engineer a set of props that can print for dev warnings
// 反向工程一组可以打印开发警告的属性
return {
type: instance.nodeName.toLowerCase(),
props: getPropsFromElement((instance: any)),
};
} else if (instance.nodeType === COMMENT_NODE) {
if (instance.data === ACTIVITY_START_DATA) {
return {
type: 'Activity',
props: {},
};
}
return {
type: 'Suspense',
props: {},
};
} else {
return instance.nodeValue;
}
}

玖一、验证可水化实例

备注
export function validateHydratableInstance(
type: string,
props: Props,
hostContext: HostContext,
): boolean {
if (__DEV__) {
// TODO: take namespace into account when validating.
// 待办:在验证时考虑命名空间。
const hostContextDev: HostContextDev = hostContext as any;
return validateDOMNesting(type, hostContextDev.ancestorInfo);
}
return true;
}

玖二、水化实例

备注
export function hydrateInstance(
instance: Instance,
type: string,
props: Props,
hostContext: HostContext,
internalInstanceHandle: Object,
): boolean {
precacheFiberNode(internalInstanceHandle, instance);
// TODO: Possibly defer this until the commit phase where all the events
// get attached.
// 待办:可能需要推迟到提交阶段,在所有事件都被附加时再处理。
updateFiberProps(instance, props);

return hydrateProperties(instance, type, props, hostContext);
}

玖三、用于开发警告的差异化水合属性

备注
// Returns a Map of properties that were different on the server.
// 返回一个包含服务器上不同属性的映射。
export function diffHydratedPropsForDevWarnings(
instance: Instance,
type: string,
props: Props,
hostContext: HostContext,
): null | $ReadOnly<Props> {
return diffHydratedProperties(instance, type, props, hostContext);
}

玖四、验证可水合文本实例

备注
export function validateHydratableTextInstance(
text: string,
hostContext: HostContext,
): boolean {
if (__DEV__) {
const hostContextDev = hostContext as any as HostContextDev;
const ancestor = hostContextDev.ancestorInfo.current;
if (ancestor != null) {
return validateTextNesting(
text,
ancestor.tag,
hostContextDev.ancestorInfo.implicitRootScope,
);
}
}
return true;
}

玖五、填充文本实例

备注
export function hydrateTextInstance(
textInstance: TextInstance,
text: string,
internalInstanceHandle: Object,
parentInstanceProps: null | Props,
): boolean {
precacheFiberNode(internalInstanceHandle, textInstance);

return hydrateText(textInstance, text, parentInstanceProps);
}

玖六、用于开发警告的差异化水合文本

备注
// Returns the server text if it differs from the client.
// 如果服务器文本与客户端不同,则返回服务器文本。
export function diffHydratedTextForDevWarnings(
textInstance: TextInstance,
text: string,
parentProps: null | Props,
): null | string {
if (
parentProps === null ||
parentProps[SUPPRESS_HYDRATION_WARNING] !== true
) {
return diffHydratedText(textInstance, text);
}
return null;
}

玖七、填充活动实例

备注
export function hydrateActivityInstance(
activityInstance: ActivityInstance,
internalInstanceHandle: Object,
) {
precacheFiberNode(internalInstanceHandle, activityInstance);
}

玖八、水合挂起实例

备注
export function hydrateSuspenseInstance(
suspenseInstance: SuspenseInstance,
internalInstanceHandle: Object,
) {
precacheFiberNode(internalInstanceHandle, suspenseInstance);
}

玖九、获取活动实例后的下一个可实例化对象

export function getNextHydratableInstanceAfterActivityInstance(
activityInstance: ActivityInstance,
): null | HydratableInstance {
return getNextHydratableInstanceAfterHydrationBoundary(activityInstance);
}

幺零零、获取悬挂实例后的下一个可水化实例

export function getNextHydratableInstanceAfterSuspenseInstance(
suspenseInstance: SuspenseInstance,
): null | HydratableInstance {
return getNextHydratableInstanceAfterHydrationBoundary(suspenseInstance);
}

幺零一、获取父级水合边界

// Returns the SuspenseInstance if this node is a direct child of a
// SuspenseInstance. I.e. if its previous sibling is a Comment with
// SUSPENSE_x_START_DATA. Otherwise, null.
// 如果该节点是 SuspenseInstance 的直接子节点,则返回 SuspenseInstance。
// 即如果它的前一个兄弟节点是带有 SUSPENSE_x_START_DATA 的注释。
// 否则,返回 null。
export function getParentHydrationBoundary(
targetInstance: Node,
): null | SuspenseInstance | ActivityInstance {
let node = targetInstance.previousSibling;
// Skip past all nodes within this suspense boundary.
// There might be nested nodes so we need to keep track of how
// deep we are and only break out when we're back on top.
// 跳过此 suspense 边界内的所有节点。
// 可能存在嵌套节点,因此我们需要跟踪我们的深度
// 只有当我们回到最顶层时才停止。
let depth = 0;
while (node) {
if (node.nodeType === COMMENT_NODE) {
const data = (node as any).data as string;
if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === ACTIVITY_START_DATA
) {
if (depth === 0) {
return node as any as SuspenseInstance | ActivityInstance;
} else {
depth--;
}
} else if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
depth++;
}
}
node = node.previousSibling;
}
return null;
}

幺零二、提交已水化的容器

备注
export function commitHydratedContainer(container: Container): void {
// Retry if any event replaying was blocked on this.
// 如果任何事件重放被阻止,则重试。
retryIfBlockedOn(container);
}

幺零三、提交已水合的活动实例

备注
export function commitHydratedActivityInstance(
activityInstance: ActivityInstance,
): void {
// Retry if any event replaying was blocked on this.
// 如果任何事件重放被阻止,则重试。
retryIfBlockedOn(activityInstance);
}

幺零四、提交已水合的悬挂实例

备注
export function commitHydratedSuspenseInstance(
suspenseInstance: SuspenseInstance,
): void {
// Retry if any event replaying was blocked on this.
// 如果任何事件重放被阻止,则重试。
retryIfBlockedOn(suspenseInstance);
}

幺零五、刷新水合事件

备注
export function flushHydrationEvents(): void {
if (enableHydrationChangeEvent) {
flushEventReplaying();
}
}

幺零六、是否应删除未初始化的尾部实例

export function shouldDeleteUnhydratedTailInstances(
parentType: string,
): boolean {
return parentType !== 'form' && parentType !== 'button';
}

幺零七、查找Fiber根

备注
export function findFiberRoot(node: Instance): null | FiberRoot {
const stack = [node];
let index = 0;
while (index < stack.length) {
const current = stack[index++];
if (isContainerMarkedAsRoot(current)) {
return getInstanceFromNodeDOMTree(current) as any as FiberRoot;
}
stack.push(...current.children);
}
return null;
}

幺零八、获取边界矩形

export function getBoundingRect(node: Instance): BoundingRect {
const rect = node.getBoundingClientRect();
return {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
};
}

幺零九、匹配可访问性角色

备注
export function matchAccessibilityRole(node: Instance, role: string): boolean {
if (hasRole(node, role)) {
return true;
}

return false;
}

幺幺零、获取文本内容

export function getTextContent(fiber: Fiber): string | null {
switch (fiber.tag) {
case HostHoistable:
case HostSingleton:
case HostComponent:
let textContent = '';
const childNodes = fiber.stateNode.childNodes;
for (let i = 0; i < childNodes.length; i++) {
const childNode = childNodes[i];
if (childNode.nodeType === Node.TEXT_NODE) {
textContent += childNode.textContent;
}
}
return textContent;
case HostText:
return fiber.stateNode.textContent;
}

return null;
}

幺幺一、是否隐藏子树

备注
export function isHiddenSubtree(fiber: Fiber): boolean {
return fiber.tag === HostComponent && fiber.memoizedProps.hidden === true;
}

幺幺二、如果可聚焦则设置焦点

export function setFocusIfFocusable(
node: Instance,
focusOptions?: FocusOptions,
): boolean {
// The logic for determining if an element is focusable is kind of complex,
// and since we want to actually change focus anyway- we can just skip it.
// Instead we'll just listen for a "focus" event to verify that focus was set.
//
// We could compare the node to document.activeElement after focus,
// but this would not handle the case where application code managed focus to automatically blur.
// 判断一个元素是否可聚焦的逻辑有点复杂,
// 既然我们无论如何都想实际更改焦点——我们可以直接跳过。
// 我们可以只监听“focus”事件来验证焦点是否被设置。

// 我们可以在焦点设置后,将节点与 document.activeElement 进行比较,
// 但这不能处理应用程序代码管理焦点以自动失焦的情况。
const element = node as any as HTMLElement;

// If this element is already the active element, it's focusable and already
// focused. Calling .focus() on it would be a no-op (no focus event fires),
// so we short-circuit here.
// 如果这个元素已经是活动元素,它是可聚焦的并且已经被聚焦。对它调用 .focus() 将不会有任何作用
// (不会触发聚焦事件),所以我们在这里直接终止。
if (element.ownerDocument.activeElement === element) {
return true;
}
let didFocus = false;
const handleFocus = () => {
didFocus = true;
};

try {
// Listen on the document in the capture phase so we detect focus even when
// it lands on a different element than the one we called .focus() on. This
// happens with <label> elements (focus delegates to the associated input)
// and shadow hosts with delegatesFocus.
// 在捕获阶段监听文档,以便即使焦点落在与我们调用 .focus() 的元素不同的元素上,也能检测到焦
// 点。这种情况发生在 <label> 元素(焦点委托给关联的输入)和具有 delegatesFocus 的
// shadow host 上。
element.ownerDocument.addEventListener('focus', handleFocus, true);
(element.focus || HTMLElement.prototype.focus).call(element, focusOptions);
} finally {
element.ownerDocument.removeEventListener('focus', handleFocus, true);
}

return didFocus;
}

幺幺三、设置交叉观察器

export function setupIntersectionObserver(
targets: Array<Instance>,
callback: ObserveVisibleRectsCallback,
options?: IntersectionObserverOptions,
): {
disconnect: () => void;
observe: (instance: Instance) => void;
unobserve: (instance: Instance) => void;
} {
const rectRatioCache: Map<Instance, RectRatio> = new Map();
targets.forEach(target => {
rectRatioCache.set(target, {
rect: getBoundingRect(target),
ratio: 0,
});
});

const handleIntersection = (entries: Array<IntersectionObserverEntry>) => {
entries.forEach(entry => {
const { boundingClientRect, intersectionRatio, target } = entry;
rectRatioCache.set(target, {
rect: {
x: boundingClientRect.left,
y: boundingClientRect.top,
width: boundingClientRect.width,
height: boundingClientRect.height,
},
ratio: intersectionRatio,
});
});

callback(Array.from(rectRatioCache.values()));
};

const observer = new IntersectionObserver(handleIntersection, options);
targets.forEach(target => {
observer.observe(target as any);
});

return {
disconnect: () => observer.disconnect(),
observe: target => {
rectRatioCache.set(target, {
rect: getBoundingRect(target),
ratio: 0,
});
observer.observe(target as any);
},
unobserve: target => {
rectRatioCache.delete(target);
observer.unobserve(target as any);
},
};
}

幺幺四、请求绘制回调

export function requestPostPaintCallback(callback: (time: number) => void) {
localRequestAnimationFrame(() => {
localRequestAnimationFrame(time => callback(time));
});
}

幺幺五、是否为宿主单例类型

export function isHostSingletonType(type: string): boolean {
return type === 'html' || type === 'head' || type === 'body';
}

幺幺六、解析单例实例

备注
export function resolveSingletonInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
validateDOMNestingDev: boolean,
): Instance {
if (__DEV__) {
const hostContextDev = hostContext as any as HostContextDev;
if (validateDOMNestingDev) {
validateDOMNesting(type, hostContextDev.ancestorInfo);
}
}
const ownerDocument = getOwnerDocumentFromRootContainer(
rootContainerInstance,
);
switch (type) {
case 'html': {
const documentElement = ownerDocument.documentElement;
if (!documentElement) {
throw new Error(
'React expected an <html> element (document.documentElement) to exist in the Document but one was' +
' not found. React never removes the documentElement for any Document it renders into so' +
' the cause is likely in some other script running on this page.',
);
}
return documentElement;
}
case 'head': {
const head = ownerDocument.head;
if (!head) {
throw new Error(
'React expected a <head> element (document.head) to exist in the Document but one was' +
' not found. React never removes the head for any Document it renders into so' +
' the cause is likely in some other script running on this page.',
);
}
return head;
}
case 'body': {
const body = ownerDocument.body;
if (!body) {
throw new Error(
'React expected a <body> element (document.body) to exist in the Document but one was' +
' not found. React never removes the body for any Document it renders into so' +
' the cause is likely in some other script running on this page.',
);
}
return body;
}
default: {
throw new Error(
'resolveSingletonInstance was called with an element type that is not supported. This is a bug in React.',
);
}
}
}

幺幺七、获取单例实例

备注
export function acquireSingletonInstance(
type: string,
props: Props,
instance: Instance,
internalInstanceHandle: Object,
): void {
if (__DEV__) {
if (
// If this instance is the container then it is invalid to acquire it as a singleton however
// the DOM nesting validation will already warn for this and the message below isn't semantically
// aligned with the actual fix you need to make so we omit the warning in this case
// 如果这个实例是容器,那么将其作为单例获取就是无效的,然而 DOM 嵌套验证已经会对此
// 发出警告,并且下面的消息在语义上与你实际需要做的修复不一致,所以在这种情况下我们
// 省略警告
!isContainerMarkedAsRoot(instance) &&
// If this instance isn't the root but is currently owned by a different HostSingleton instance then
// we we need to warn that you are rendering more than one singleton at a time.
// 如果这个实例不是根实例,但当前归属于不同的 HostSingleton 实例,则我们需要警告
// 你同时渲染了多个单例。
getInstanceFromNodeDOMTree(instance)
) {
const tagName = instance.tagName.toLowerCase();
console.error(
'You are mounting a new %s component when a previous one has not first unmounted. It is an' +
' error to render more than one %s component at a time and attributes and children of these' +
' components will likely fail in unpredictable ways. Please only render a single instance of' +
' <%s> and if you need to mount a new one, ensure any previous ones have unmounted first.',
tagName,
tagName,
tagName,
);
}
switch (type) {
case 'html':
case 'head':
case 'body': {
break;
}
default: {
console.error(
'acquireSingletonInstance was called with an element type that is not supported. This is a bug in React.',
);
}
}
}
const attributes = instance.attributes;
while (attributes.length) {
instance.removeAttributeNode(attributes[0]);
}

setInitialProperties(instance, type, props);
precacheFiberNode(internalInstanceHandle, instance);
updateFiberProps(instance, props);
}

幺幺八、释放单例实例

备注
export function releaseSingletonInstance(instance: Instance): void {
const attributes = instance.attributes;
while (attributes.length) {
instance.removeAttributeNode(attributes[0]);
}
detachDeletedInstance(instance);
}

幺幺九、准备提交可提升项

export function prepareToCommitHoistables() {
tagCaches = null;
}

幺贰零、可提升根

// getRootNode is missing from IE and old jsdom versions
// getRootNode 在 IE 和旧版本 jsdom 中缺失
export function getHoistableRoot(container: Container): HoistableRoot {
return typeof container.getRootNode === 'function'
? (container.getRootNode() as Document | ShadowRoot)
: container.nodeType === DOCUMENT_NODE
? (container as Document)
: container.ownerDocument;
}

幺贰一、获取资源

备注
// This function is called in begin work and we should always have a currentDocument set
// 这个函数在开始工作时被调用,我们应该始终设置一个 currentDocument
export function getResource(
type: string,
currentProps: any,
pendingProps: any,
currentResource: null | Resource,
): null | Resource {
const resourceRoot = getCurrentResourceRoot();
if (!resourceRoot) {
throw new Error(
'"resourceRoot" was expected to exist. This is a bug in React.',
);
}
switch (type) {
case 'meta':
case 'title': {
return null;
}
case 'style': {
if (
typeof pendingProps.precedence === 'string' &&
typeof pendingProps.href === 'string'
) {
const key = getStyleKey(pendingProps.href);
const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
let resource = styles.get(key);
if (!resource) {
resource = {
type: 'style',
instance: null,
count: 0,
state: null,
};
styles.set(key, resource);
}
return resource;
}
return {
type: 'void',
instance: null,
count: 0,
state: null,
};
}
case 'link': {
if (
pendingProps.rel === 'stylesheet' &&
typeof pendingProps.href === 'string' &&
typeof pendingProps.precedence === 'string'
) {
const qualifiedProps: StylesheetQualifyingProps = pendingProps;
const key = getStyleKey(qualifiedProps.href);

const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;

let resource = styles.get(key);
if (!resource) {
// We asserted this above but Flow can't figure out that the type satisfies
// 我们在上面已经断言过这一点,但 Flow 无法确定该类型是否满足
const ownerDocument = getDocumentFromRoot(resourceRoot);
resource = {
type: 'stylesheet',
instance: null,
count: 0,
state: {
loading: NotLoaded,
preload: null,
},
} as StylesheetResource;
styles.set(key, resource);
const instance = ownerDocument.querySelector(
getStylesheetSelectorFromKey(key),
);
if (instance) {
const loadingState: ?Promise<mixed> = (instance as any)._p;
if (loadingState) {
// This instance is inserted as part of a boundary reveal and is not yet
// loaded
// 这个实例作为边界显示的一部分被插入,但尚未加载
} else {
// This instance is already loaded
// 此实例已加载
resource.instance = instance;
resource.state.loading = Loaded | Inserted;
}
}

if (!preloadPropsMap.has(key)) {
const preloadProps = preloadPropsFromStylesheet(qualifiedProps);
preloadPropsMap.set(key, preloadProps);
if (!instance) {
preloadStylesheet(
ownerDocument,
key,
preloadProps,
resource.state,
);
}
}
}
if (currentProps && currentResource === null) {
// This node was previously an Instance type and is becoming a Resource type
// For now we error because we don't support flavor changes
// 这个节点以前是实例类型,现在正在变为资源类型
// 目前我们报错,因为不支持类型变更
let diff = '';
if (__DEV__) {
diff = `

- ${describeLinkForResourceErrorDEV(currentProps)}
+ ${describeLinkForResourceErrorDEV(pendingProps)}`;
}
throw new Error(
'Expected <link> not to update to be updated to a stylesheet with precedence.' +
' Check the `rel`, `href`, and `precedence` props of this component.' +
' Alternatively, check whether two different <link> components render in the same slot or share the same key.' +
diff,
);
}
return resource;
} else {
if (currentProps && currentResource !== null) {
// This node was previously a Resource type and is becoming an Instance type
// For now we error because we don't support flavor changes
// 这个节点以前是资源类型,现在正在变为实例类型
// 目前我们报错,因为不支持类型变更
let diff = '';
if (__DEV__) {
diff = `

- ${describeLinkForResourceErrorDEV(currentProps)}
+ ${describeLinkForResourceErrorDEV(pendingProps)}`;
}
throw new Error(
'Expected stylesheet with precedence to not be updated to a different kind of <link>.' +
' Check the `rel`, `href`, and `precedence` props of this component.' +
' Alternatively, check whether two different <link> components render in the same slot or share the same key.' +
diff,
);
}
return null;
}
}
case 'script': {
const async = pendingProps.async;
const src = pendingProps.src;
if (
typeof src === 'string' &&
async &&
typeof async !== 'function' &&
typeof async !== 'symbol'
) {
const key = getScriptKey(src);
const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;

let resource = scripts.get(key);
if (!resource) {
resource = {
type: 'script',
instance: null,
count: 0,
state: null,
};
scripts.set(key, resource);
}
return resource;
}
return {
type: 'void',
instance: null,
count: 0,
state: null,
};
}
default: {
throw new Error(
`getResource encountered a type it did not expect: "${type}". this is a bug in React.`,
);
}
}
}

幺贰二、获取资源

备注
export function acquireResource(
hoistableRoot: HoistableRoot,
resource: Resource,
props: any,
): null | Instance {
resource.count++;
if (resource.instance === null) {
switch (resource.type) {
case 'style': {
const qualifiedProps: StyleTagQualifyingProps = props;

// Attempt to hydrate instance from DOM
// 尝试从 DOM 中挂载实例
let instance: null | Instance = hoistableRoot.querySelector(
getStyleTagSelector(qualifiedProps.href),
);
if (instance) {
resource.instance = instance;
markNodeAsHoistable(instance);
return instance;
}

const styleProps = styleTagPropsFromRawProps(props);
const ownerDocument = getDocumentFromRoot(hoistableRoot);
instance = ownerDocument.createElement('style');

markNodeAsHoistable(instance);
setInitialProperties(instance, 'style', styleProps);

// TODO: `style` does not have loading state for tracking insertions. I
// guess because these aren't suspensey? Not sure whether this is a
// factoring smell.
// resource.state.loading |= Inserted;
// TODO: `style` 没有用于跟踪插入的加载状态。
// 我猜是因为这些不是 suspense 风格的?不确定这是否是一种结构问题。
// resource.state.loading |= Inserted;
insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
resource.instance = instance;

return instance;
}
case 'stylesheet': {
// This typing is enforce by `getResource`. If we change the logic
// there for what qualifies as a stylesheet resource we need to ensure
// this cast still makes sense;
// 这个类型是由 `getResource` 强制的。如果我们改变了那里定义样式表资源的逻辑,
// 我们需要确保这个类型转换仍然合理;
const qualifiedProps: StylesheetQualifyingProps = props;
const key = getStyleKey(qualifiedProps.href);

// Attempt to hydrate instance from DOM
// 尝试从 DOM 中挂载实例
let instance: null | Instance = hoistableRoot.querySelector(
getStylesheetSelectorFromKey(key),
);
if (instance) {
resource.state.loading |= Inserted;
resource.instance = instance;
markNodeAsHoistable(instance);
return instance;
}

const stylesheetProps = stylesheetPropsFromRawProps(props);
const preloadProps = preloadPropsMap.get(key);
if (preloadProps) {
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
}

// Construct and insert a new instance
// 构建并插入一个新实例
const ownerDocument = getDocumentFromRoot(hoistableRoot);
instance = ownerDocument.createElement('link');
markNodeAsHoistable(instance);
const linkInstance: HTMLLinkElement = instance as any;
(linkInstance as any)._p = new Promise((resolve, reject) => {
linkInstance.onload = resolve;
linkInstance.onerror = reject;
});
setInitialProperties(instance, 'link', stylesheetProps);
resource.state.loading |= Inserted;
insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
resource.instance = instance;

return instance;
}
case 'script': {
// This typing is enforce by `getResource`. If we change the logic
// there for what qualifies as a stylesheet resource we need to ensure
// this cast still makes sense;
// 这个类型是由 `getResource` 强制的。如果我们改变了那里定义样式表资源的逻辑,
// 我们需要确保这个类型转换仍然合理;
const borrowedScriptProps: ScriptProps = props;
const key = getScriptKey(borrowedScriptProps.src);

// Attempt to hydrate instance from DOM
// 尝试从 DOM 中挂载实例
let instance: null | Instance = hoistableRoot.querySelector(
getScriptSelectorFromKey(key),
);
if (instance) {
resource.instance = instance;
markNodeAsHoistable(instance);
return instance;
}

let scriptProps = borrowedScriptProps;
const preloadProps = preloadPropsMap.get(key);
if (preloadProps) {
scriptProps = { ...borrowedScriptProps };
adoptPreloadPropsForScript(scriptProps, preloadProps);
}

// Construct and insert a new instance
// 构建并插入一个新实例
const ownerDocument = getDocumentFromRoot(hoistableRoot);
instance = ownerDocument.createElement('script');
markNodeAsHoistable(instance);
setInitialProperties(instance, 'link', scriptProps);
(ownerDocument.head as any).appendChild(instance);
resource.instance = instance;

return instance;
}
case 'void': {
return null;
}
default: {
throw new Error(
`acquireResource encountered a resource type it did not expect: "${resource.type}". this is a bug in React.`,
);
}
}
} else {
// In the case of stylesheets, they might have already been assigned an
// instance during `suspendResource`. But that doesn't mean they were
// inserted, because the commit might have been interrupted. So we need to
// check now.
//
// The other resource types are unaffected because they are not
// yet suspensey.
//
// TODO: This is a bit of a code smell. Consider refactoring how
// `suspendResource` and `acquireResource` work together. The idea is that
// `suspendResource` does all the same stuff as `acquireResource` except
// for the insertion.
// 对于样式表,它们可能已经在 `suspendResource` 期间被分配了一个实例。
// 但这并不意味着它们已经被插入,因为提交可能被中断。
// 所以我们现在需要检查。
//
// 其他资源类型不受影响,因为它们尚未处于挂起状态。
//
// 待办事项:这有点像代码异味。考虑重构 `suspendResource` 和
// `acquireResource` 的协作方式。其思路是
// `suspendResource` 做的事情与 `acquireResource` 相同,
// 只是除了插入操作。
if (
resource.type === 'stylesheet' &&
(resource.state.loading & Inserted) === NotLoaded
) {
const qualifiedProps: StylesheetQualifyingProps = props;
const instance: Instance = resource.instance;
resource.state.loading |= Inserted;
insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
}
}
return resource.instance;
}

幺贰三、释放资源

export function releaseResource(resource: Resource): void {
resource.count--;
}

幺贰四、可升降水化

备注
export function hydrateHoistable(
hoistableRoot: HoistableRoot,
type: HoistableTagType,
props: any,
internalInstanceHandle: Object,
): Instance {
const ownerDocument = getDocumentFromRoot(hoistableRoot);

let instance: ?Instance = null;
getInstance: switch (type) {
case 'title': {
instance = ownerDocument.getElementsByTagName('title')[0];
if (
!instance ||
isOwnedInstance(instance) ||
instance.namespaceURI === SVG_NAMESPACE ||
instance.hasAttribute('itemprop')
) {
instance = ownerDocument.createElement(type);
(ownerDocument.head as any).insertBefore(
instance,
ownerDocument.querySelector('head > title'),
);
}
setInitialProperties(instance, type, props);
precacheFiberNode(internalInstanceHandle, instance);
markNodeAsHoistable(instance);
return instance;
}
case 'link': {
const cache = getHydratableHoistableCache('link', 'href', ownerDocument);
const key = type + (props.href || '');
const maybeNodes = cache.get(key);
if (maybeNodes) {
const nodes = maybeNodes;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (
node.getAttribute('href') !==
(props.href == null || props.href === '' ? null : props.href) ||
node.getAttribute('rel') !==
(props.rel == null ? null : props.rel) ||
node.getAttribute('title') !==
(props.title == null ? null : props.title) ||
node.getAttribute('crossorigin') !==
(props.crossOrigin == null ? null : props.crossOrigin)
) {
// mismatch, try the next node;
// 不匹配,尝试下一个节点;
continue;
}
instance = node;
nodes.splice(i, 1);
break getInstance;
}
}
instance = ownerDocument.createElement(type);
setInitialProperties(instance, type, props);
(ownerDocument.head as any).appendChild(instance);
break;
}
case 'meta': {
const cache = getHydratableHoistableCache(
'meta',
'content',
ownerDocument,
);
const key = type + (props.content || '');
const maybeNodes = cache.get(key);
if (maybeNodes) {
const nodes = maybeNodes;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];

// We coerce content to string because it is the most likely one to
// use a `toString` capable value. For the rest we just do identity match
// passing non-strings here is not really valid anyway.
// 我们将内容强制转换为字符串,因为它最有可能是一个可以使用 `toString` 的值。对于其他情况,我们只是进行
// 身份匹配。无论如何,传递非字符串值在这里并不真正有效。
if (__DEV__) {
checkAttributeStringCoercion(props.content, 'content');
}
if (
node.getAttribute('content') !==
(props.content == null ? null : '' + props.content) ||
node.getAttribute('name') !==
(props.name == null ? null : props.name) ||
node.getAttribute('property') !==
(props.property == null ? null : props.property) ||
node.getAttribute('http-equiv') !==
(props.httpEquiv == null ? null : props.httpEquiv) ||
node.getAttribute('charset') !==
(props.charSet == null ? null : props.charSet)
) {
// mismatch, try the next node;
// 不匹配,尝试下一个节点;
continue;
}
instance = node;
nodes.splice(i, 1);
break getInstance;
}
}
instance = ownerDocument.createElement(type);
setInitialProperties(instance, type, props);
(ownerDocument.head as any).appendChild(instance);
break;
}
default:
throw new Error(
`getNodesForType encountered a type it did not expect: "${type}". This is a bug in React.`,
);
}

// This node is a match
// 这个节点是匹配
precacheFiberNode(internalInstanceHandle, instance);
markNodeAsHoistable(instance);
return instance;
}

幺贰五、可升降安装

export function mountHoistable(
hoistableRoot: HoistableRoot,
type: HoistableTagType,
instance: Instance,
): void {
const ownerDocument = getDocumentFromRoot(hoistableRoot);
(ownerDocument.head as any).insertBefore(
instance,
type === 'title' ? ownerDocument.querySelector('head > title') : null,
);
}

幺贰六、卸载可提升组件

export function unmountHoistable(instance: Instance): void {
(instance.parentNode as any).removeChild(instance);
}

幺贰七、是否为宿主可提升类型

备注
export function isHostHoistableType(
type: string,
props: RawProps,
hostContext: HostContext,
): boolean {
let outsideHostContainerContext: boolean;
let hostContextProd: HostContextProd;
if (__DEV__) {
const hostContextDev: HostContextDev = hostContext as any;
// We can only render resources when we are not within the host container context
// 只有在不处于宿主容器上下文中时,我们才能呈现资源
outsideHostContainerContext =
!hostContextDev.ancestorInfo.containerTagInScope;
hostContextProd = hostContextDev.context;
} else {
hostContextProd = hostContext as any;
}

// Global opt out of hoisting for anything in SVG Namespace or anything with an itemProp inside an itemScope
// 全局取消对 SVG 命名空间中任何内容或在 itemScope 内具有 itemProp 的任何内容的提升
if (hostContextProd === HostContextNamespaceSvg || props.itemProp != null) {
if (__DEV__) {
if (
outsideHostContainerContext &&
props.itemProp != null &&
(type === 'meta' ||
type === 'title' ||
type === 'style' ||
type === 'link' ||
type === 'script')
) {
console.error(
'Cannot render a <%s> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an' +
' `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <%s> remove the `itemProp` prop.' +
' Otherwise, try moving this tag into the <head> or <body> of the Document.',
type,
type,
);
}
}
return false;
}

switch (type) {
case 'meta':
case 'title': {
return true;
}
case 'style': {
if (
typeof props.precedence !== 'string' ||
typeof props.href !== 'string' ||
props.href === ''
) {
if (__DEV__) {
if (outsideHostContainerContext) {
console.error(
'Cannot render a <style> outside the main document without knowing its precedence and a unique href key.' +
' React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that' +
' does not conflict with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. ' +
' Note that hoisting <style> tags is considered an advanced feature that most will not use directly.' +
' Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`.',
);
}
}
return false;
}
return true;
}
case 'link': {
if (
typeof props.rel !== 'string' ||
typeof props.href !== 'string' ||
props.href === '' ||
props.onLoad ||
props.onError
) {
if (__DEV__) {
if (
props.rel === 'stylesheet' &&
typeof props.precedence === 'string'
) {
validateLinkPropsForStyleResource(props);
}
if (outsideHostContainerContext) {
if (
typeof props.rel !== 'string' ||
typeof props.href !== 'string' ||
props.href === ''
) {
console.error(
'Cannot render a <link> outside the main document without a `rel` and `href` prop.' +
' Try adding a `rel` and/or `href` prop to this <link> or moving the link into the <head> tag',
);
} else if (props.onError || props.onLoad) {
console.error(
'Cannot render a <link> with onLoad or onError listeners outside the main document.' +
' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
' somewhere in the <body>.',
);
}
}
}
return false;
}
switch (props.rel) {
case 'stylesheet': {
const { precedence, disabled } = props;
if (__DEV__) {
if (typeof precedence !== 'string') {
if (outsideHostContainerContext) {
console.error(
'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence.' +
' Consider adding precedence="default" or moving it into the root <head> tag.',
);
}
}
}
return typeof precedence === 'string' && disabled == null;
}
default: {
return true;
}
}
}
case 'script': {
const isAsync =
props.async &&
typeof props.async !== 'function' &&
typeof props.async !== 'symbol';
if (
!isAsync ||
props.onLoad ||
props.onError ||
!props.src ||
typeof props.src !== 'string'
) {
if (__DEV__) {
if (outsideHostContainerContext) {
if (!isAsync) {
console.error(
'Cannot render a sync or defer <script> outside the main document without knowing its order.' +
' Try adding async="" or moving it into the root <head> tag.',
);
} else if (props.onLoad || props.onError) {
console.error(
'Cannot render a <script> with onLoad or onError listeners outside the main document.' +
' Try removing onLoad={...} and onError={...} or moving it into the root <head> tag or' +
' somewhere in the <body>.',
);
} else {
console.error(
'Cannot render a <script> outside the main document without `async={true}` and a non-empty `src` prop.' +
' Ensure there is a valid `src` and either make the script async or move it into the root <head> tag or' +
' somewhere in the <body>.',
);
}
}
}
return false;
}
return true;
}
case 'noscript':
case 'template': {
if (__DEV__) {
if (outsideHostContainerContext) {
console.error(
'Cannot render <%s> outside the main document. Try moving it into the root <head> tag.',
type,
);
}
}
return false;
}
}
return false;
}

幺贰八、可能暂停提交

export function maySuspendCommit(type: Type, props: Props): boolean {
if (!enableSuspenseyImages && !enableViewTransition) {
return false;
}
// Suspensey images are the default, unless you opt-out of with either
// loading="lazy" or onLoad={...} which implies you're ok waiting.
// 挂起风格的图片是默认设置,除非你选择使用 loading="lazy" 或 onLoad={...} 来取消,这意味着你可以接受等待。
return (
type === 'img' &&
props.src != null &&
props.src !== '' &&
props.onLoad == null &&
props.loading !== 'lazy'
);
}

幺贰九、可能在更新时暂停提交

export function maySuspendCommitOnUpdate(
type: Type,
oldProps: Props,
newProps: Props,
): boolean {
return (
maySuspendCommit(type, newProps) &&
(newProps.src !== oldProps.src || newProps.srcSet !== oldProps.srcSet)
);
}

幺叁零、可能在同步渲染中挂起提交

export function maySuspendCommitInSyncRender(
type: Type,
props: Props,
): boolean {
// TODO: Allow sync lanes to suspend too with an opt-in.
// TODO: 允许同步通道也可以挂起,并提供可选功能。
return false;
}

幺叁一、可能暂停资源提交

export function mayResourceSuspendCommit(resource: Resource): boolean {
return (
resource.type === 'stylesheet' &&
(resource.state.loading & Inserted) === NotLoaded
);
}

幺叁二、预加载实例

export function preloadInstance(
instance: Instance,
type: Type,
props: Props,
): boolean {
// We don't need to preload Suspensey images because the browser will
// load them early once we set the src.
// If we return true here, we'll still get a suspendInstance call in the
// pre-commit phase to determine if we still need to decode the image or
// if was dropped from cache. This just avoids rendering Suspense fallback.
// 我们不需要预加载 Suspensey 图像,因为一旦设置了 src,浏览器会提前加载它们。
// 如果我们在这里返回 true,我们仍然会在预提交阶段收到 suspendInstance 调用,以确定我们
// 是否仍然需要解码图像,或者它是否已从缓存中移除。这只是为了避免渲染 Suspense 的回退内容。
return !!(instance as any).complete;
}

幺叁三、预加载资源

export function preloadResource(resource: Resource): boolean {
if (
resource.type === 'stylesheet' &&
(resource.state.loading & Settled) === NotLoaded
) {
// Return false to indicate this resource should suspend
// 返回 false 表示该资源应暂停
return false;
}

// Return true to indicate this resource should not suspend
// 返回 true 表示该资源不应被挂起
return true;
}

幺叁四、开始挂起提交

export function startSuspendingCommit(): SuspendedState {
return {
stylesheets: null,
count: 0,
imgCount: 0,
imgBytes: 0,
suspenseyImages: [],
waitingForImages: true,
waitingForViewTransition: false,
// We use a noop function when we begin suspending because if possible we want the
// waitfor step to finish synchronously. If it doesn't we'll return a function to
// provide the actual unsuspend function and that will get completed when the count
// hits zero or it will get cancelled if the root starts new work.
// 当我们开始挂起时,我们使用一个空操作函数,因为如果可能的话,我们希望 waitfor 步骤能够同步完成。
// 如果无法完成,我们将返回一个函数来提供实际的取消挂起功能,并且当计数归零时该功能将完成,或者如果
// 根节点开始新的工作,它将被取消。
unsuspend: noop,
};
}

幺叁五、挂起实例

备注
export function suspendInstance(
state: SuspendedState,
instance: Instance,
type: Type,
props: Props,
): void {
if (!enableSuspenseyImages && !enableViewTransition) {
return;
}
if (typeof instance.decode === 'function') {
// If this browser supports decode() API, we use it to suspend waiting on the image.
// The loading should have already started at this point, so it should be enough to
// just call decode() which should also wait for the data to finish loading.
// 如果该浏览器支持 decode() API,我们可以使用它来挂起对图像的等待。
// 此时加载应该已经开始,因此只需调用 decode() 就足够了,它也会等待数据加载完成。
state.imgCount++;
// Estimate the byte size that we're about to download based on the width/height
// specified in the props. This is best practice to know ahead of time but if it's
// unspecified we'll fallback to a guess of 100x100 pixels.
// 根据 props 中指定的宽度/高度估算我们即将下载的字节大小。
// 这是提前了解的最佳做法,但如果未指定,我们将回退到猜测 100x100 像素。
if (!(instance as any).complete) {
state.imgBytes += estimateImageBytes(instance as any);
state.suspenseyImages.push(instance as any);
}
const ping = onUnsuspendImg.bind(state);
instance.decode().then(ping, ping);
}
}

幺叁六、挂起资源

export function suspendResource(
state: SuspendedState,
hoistableRoot: HoistableRoot,
resource: Resource,
props: any,
): void {
if (resource.type === 'stylesheet') {
if (typeof props.media === 'string') {
// If we don't currently match media we avoid suspending on this resource
// and let it insert on the mutation path
// 如果我们当前不匹配媒体,我们会避免暂停此资源
// 并允许它在变更路径上插入
if (matchMedia(props.media).matches === false) {
return;
}
}
if ((resource.state.loading & Inserted) === NotLoaded) {
if (resource.instance === null) {
const qualifiedProps: StylesheetQualifyingProps = props;
const key = getStyleKey(qualifiedProps.href);

// Attempt to hydrate instance from DOM
// 尝试从 DOM 中挂载实例
let instance: null | Instance = hoistableRoot.querySelector(
getStylesheetSelectorFromKey(key),
);
if (instance) {
// If this instance has a loading state it came from the Fizz runtime.
// If there is not loading state it is assumed to have been server rendered
// as part of the preamble and therefore synchronously loaded. It could have
// errored however which we still do not yet have a means to detect. For now
// we assume it is loaded.
// 如果这个实例有加载状态,则它来自 Fizz 运行时。如果没有加载状态,则假定它是服务器端渲染的
// 作为前导部分,因此是同步加载的。不过,它可能出现了错误,我们目前还没有方法来检测。现在
// 我们假设它已经加载完成。
const maybeLoadingState: ?Promise<mixed> = (instance as any)._p;
if (
maybeLoadingState !== null &&
typeof maybeLoadingState === 'object' &&
typeof maybeLoadingState.then === 'function'
) {
const loadingState = maybeLoadingState;
state.count++;
const ping = onUnsuspend.bind(state);
loadingState.then(ping, ping);
}
resource.state.loading |= Inserted;
resource.instance = instance;
markNodeAsHoistable(instance);
return;
}

const ownerDocument = getDocumentFromRoot(hoistableRoot);

const stylesheetProps = stylesheetPropsFromRawProps(props);
const preloadProps = preloadPropsMap.get(key);
if (preloadProps) {
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
}

// Construct and insert a new instance
// 构建并插入一个新实例
instance = ownerDocument.createElement('link');
markNodeAsHoistable(instance);
const linkInstance: HTMLLinkElement = instance as any;
// This Promise is a loading state used by the Fizz runtime. We need this incase there is a race
// between this resource being rendered on the client and being rendered with a late completed boundary.
// 这个 Promise 是 Fizz 运行时使用的加载状态。我们需要它,以防这个资源在客户端渲染时与延迟完成的边界渲染之间
// 发生竞争。
(linkInstance as any)._p = new Promise((resolve, reject) => {
linkInstance.onload = resolve;
linkInstance.onerror = reject;
});
setInitialProperties(instance, 'link', stylesheetProps);
resource.instance = instance;
}

if (state.stylesheets === null) {
state.stylesheets = new Map();
}
state.stylesheets.set(resource, hoistableRoot);

const preloadEl = resource.state.preload;
if (preloadEl && (resource.state.loading & Settled) === NotLoaded) {
state.count++;
const ping = onUnsuspend.bind(state);
preloadEl.addEventListener('load', ping);
preloadEl.addEventListener('error', ping);
}
}
}
}

幺叁七、在活动视图切换时挂起

export function suspendOnActiveViewTransition(
state: SuspendedState,
rootContainer: Container,
): void {
const ownerDocument =
rootContainer.nodeType === DOCUMENT_NODE
? rootContainer
: rootContainer.ownerDocument;
const activeViewTransition = ownerDocument.__reactViewTransition;
if (activeViewTransition == null) {
return;
}
state.count++;
state.waitingForViewTransition = true;
const ping = onUnsuspend.bind(state);
activeViewTransition.finished.then(ping, ping);
}

幺叁八、等待提交准备就绪

export function waitForCommitToBeReady(
state: SuspendedState,
timeoutOffset: number,
): null | ((callback: () => void) => () => void) {
if (state.stylesheets && state.count === 0) {
// We are not currently blocked but we have not inserted all stylesheets.
// If this insertion happens and loads or errors synchronously then we can
// avoid suspending the commit. To do this we check the count again immediately after
// 我们目前没有被阻塞,但尚未插入所有样式表。
// 如果此插入操作发生并且同步加载或出错,那么我们可以
// 避免挂起提交。为此,我们会在之后立即再次检查计数
insertSuspendedStylesheets(state, state.stylesheets);
}

// We need to check the count again because the inserted stylesheets may have led to new
// tasks to wait on.
// 我们需要再次检查计数,因为插入的样式表可能导致有新的任务需要等待。
if (state.count > 0 || state.imgCount > 0) {
return commit => {
// We almost never want to show content before its styles have loaded. But
// eventually we will give up and allow unstyled content. So this number is
// somewhat arbitrary — big enough that you'd only reach it under
// extreme circumstances.
// TODO: Figure out what the browser engines do during initial page load and
// consider aligning our behavior with that.
// 我们几乎从不希望在样式加载之前显示内容。但
// 最终我们会放弃并允许未样式化的内容。所以这个数字是
// 有些随意的——足够大,只有在极端情况下才会达到。
// 待办事项:弄清楚浏览器引擎在初始页面加载时的行为,
// 并考虑让我们的行为与之保持一致。
const stylesheetTimer = setTimeout(() => {
if (state.stylesheets) {
insertSuspendedStylesheets(state, state.stylesheets);
}
if (state.unsuspend) {
const unsuspend = state.unsuspend;
state.unsuspend = null;
unsuspend();
}
}, SUSPENSEY_STYLESHEET_TIMEOUT + timeoutOffset);

if (state.imgBytes > 0 && estimatedBytesWithinLimit === 0) {
// Estimate how many bytes we can download in 500ms.
// 估计我们在500毫秒内可以下载多少字节。
const mbps = estimateBandwidth();
estimatedBytesWithinLimit = mbps * 125 * SUSPENSEY_IMAGE_TIME_ESTIMATE;
}
// If we have more images to download than we expect to fit in the timeout, then
// don't wait for images longer than 50ms. The 50ms lets us still do decoding and
// hitting caches if it turns out that they're already in the HTTP cache.
// 如果我们要下载的图片比我们预计在超时时间内能处理的多,
// 那么就不要等待图片超过50毫秒。50毫秒的等待时间仍然允许我们进行解码,
// 并且如果图片已经在HTTP缓存中,还能命中缓存。
const imgTimeout =
state.imgBytes > estimatedBytesWithinLimit
? 50
: SUSPENSEY_IMAGE_TIMEOUT;
const imgTimer = setTimeout(() => {
// We're no longer blocked on images. If CSS resolves after this we can commit.
// 我们不再受图片阻碍。如果 CSS 之后解决了,我们就可以提交。
state.waitingForImages = false;
if (state.count === 0) {
if (state.stylesheets) {
insertSuspendedStylesheets(state, state.stylesheets);
}
if (state.unsuspend) {
const unsuspend = state.unsuspend;
state.unsuspend = null;
unsuspend();
}
}
}, imgTimeout + timeoutOffset);

state.unsuspend = commit;

return () => {
state.unsuspend = null;
clearTimeout(stylesheetTimer);
clearTimeout(imgTimer);
};
};
}
return null;
}

幺叁九、获取暂停提交的原因

export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
if (state.waitingForViewTransition) {
return 'Waiting for the previous Animation';
}
if (state.count > 0) {
if (state.imgCount > 0) {
return 'Suspended on CSS and Images';
}
return 'Suspended on CSS';
}
if (state.imgCount === 1) {
return 'Suspended on an Image';
}
if (state.imgCount > 0) {
return 'Suspended on Images';
}
return null;
}

幺肆零、重置表单实例

export function resetFormInstance(form: FormInstance): void {
form.reset();
}

幺肆一、常量

1. 抑制水化警告

备注

在源码 267 - 284 行

// 抑制水化警告
const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';

// 活动开始数据
const ACTIVITY_START_DATA = '&';
// 活动结束数据
const ACTIVITY_END_DATA = '/&';
// 挂起开始数据
const SUSPENSE_START_DATA = '$';
// 挂起结束数据
const SUSPENSE_END_DATA = '/$';
// 挂起未决的启动数据
const SUSPENSE_PENDING_START_DATA = '$?';
// 挂起未决排队开始数据
const SUSPENSE_QUEUED_START_DATA = '$~';
// 挂起回退开始数据
const SUSPENSE_FALLBACK_START_DATA = '$!';
// 序言贡献 HTML
const PREAMBLE_CONTRIBUTION_HTML = 'html';
// 序言贡献 body
const PREAMBLE_CONTRIBUTION_BODY = 'body';
// 序言贡献 head
const PREAMBLE_CONTRIBUTION_HEAD = 'head';
// 表单状态为匹配中
const FORM_STATE_IS_MATCHING = 'F!';
// 表单状态不匹配
const FORM_STATE_IS_NOT_MATCHING = 'F';
// 文档加载中
const DOCUMENT_READY_STATE_LOADING = 'loading';

// 样式
const STYLE = 'style';

2. 宿主上下文命名空间 SVG

备注

源码的 288 - 289 行

// 宿主上下文命名空间 SVG
const HostContextNamespaceSvg: HostContextNamespace = 1;
// 宿主上下文命名空间 Math
const HostContextNamespaceMath: HostContextNamespace = 2;

3. 警告未知标签

备注

源码中 511 - 522 行

const warnedUnknownTags: {
[key: string]: boolean;
} = {
// There are working polyfills for <dialog>. Let people use it.
// 已经有可用的 <dialog> 填充方案。让大家使用它。
dialog: true,
// Electron ships a custom <webview> tag to display external web content in
// an isolated frame and process.
// This tag is not present in non Electron environments such as JSDom which
// is often used for testing purposes.
// Electron 提供了一个自定义的 <webview> 标签,用于在独立的框架和进程中显示外部网页
// 内容。这个标签在非 Electron 环境中(例如经常用于测试的 JSDom)不存在。
// @see https://electronjs.org/docs/api/webview-tag
webview: true,
};

4. 本地 Promise

备注

源码中 827 - 831 行

// 本地承诺
const localPromise = typeof Promise === 'function' ? Promise : undefined;
// 本地请求动画帧
const localRequestAnimationFrame =
typeof requestAnimationFrame === 'function'
? requestAnimationFrame
: scheduleTimeout;

5. 支持移动到前

备注

源码中 1012 - 1016 行

const supportsMoveBefore =
enableMoveBefore &&
typeof window !== 'undefined' &&
typeof window.Element.prototype.moveBefore === 'function';

6. 挂起字体和图像超时

备注

源码 2067 - 2073 行

// How long to wait for new fonts to load before just committing anyway.
// This freezes the screen. It needs to be short enough that it doesn't cause too much of
// an issue when it's a new load and slow, yet long enough that you have a chance to load
// it. Otherwise we wait for no reason. The assumption here is that you likely have
// either cached the font or preloaded it earlier.
// This timeout is also used for Suspensey Images when they're blocking a View Transition.
// 在提交之前,需要等待新字体加载多长时间。
// 这会冻结屏幕。等待时间需要足够短,以避免新加载时加载缓慢带来的问题,
// 但又要足够长,以便有机会加载字体。否则,我们就是无缘无故地等待。
// 这里的假设是,你可能已经缓存了字体或之前已经预加载过。
// 这个超时设置也用于在阻塞视图转换时的 Suspense 风格图像。
const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 500;

7. 未加载

备注

源码 4769 - 4773 行

// 未加载
const NotLoaded = /* */ 0b000;
// 已加载
const Loaded = /* */ 0b001;
// 出错
const Errored = /* */ 0b010;
// 已解决
const Settled = /* */ 0b011;
// 已插入
const Inserted = /* */ 0b100;

8. 预加载属性映射

备注

源码 4818 - 4821 行

// global collections of Resources
// 资源的全局集合
const preloadPropsMap: Map<string, PreloadProps | PreloadModuleProps> =
new Map();
// 预连接集合
const preconnectsSet: Set<string> = new Set();

9. 前调度器

备注

源码中 4847 - 4861 行

const previousDispatcher =
ReactDOMSharedInternals.d; /* ReactDOMCurrentDispatcher */

ReactDOMSharedInternals.d /* ReactDOMCurrentDispatcher */ = {
f /* flushSyncWork */: disableLegacyMode
? flushSyncWork
: previousDispatcher.f /* flushSyncWork */,
r: requestFormReset,
D /* prefetchDNS */: prefetchDNS,
C /* preconnect */: preconnect,
L /* preload */: preload,
m /* preloadModule */: preloadModule,
X /* preinitScript */: preinitScript,
S /* preinitStyle */: preinitStyle,
M /* preinitModuleScript */: preinitModuleScript,
};

10. 全局文档

备注

源码中 4894 - 4900 行

// We expect this to get inlined. It is a function mostly to communicate the special nature of
// how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling
// these methods outside of render there is no way to know which Document or ShadowRoot is 'scoped'
// and so we have to fall back to something universal. Currently we just refer to the global document.
// This is notable because nowhere else in ReactDOM do we actually reference the global document or window
// because we may be rendering inside an iframe.
// 我们希望这个函数被内联。这个函数主要是为了说明我们如何解析 ReactDOM.pre*() 方法的可
// 提升根节点的特殊性质。因为我们支持在 render 之外调用这些方法,所以无法确定哪个
// Document 或 ShadowRoot 是“作用域内”的,因此我们必须回退到一些通用的东西。目前我们只
// 是引用全局的 document。这一点值得注意,因为在 ReactDOM 的其他地方,我们实际上并不会
// 引用全局的 document 或 window,因为我们可能在 iframe 内进行渲染。
const globalDocument = typeof document === 'undefined' ? null : document;

11. 挂起样式表超时

备注

源码中 6397 - 6401 行

// 挂起样式表超时
const SUSPENSEY_STYLESHEET_TIMEOUT = 60000;
// 挂起图片超时
const SUSPENSEY_IMAGE_TIMEOUT = 800;
// 挂起图片时间预估
const SUSPENSEY_IMAGE_TIME_ESTIMATE = 500;

12. 最后优先

备注

源码中 6525 - 6530 行

// We use a value that is type distinct from precedence to track which one is last.
// This ensures there is no collision with user defined precedences. Normally we would
// just track this in module scope but since the precedences are tracked per HoistableRoot
// we need to associate it to something other than a global scope hence why we try to
// colocate it with the map of precedences in the first place
// 我们使用一种与优先级不同类型的值来跟踪最后一个。
// 这确保不会与用户定义的优先级冲突。通常我们会
// 只在模块作用域中跟踪它,但由于优先级是按可提升根节点(HoistableRoot)跟踪的,
// 我们需要将其与某物关联,而不是全局作用域,这就是为什么我们首先尝试
// 将它与优先级映射放在一起的原因
const LAST_PRECEDENCE = null;

幺肆二、变量

1. 事件已启用

备注

源码中 291 - 292 行

// 事件已启用
let eventsEnabled: ?boolean = null;
// 选择信息
let selectionInformation: null | SelectionInformation = null;

2. 已警告脚本标签

备注

源码中的 472 行

let didWarnScriptTags = false;

3. 判定是否为克隆提供的警示

备注

源码中 656 行

let didWarnForClone = false;

4. 当前Popstate过渡事件

备注

源码 777 行

let currentPopstateTransitionEvent: Event | null = null;

5. 调度器事件

备注

源码 802 行

let schedulerEvent: void | Event = undefined;

6. 进入作用域单例时的先前可水化项

备注

源码 4184 - 4189 行

// If it were possible to have more than one scope singleton in a DOM tree
// we would need to model this as a stack but since you can only have one <head>
// and head is the only singleton that is a scope in DOM we can get away with
// tracking this as a single value.
// 如果在 DOM 树中可能有多个作用域单例。我们将需要将其建模为一个堆栈,但由于你只能有一个
// <head>。并且 head 是 DOM 中唯一作为作用域的单例,我们可以通过将其作为单个值来跟踪
let previousHydratableOnEnteringScopedSingleton: null | HydratableInstance =
null;

7. 标签缓存

备注

源码中 5795 行

let tagCaches: null | DocumentTagCaches = null;

8. 估计字节在限制内

备注

源码中 6403 行

let estimatedBytesWithinLimit: number = 0;

9. 按根优先级

备注

源码中 6532 - 6538 行

// This is typecast to non-null because it will always be set before read.
// it is important that this not be used except when the stack guarantees it exists.
// Currentlyt his is only during insertSuspendedStylesheet.
// 这被强制类型转换为非空,因为它在读取前一定会被设置。
// 重要的是,除非堆栈保证其存在,否则不要使用它。
// 目前,这仅在 insertSuspendedStylesheet 期间使用。
let precedencesByRoot: Map<
HoistableRoot,
Map<string | typeof LAST_PRECEDENCE, Instance>
> = null as any;

幺肆三、工具

1. 从根容器获取所属文档

function getOwnerDocumentFromRootContainer(
rootContainerElement: Element | Document | DocumentFragment,
): Document {
return rootContainerElement.nodeType === DOCUMENT_NODE
? (rootContainerElement as any)
: rootContainerElement.ownerDocument;
}

2. 获取自身宿主上下文

function getOwnHostContext(namespaceURI: string): HostContextNamespace {
switch (namespaceURI) {
case SVG_NAMESPACE:
return HostContextNamespaceSvg;
case MATH_NAMESPACE:
return HostContextNamespaceMath;
default:
return HostContextNamespaceNone;
}
}

3. 获取子宿主上下文生产

function getChildHostContextProd(
parentNamespace: HostContextNamespace,
type: string,
): HostContextNamespace {
if (parentNamespace === HostContextNamespaceNone) {
// No (or default) parent namespace: potential entry point.
// 没有(或默认)父命名空间:潜在的入口点。
switch (type) {
case 'svg':
return HostContextNamespaceSvg;
case 'math':
return HostContextNamespaceMath;
default:
return HostContextNamespaceNone;
}
}
if (parentNamespace === HostContextNamespaceSvg && type === 'foreignObject') {
// We're leaving SVG.
// 我们离开 SVG。
return HostContextNamespaceNone;
}
// By default, pass namespace below.
// 默认情况下,请传递以下命名空间。
return parentNamespace;
}

4. 判定是否是脚本数据块

function isScriptDataBlock(props: Props): boolean {
const scriptType = props.type;
if (typeof scriptType !== 'string' || scriptType === '') {
return false;
}
const lower = scriptType.toLowerCase();
// Special non-MIME keywords recognized by the HTML spec
// HTML 规范识别的特殊非 MIME 关键字
// TODO: May be fine to also not warn about having these types be parsed as "parser-inserted"
// 待办事项:也许不警告将这些类型解析为“解析器插入”也可以
if (
lower === 'module' ||
lower === 'importmap' ||
lower === 'speculationrules'
) {
return false;
}
// JavaScript MIME types per https://mimesniff.spec.whatwg.org/#javascript-mime-type
// 根据 https://mimesniff.spec.whatwg.org/#javascript-mime-type 的 JavaScript MIME 类型
switch (lower) {
case 'application/ecmascript':
case 'application/javascript':
case 'application/x-ecmascript':
case 'application/x-javascript':
case 'text/ecmascript':
case 'text/javascript':
case 'text/javascript1.0':
case 'text/javascript1.1':
case 'text/javascript1.2':
case 'text/javascript1.3':
case 'text/javascript1.4':
case 'text/javascript1.5':
case 'text/jscript':
case 'text/livescript':
case 'text/x-ecmascript':
case 'text/x-javascript':
return false;
}
// Any other non-empty type value means this is a data block
// 任何其他非空类型值都表示这是一个数据块
return true;
}

5. 在下一个周期处理错误

function handleErrorInNextTick(error: any) {
setTimeout(() => {
throw error;
});
}

6. 警告 React 子组件冲突

备注
function warnForReactChildrenConflict(container: Container): void {
if (__DEV__) {
if ((container as any).__reactWarnedAboutChildrenConflict) {
return;
}
const props = getFiberCurrentPropsFromNode(container);
if (props !== null) {
const fiber = getInstanceFromNode(container);
if (fiber !== null) {
if (
typeof props.children === 'string' ||
typeof props.children === 'number'
) {
(container as any).__reactWarnedAboutChildrenConflict = true;
// Run the warning with the Fiber of the container for context of where the children are specified.
// We could also maybe use the Portal. The current execution context is the child being added.
// 使用容器的 Fiber 运行警告,以获取指定子元素的位置上下文。
// 我们也许也可以使用 Portal。当前的执行上下文是正在添加的子元素。
runWithFiberInDEV(fiber, () => {
console.error(
'Cannot use a ref on a React element as a container to `createRoot` or `createPortal` ' +
'if that element also sets "children" text content using React. It should be a leaf with no children. ' +
"Otherwise it's ambiguous which children should be used.",
);
});
} else if (props.dangerouslySetInnerHTML != null) {
(container as any).__reactWarnedAboutChildrenConflict = true;
runWithFiberInDEV(fiber, () => {
console.error(
'Cannot use a ref on a React element as a container to `createRoot` or `createPortal` ' +
'if that element also sets "dangerouslySetInnerHTML" using React. It should be a leaf with no children. ' +
"Otherwise it's ambiguous which children should be used.",
);
});
}
}
}
}
}

7. 创建事件

function createEvent(type: DOMEventName, bubbles: boolean): Event {
const event = document.createEvent('Event');
event.initEvent(type as any as string, bubbles, false);
return event;
}

8. 在解除焦点前派发

备注
function dispatchBeforeDetachedBlur(
target: HTMLElement,
internalInstanceHandle: Object,
): void {
if (enableCreateEventHandleAPI) {
const event = createEvent('beforeblur', true);
// Dispatch "beforeblur" directly on the target,
// so it gets picked up by the event system and
// can propagate through the React internal tree.
// 直接在目标上派发 "beforeblur",这样事件系统就能捕获它,并且可以在 React 内部树
// 中传播。
event._detachedInterceptFiber = internalInstanceHandle;
target.dispatchEvent(event);
}
}

9. 在脱离模糊后分发

function dispatchAfterDetachedBlur(target: HTMLElement): void {
if (enableCreateEventHandleAPI) {
const event = createEvent('afterblur', false);
// So we know what was detached, make the relatedTarget the
// detached target on the "afterblur" event.
// 所以我们知道什么被分离了,在“afterblur”事件中,将 relatedTarget 设置为被分离
// 的目标。
(event: any).relatedTarget = target;
// Dispatch the event on the document.
// 在文档上分发事件。
document.dispatchEvent(event);
}
}

10. 清除水化边界

备注
function clearHydrationBoundary(
parentInstance: Instance,
hydrationInstance: SuspenseInstance | ActivityInstance,
): void {
let node: Node = hydrationInstance;
// Delete all nodes within this suspense boundary.
// There might be nested nodes so we need to keep track of how
// deep we are and only break out when we're back on top.
// 删除此 suspense 边界内的所有节点。可能存在嵌套节点,所以我们需要跟踪当前的深度
// 只有在回到顶层时才退出。
let depth = 0;
do {
const nextNode = node.nextSibling;
parentInstance.removeChild(node);
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
const data = (nextNode as any).data as string;
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
if (depth === 0) {
parentInstance.removeChild(nextNode);
// Retry if any event replaying was blocked on this.
// 如果任何事件重放被阻止,则重试。
retryIfBlockedOn(hydrationInstance);
return;
} else {
depth--;
}
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === ACTIVITY_START_DATA
) {
depth++;
} else if (data === PREAMBLE_CONTRIBUTION_HTML) {
// If a preamble contribution marker is found within the bounds of this boundary,
// then it contributed to the html tag and we need to reset it.
// 如果在此边界的范围内找到前导贡献标记,那么它会对 HTML 标签产生贡献,我们需要
// 重置它。
const ownerDocument = parentInstance.ownerDocument;
const documentElement: Element = ownerDocument.documentElement as any;
releaseSingletonInstance(documentElement);
} else if (data === PREAMBLE_CONTRIBUTION_HEAD) {
const ownerDocument = parentInstance.ownerDocument;
const head: Element = ownerDocument.head as any;
releaseSingletonInstance(head);
// We need to clear the head because this is the only singleton that can have children that
// were part of this boundary but are not inside this boundary.
// 我们需要清理头部,因为这是唯一可以拥有那些曾属于该边界但不在该边界内的子节点的
// 单例。
clearHead(head);
} else if (data === PREAMBLE_CONTRIBUTION_BODY) {
const ownerDocument = parentInstance.ownerDocument;
const body: Element = ownerDocument.body as any;
releaseSingletonInstance(body);
}
}
node = nextNode;
} while (node);
// TODO: Warn, we didn't find the end comment boundary.
// Retry if any event replaying was blocked on this.
// 待办事项:警告,我们没有找到结束的注释边界。如果有任何事件重放被阻塞,请重试。
retryIfBlockedOn(hydrationInstance);
}

11. 从容器中清除水化边界

备注
function clearHydrationBoundaryFromContainer(
container: Container,
hydrationInstance: SuspenseInstance | ActivityInstance,
): void {
let parentNode: DocumentFragment | Element;
if (container.nodeType === DOCUMENT_NODE) {
parentNode = (container as any).body;
} else if (
!disableCommentsAsDOMContainers &&
container.nodeType === COMMENT_NODE
) {
parentNode = container.parentNode as any;
} else if (container.nodeName === 'HTML') {
parentNode = container.ownerDocument.body as any;
} else {
parentNode = container as any;
}
clearHydrationBoundary(parentNode, hydrationInstance);
// Retry if any event replaying was blocked on this.
// 如果任何事件重放被阻止,则重试。
retryIfBlockedOn(container);
}

12. 隐藏或显示脱水边界

function hideOrUnhideDehydratedBoundary(
suspenseInstance: SuspenseInstance | ActivityInstance,
isHidden: boolean,
) {
let node: Node = suspenseInstance;
// Unhide all nodes within this suspense boundary.
// 显示此 suspense 边界内的所有节点。
let depth = 0;
do {
const nextNode = node.nextSibling;
if (node.nodeType === ELEMENT_NODE) {
const instance = node as any as HTMLElement & {
_stashedDisplay?: string;
};
if (isHidden) {
instance._stashedDisplay = instance.style.display;
instance.style.display = 'none';
} else {
instance.style.display = instance._stashedDisplay || '';
if (instance.getAttribute('style') === '') {
instance.removeAttribute('style');
}
}
} else if (node.nodeType === TEXT_NODE) {
const textNode = node as any as Text & { _stashedText?: string };
if (isHidden) {
textNode._stashedText = textNode.nodeValue;
textNode.nodeValue = '';
} else {
textNode.nodeValue = textNode._stashedText || '';
}
}
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
const data = (nextNode as any).data as string;
if (data === SUSPENSE_END_DATA) {
if (depth === 0) {
return;
} else {
depth--;
}
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA
) {
depth++;
}
// TODO: Should we hide preamble contribution in this case?
// 待办:在这种情况下我们是否应该隐藏前言贡献?
}
node = nextNode;
} while (node);
}

13. 警告内联元素中的块级元素

备注
function warnForBlockInsideInline(instance: HTMLElement) {
if (__DEV__) {
let nextNode = instance.firstChild;
outer: while (nextNode != null) {
let node: Node = nextNode;
if (
node.nodeType === ELEMENT_NODE &&
getComputedStyle(node as any).display === 'block'
) {
const fiber =
getInstanceFromNode(node) || getInstanceFromNode(instance);
runWithFiberInDEV(
fiber,
(parentTag: string, childTag: string) => {
console.error(
"You're about to start a <ViewTransition> around a display: inline " +
'element <%s>, which itself has a display: block element <%s> inside it. ' +
'This might trigger a bug in Safari which causes the View Transition to ' +
'be skipped with a duplicate name error.\n' +
'https://bugs.webkit.org/show_bug.cgi?id=290923',
parentTag.toLocaleLowerCase(),
childTag.toLocaleLowerCase(),
);
},
instance.tagName,
(node as any).tagName,
);
break;
}
if (node.firstChild != null) {
nextNode = node.firstChild;
continue;
}
if (node === instance) {
break;
}
while (node.nextSibling == null) {
if (node.parentNode == null || node.parentNode === instance) {
break;
}
node = node.parentNode;
}
nextNode = node.nextSibling;
}
}
}

14. 计算客户端矩形

function countClientRects(rects: Array<ClientRect>): number {
if (rects.length === 1) {
return 1;
}
// Count non-zero rects.
// 计算非零矩形。
let count = 0;
for (let i = 0; i < rects.length; i++) {
const rect = rects[i];
if (rect.width > 0 && rect.height > 0) {
count++;
}
}
return count;
}

15. 获取计算变换

function getComputedTransform(style: CSSStyleDeclaration): string {
// Gets the merged transform of all the short hands.
// 获取所有简写的合并变换。
const computedStyle: any = style;
let transform: string = computedStyle.transform;
if (transform === 'none') {
transform = '';
}
const scale: string = computedStyle.scale;
if (scale !== 'none' && scale !== '') {
const parts = scale.split(' ');
transform =
(parts.length === 3 ? 'scale3d' : 'scale') +
'(' +
parts.join(', ') +
') ' +
transform;
}
const rotate: string = computedStyle.rotate;
if (rotate !== 'none' && rotate !== '') {
const parts = rotate.split(' ');
if (parts.length === 1) {
transform = 'rotate(' + parts[0] + ') ' + transform;
} else if (parts.length === 2) {
transform =
'rotate' + parts[0].toUpperCase() + '(' + parts[1] + ') ' + transform;
} else {
transform = 'rotate3d(' + parts.join(', ') + ') ' + transform;
}
}
const translate: string = computedStyle.translate;
if (translate !== 'none' && translate !== '') {
const parts = translate.split(' ');
transform =
(parts.length === 3 ? 'translate3d' : 'translate') +
'(' +
parts.join(', ') +
') ' +
transform;
}
return transform;
}

16. 移出视口

function moveOutOfViewport(
originalStyle: CSSStyleDeclaration,
element: HTMLElement,
): void {
// Apply a transform that safely puts the whole element outside the viewport
// while still letting it paint its "old" state to a snapshot.
// 应用一个变换,将整个元素安全地移到视口之外同时仍然允许它将“旧”的状态绘制到快照中。
const transform = getComputedTransform(originalStyle);
// Clear the long form properties.
// 清除长表单属性。
element.style.translate = 'none';
element.style.scale = 'none';
element.style.rotate = 'none';
// Apply a translate to move it way out of the viewport. This is applied first
// so that it is in the coordinate space of the parent and not after applying
// other transforms. That's why we need to merge the long form properties.
// TODO: Ideally we'd adjust for the parent's rotate/scale. Otherwise when
// we move back the ::view-transition-group we might overshoot or undershoot.
// 应用一个平移,将其移出视口外。这是首先应用的这样它就在父元素的坐标空间中,而不是在应
// 用其他变换之后。这就是为什么我们需要合并长格式属性。
// TODO: 理想情况下,我们应该调整父元素的旋转/缩放。否则当我们将
// ::view-transition-group 移回时,可能会超出或不足。
element.style.transform = 'translate(-20000px, -20000px) ' + transform;
}

17. 将旧帧移动到视口中

function moveOldFrameIntoViewport(keyframe: any): void {
// In the resulting View Transition Animation, the first frame will be offset.
// 在生成的视图过渡动画中,第一帧将会有偏移。
const computedTransform: ?string = keyframe.transform;
if (computedTransform != null) {
let transform = computedTransform === 'none' ? '' : computedTransform;
transform = 'translate(20000px, 20000px) ' + transform;
keyframe.transform = transform;
}
}

18. 创建测量

function createMeasurement(
rect: ClientRect | DOMRect,
computedStyle: CSSStyleDeclaration,
element: Element,
): InstanceMeasurement {
const ownerWindow = element.ownerDocument.defaultView;
return {
rect: rect,
abs:
// Absolutely positioned instances don't contribute their size to the parent.
// 绝对定位的实例不会将它们的尺寸贡献给父元素。
computedStyle.position === 'absolute' ||
computedStyle.position === 'fixed',
clip:
// If a ViewTransition boundary acts as a clipping parent group we should
// always mark it to animate if its children do so that we can clip them.
// This doesn't actually have any effect yet until browsers implement
// layered capture and nested view transitions.
// 如果一个 ViewTransition 边界充当裁剪父组,我们应该
// 如果它的子元素有动画,则总是标记它进行动画,以便我们可以裁剪它们。
// 但在浏览器实现分层捕获和嵌套视图过渡之前,这实际上还没有任何效果。
computedStyle.clipPath !== 'none' ||
computedStyle.overflow !== 'visible' ||
computedStyle.filter !== 'none' ||
computedStyle.mask !== 'none' ||
computedStyle.mask !== 'none' ||
computedStyle.borderRadius !== '0px',
view:
// If the instance was within the bounds of the viewport. We don't care as
// much about if it was fully occluded because then it can still pop out.
// 如果实例在视口的范围内。我们不太在意它是否完全被遮挡,因为那样它仍然可以弹出。
rect.bottom >= 0 &&
rect.right >= 0 &&
rect.top <= ownerWindow.innerHeight &&
rect.left <= ownerWindow.innerWidth,
};
}

19. 自定义视图转换错误

function customizeViewTransitionError(
error: Object,
ignoreAbort: boolean,
): mixed {
if (typeof error === 'object' && error !== null) {
switch (error.name) {
case 'TimeoutError': {
// We assume that the only reason a Timeout can happen is because the Navigation
// promise. We expect any other work to either be fast or have a timeout (fonts).
// 我们假设超时发生的唯一原因是导航 promise。
// 我们期望其他任何工作要么很快完成,要么有超时(字体)。
if (__DEV__) {
return new Error(
'A ViewTransition timed out because a Navigation stalled. ' +
'This can happen if a Navigation is blocked on React itself. ' +
"Such as if it's resolved inside useEffect. " +
'This can be solved by moving the resolution to useLayoutEffect.',
{ cause: error },
);
}
break;
}
case 'AbortError': {
if (ignoreAbort) {
return null;
}
if (__DEV__) {
return new Error(
'A ViewTransition was aborted early. This might be because you have ' +
'other View Transition libraries on the page and only one can run at ' +
"a time. To avoid this, use only React's built-in <ViewTransition> " +
'to coordinate.',
{ cause: error },
);
}
break;
}
case 'InvalidStateError': {
if (
error.message ===
'View transition was skipped because document visibility state is hidden.' ||
error.message ===
'Skipping view transition because document visibility state has become hidden.' ||
error.message ===
'Skipping view transition because viewport size changed.' ||
// Chrome uses a generic error message instead of specific reasons. It will log a
// more specific reason in the console but the user might not look there.
// Some of these errors are important to surface like duplicate name errors but
// it's too noisy for unactionable cases like the document was hidden. Therefore,
// we hide all of them and hopefully it surfaces in another browser.
// Chrome 使用的是通用错误信息,而不是具体原因。它会在控制台记录更具体的原
// 因,但用户可能不会查看那里。其中一些错误很重要,例如重复名称错误,但对于无
// 法采取行动的情况(如文档被隐藏)来说,则太嘈杂。因此,我们隐藏了所有这些错
// 误,并希望它能在其他浏览器中显示出来。
error.message === 'Transition was aborted because of invalid state'
) {
// Skip logging this. This is not considered an error.
// 跳过记录这个。这不被认为是错误。
return null;
}
break;
}
}
}
return error;
}

20. 强制布局

/** @noinline */
function forceLayout(ownerDocument: Document) {
// This function exists to trick minifiers to not remove this unused member expression.
// 这个函数的存在是为了欺骗压缩工具,不去删除这个未使用的成员表达式。
return (ownerDocument.documentElement as any).clientHeight;
}

21. 等待图像加载

function waitForImageToLoad(this: HTMLImageElement, resolve: () => void) {
// TODO: Use decode() instead of the load event here once the fix in
// TODO: 修复后这里使用 decode() 替代 load 事件
// https://issues.chromium.org/issues/420748301 has propagated fully.
this.addEventListener('load', resolve);
this.addEventListener('error', resolve);
}

22. 合并翻译

function mergeTranslate(translateA: ?string, translateB: ?string): string {
if (!translateA || translateA === 'none') {
return translateB || '';
}
if (!translateB || translateB === 'none') {
return translateA || '';
}
const partsA = translateA.split(' ');
const partsB = translateB.split(' ');
let i;
let result = '';
for (i = 0; i < partsA.length && i < partsB.length; i++) {
if (i > 0) {
result += ' ';
}
result += 'calc(' + partsA[i] + ' + ' + partsB[i] + ')';
}
for (; i < partsA.length; i++) {
result += ' ' + partsA[i];
}
for (; i < partsB.length; i++) {
result += ' ' + partsB[i];
}
return result;
}

23. 动画手势

function animateGesture(
keyframes: any,
targetElement: Element,
pseudoElement: string,
timeline: GestureTimeline,
viewTransitionAnimations: Array<Animation>,
customTimelineCleanup: Array<() => void>,
rangeStart: number,
rangeEnd: number,
moveFirstFrameIntoViewport: boolean,
moveAllFramesIntoViewport: boolean,
) {
let width;
let height;
let unchangedDimensions = true;
for (let i = 0; i < keyframes.length; i++) {
const keyframe = keyframes[i];
// Delete any easing since we always apply linear easing to gestures.
// 删除任何缓动,因为我们总是对手势应用线性缓动。
delete keyframe.easing;
delete keyframe.computedOffset;
const w = keyframe.width;
if (width === undefined) {
width = w;
} else if (width !== w) {
unchangedDimensions = false;
}
const h = keyframe.height;
if (height === undefined) {
height = h;
} else if (height !== h) {
unchangedDimensions = false;
}
// Chrome returns "auto" for width/height which is not a valid value to
// animate to. Similarly, transform: "none" is actually lack of transform.
// Chrome 返回的 width/height 为 "auto",这不是一个有效的动画目标值。同样,
// transform: "none" 实际上表示没有变换。
if (keyframe.width === 'auto') {
delete keyframe.width;
}
if (keyframe.height === 'auto') {
delete keyframe.height;
}
if (keyframe.transform === 'none') {
delete keyframe.transform;
}
if (moveAllFramesIntoViewport) {
if (keyframe.transform == null) {
// If a transform is not explicitly specified to override the auto
// generated one on the pseudo element, then we need to adjust it to
// put it back into the viewport. We don't know the offset relative to
// the screen so instead we use the translate prop to do a relative
// adjustment.
// TODO: If the "transform" was manually overridden on the pseudo
// element itself and no longer the auto generated one, then we shouldn't
// adjust it. I'm not sure how to detect this.
// 如果没有显式指定一个变换来覆盖伪元素上自动生成的变换,那么我们需要调整它以将其
// 放回视口中。我们不知道相对于屏幕的偏移量,所以改为使用 translate 属性进行相
// 对调整。
// TODO: 如果伪元素本身的 “transform” 已被手动覆盖,而不再是自动生成的,那么
// 我们不应该调整它。我不确定如何检测这一点。
if (keyframe.translate == null || keyframe.translate === '') {
// TODO: If there's a CSS rule targeting translate on the pseudo element
// already we need to merge it.
// 待办事项:如果已有 CSS 规则针对伪元素的 translate,我们需要将其合并。
const elementTranslate: ?string = (
getComputedStyle(targetElement, pseudoElement) as any
).translate;
keyframe.translate = mergeTranslate(
elementTranslate,
'20000px 20000px',
);
} else {
keyframe.translate = mergeTranslate(
keyframe.translate,
'20000px 20000px',
);
}
}
}
}
if (moveFirstFrameIntoViewport) {
// If this is the generated animation that does a FLIP matrix translation
// from the old position, we need to adjust it from the out of viewport
// position. If this is going from old to new it only applies to first
// keyframe. Otherwise it applies to every keyframe.
// 如果这是生成的动画,它执行从旧位置的 FLIP 矩阵平移。我们需要根据视口之外的位置进
// 行调整。如果这是从旧位置到新位置的转换,它只适用于第一个关键帧。否则,它适用于每一
// 个关键帧。
moveOldFrameIntoViewport(keyframes[0]);
}
if (unchangedDimensions && width !== undefined && height !== undefined) {
// Read the underlying width/height of the pseudo-element. The previous animation
// should have already been cancelled so we should observe the underlying element.
// 读取伪元素的实际宽度/高度。之前的动画应该已经被取消,所以我们应当观察底层元素。
const computedStyle = getComputedStyle(targetElement, pseudoElement);
if (computedStyle.width === width && computedStyle.height === height) {
for (let i = 0; i < keyframes.length; i++) {
const keyframe = keyframes[i];
delete keyframe.width;
delete keyframe.height;
}
}
}

// TODO: Reverse the reverse if the original direction is reverse.
// TODO: 如果原始方向是相反的,则反转反转。
const reverse = rangeStart > rangeEnd;
if (timeline instanceof AnimationTimeline) {
// Native Timeline
// 原生时间线
const animation = targetElement.animate(keyframes, {
pseudoElement: pseudoElement,
// Set the timeline to the current gesture timeline to drive the updates.
// 将时间线设置为当前手势时间线以驱动更新。
timeline: timeline,
// We reset all easing functions to linear so that it feels like you
// have direct impact on the transition and to avoid double bouncing
// from scroll bouncing.
// 我们将所有缓动函数重置为线性,这样可以让你感觉对过渡有直接影响,并避免滚动反弹时
// 的二次弹跳。
easing: 'linear',
// We fill in both direction for overscroll.
// 我们为过度滚动填充两个方向。
fill: 'both', // TODO: Should we preserve the fill instead?
// We play all gestures in reverse, except if we're in reverse direction
// in which case we need to play it in reverse of the reverse.
// 我们以相反的顺序播放所有手势,除非我们处于反向模式在这种情况下,我们需要播放反向
// 的反向手势。
direction: reverse ? 'normal' : 'reverse',
// Range start needs to be higher than range end. If it goes in reverse
// we reverse the whole animation below.
// 范围起点需要大于范围终点。如果顺序相反,我们将在下面反转整个动画。
rangeStart: (reverse ? rangeEnd : rangeStart) + '%',
rangeEnd: (reverse ? rangeStart : rangeEnd) + '%',
});
viewTransitionAnimations.push(animation);
} else {
// Custom Timeline
// 自定义时间轴
const animation = targetElement.animate(keyframes, {
pseudoElement: pseudoElement,
// We reset all easing functions to linear so that it feels like you
// have direct impact on the transition and to avoid double bouncing
// from scroll bouncing.
// 我们将所有缓动函数重置为线性,这样可以让你感觉对过渡有直接影响,并避免滚动反弹时
// 的二次弹跳。
easing: 'linear',
// We fill in both direction for overscroll.
// 我们为过度滚动填充两个方向。
fill: 'both', // TODO: Should we preserve the fill instead?
// We play all gestures in reverse, except if we're in reverse direction
// in which case we need to play it in reverse of the reverse.
// 我们会将所有动作倒放,除非我们正处于倒放方向.在这种情况下,我们需要播放动作的倒
// 放版本的倒放。
direction: reverse ? 'normal' : 'reverse',
// We set the delay and duration to represent the span of the range.
// 我们设置延迟和持续时间来表示范围的跨度。
delay: reverse ? rangeEnd : rangeStart,
duration: reverse ? rangeStart - rangeEnd : rangeEnd - rangeStart,
});
viewTransitionAnimations.push(animation);
// Let the custom timeline take control of driving the animation.
// 让自定义时间轴控制动画的播放。
const cleanup = timeline.animate(animation);
if (cleanup) {
customTimelineCleanup.push(cleanup);
}
}
}

24. 视图过渡伪元素

function ViewTransitionPseudoElement(
this: ViewTransitionPseudoElementType,
pseudo: string,
name: string,
) {
this._scope = document.documentElement as any;
this._selector = '::view-transition-' + pseudo + '(' + name + ')';
}

ViewTransitionPseudoElement.prototype.animate = function (
this: ViewTransitionPseudoElementType,
keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
options?: number | KeyframeAnimationOptions,
): Animation {
const opts: any =
typeof options === 'number'
? {
duration: options,
}
: Object.assign({} as KeyframeAnimationOptions, options);
opts.pseudoElement = this._selector;
// TODO: Handle multiple child instances.
// 待办:处理多个子实例。
return this._scope.animate(keyframes, opts);
};

ViewTransitionPseudoElement.prototype.getAnimations = function (
this: ViewTransitionPseudoElementType,
options?: GetAnimationsOptions,
): Animation[] {
const scope = this._scope;
const selector = this._selector;
const animations = scope.getAnimations({ subtree: true });
const result = [];
for (let i = 0; i < animations.length; i++) {
const effect: null | {
target?: Element;
pseudoElement?: string;
// ...
} = animations[i].effect as any;
// TODO: Handle multiple child instances.
// 待办:处理多个子实例。
if (
effect !== null &&
effect.target === scope &&
effect.pseudoElement === selector
) {
result.push(animations[i]);
}
}
return result;
};

ViewTransitionPseudoElement.prototype.getComputedStyle = function (
this: ViewTransitionPseudoElementType,
): CSSStyleDeclaration {
const scope = this._scope;
const selector = this._selector;
return getComputedStyle(scope, selector);
};

25. 片段实例

备注
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
this._fragmentFiber = fragmentFiber;
this._eventListeners = null;
this._observers = null;
}

FragmentInstance.prototype.addEventListener = function (
this: FragmentInstanceType,
type: string,
listener: EventListener,
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
): void {
if (this._eventListeners === null) {
this._eventListeners = [];
}

const listeners = this._eventListeners;
// Element.addEventListener will only apply uniquely new event listeners by default. Since we
// need to collect the listeners to apply to appended children, we track them ourselves and use
// custom equality check for the options.
// Element.addEventListener 默认只会应用唯一的新事件监听器。由于我们需要收集监听器
// 以应用到附加的子元素,因此我们自己跟踪它们并对选项使用自定义的相等性检查。
const isNewEventListener =
indexOfEventListener(listeners, type, listener, optionsOrUseCapture) === -1;
if (isNewEventListener) {
listeners.push({ type, listener, optionsOrUseCapture });
traverseFragmentInstance(
this._fragmentFiber,
addEventListenerToChild,
type,
listener,
optionsOrUseCapture,
);
}
this._eventListeners = listeners;
};
function addEventListenerToChild(
child: Fiber,
type: string,
listener: EventListener,
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
): boolean {
const instance = getInstanceFromHostFiber<Instance>(child);
instance.addEventListener(type, listener, optionsOrUseCapture);
return false;
}

FragmentInstance.prototype.removeEventListener = function (
this: FragmentInstanceType,
type: string,
listener: EventListener,
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
): void {
const listeners = this._eventListeners;
if (listeners === null) {
return;
}
if (typeof listeners !== 'undefined' && listeners.length > 0) {
traverseFragmentInstance(
this._fragmentFiber,
removeEventListenerFromChild,
type,
listener,
optionsOrUseCapture,
);
const index = indexOfEventListener(
listeners,
type,
listener,
optionsOrUseCapture,
);
if (this._eventListeners !== null) {
this._eventListeners.splice(index, 1);
}
}
};
function removeEventListenerFromChild(
child: Fiber,
type: string,
listener: EventListener,
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
): boolean {
const instance = getInstanceFromHostFiber<Instance>(child);
instance.removeEventListener(type, listener, optionsOrUseCapture);
return false;
}

function normalizeListenerOptions(
opts: ?EventListenerOptionsOrUseCapture,
): string {
if (opts == null) {
return '0';
}

if (typeof opts === 'boolean') {
return `c=${opts ? '1' : '0'}`;
}

return `c=${opts.capture ? '1' : '0'}&o=${opts.once ? '1' : '0'}&p=${opts.passive ? '1' : '0'}`;
}
function indexOfEventListener(
eventListeners: Array<StoredEventListener>,
type: string,
listener: EventListener,
optionsOrUseCapture: void | EventListenerOptionsOrUseCapture,
): number {
if (eventListeners.length === 0) {
return -1;
}
const normalizedOptions = normalizeListenerOptions(optionsOrUseCapture);
for (let i = 0; i < eventListeners.length; i++) {
const item = eventListeners[i];
if (
item.type === type &&
item.listener === listener &&
normalizeListenerOptions(item.optionsOrUseCapture) === normalizedOptions
) {
return i;
}
}
return -1;
}
FragmentInstance.prototype.dispatchEvent = function (
this: FragmentInstanceType,
event: Event,
): boolean {
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
if (parentHostFiber === null) {
return true;
}
const parentHostInstance =
getInstanceFromHostFiber<Instance>(parentHostFiber);
const eventListeners = this._eventListeners;
if (
(eventListeners !== null && eventListeners.length > 0) ||
!event.bubbles
) {
const temp = document.createTextNode('');
if (eventListeners) {
for (let i = 0; i < eventListeners.length; i++) {
const { type, listener, optionsOrUseCapture } = eventListeners[i];
temp.addEventListener(type, listener, optionsOrUseCapture);
}
}
parentHostInstance.appendChild(temp);
const cancelable = temp.dispatchEvent(event);
if (eventListeners) {
for (let i = 0; i < eventListeners.length; i++) {
const { type, listener, optionsOrUseCapture } = eventListeners[i];
temp.removeEventListener(type, listener, optionsOrUseCapture);
}
}
parentHostInstance.removeChild(temp);
return cancelable;
} else {
return parentHostInstance.dispatchEvent(event);
}
};
FragmentInstance.prototype.focus = function (
this: FragmentInstanceType,
focusOptions?: FocusOptions,
): void {
traverseFragmentInstanceDeeply(
this._fragmentFiber,
setFocusOnFiberIfFocusable,
focusOptions,
);
};
function setFocusOnFiberIfFocusable(
fiber: Fiber,
focusOptions?: FocusOptions,
): boolean {
if (enableFragmentRefsTextNodes) {
// Skip text nodes - they are not focusable
// 跳过文本节点 - 它们不可聚焦
if (fiber.tag === HostText) {
return false;
}
}
const instance = getInstanceFromHostFiber<Instance>(fiber);
return setFocusIfFocusable(instance, focusOptions);
}
FragmentInstance.prototype.focusLast = function (
this: FragmentInstanceType,
focusOptions?: FocusOptions,
): void {
const children: Array<Fiber> = [];
traverseFragmentInstanceDeeply(
this._fragmentFiber,
collectChildren,
children,
);
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (setFocusOnFiberIfFocusable(child, focusOptions)) {
break;
}
}
};
function collectChildren(child: Fiber, collection: Array<Fiber>): boolean {
collection.push(child);
return false;
}
FragmentInstance.prototype.blur = function (this: FragmentInstanceType): void {
// Early exit if activeElement is not within the fragment's parent
// 如果 activeElement 不在片段的父元素内,则提前退出
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
if (parentHostFiber === null) {
return;
}
const parentHostInstance =
getInstanceFromHostFiber<Instance>(parentHostFiber);
const activeElement = parentHostInstance.ownerDocument.activeElement;
if (activeElement === null || !parentHostInstance.contains(activeElement)) {
return;
}
traverseFragmentInstance(
this._fragmentFiber,
blurActiveElementWithinFragment,
activeElement,
);
};
function blurActiveElementWithinFragment(
child: Fiber,
activeElement: Element,
): boolean {
// Skip text nodes - they can't be focused
// 跳过文本节点 - 它们不能被聚焦
if (enableFragmentRefsTextNodes && child.tag === HostText) {
return false;
}
const instance = getInstanceFromHostFiber<Instance>(child);
if (instance === activeElement) {
instance.blur();
return true;
}
return false;
}
FragmentInstance.prototype.observeUsing = function (
this: FragmentInstanceType,
observer: IntersectionObserver | ResizeObserver,
): void {
if (__DEV__) {
if (enableFragmentRefsTextNodes) {
let hasText = false;
let hasElement = false;
traverseFragmentInstance(this._fragmentFiber, (child: Fiber) => {
if (child.tag === HostText) {
hasText = true;
} else {
// Stop traversal, found element
// 停止遍历,找到元素
hasElement = true;
return true;
}
return false;
});
if (hasText && !hasElement) {
console.error(
'observeUsing() was called on a FragmentInstance with only text children. ' +
'Observers do not work on text nodes.',
);
}
}
}
if (this._observers === null) {
this._observers = new Set();
}
this._observers.add(observer);
traverseFragmentInstance(this._fragmentFiber, observeChild, observer);
};
function observeChild(
child: Fiber,
observer: IntersectionObserver | ResizeObserver,
) {
if (enableFragmentRefsTextNodes) {
// Skip text nodes - observers don't work on them
// 跳过文本节点 - 观察者无法作用于它们
if (child.tag === HostText) {
return false;
}
}
const instance = getInstanceFromHostFiber<Instance>(child);
observer.observe(instance);
return false;
}

FragmentInstance.prototype.unobserveUsing = function (
this: FragmentInstanceType,
observer: IntersectionObserver | ResizeObserver,
): void {
const observers = this._observers;
if (observers === null || !observers.has(observer)) {
if (__DEV__) {
console.error(
'You are calling unobserveUsing() with an observer that is not being observed with this fragment ' +
'instance. First attach the observer with observeUsing()',
);
}
} else {
observers.delete(observer);
traverseFragmentInstance(this._fragmentFiber, unobserveChild, observer);
}
};
function unobserveChild(
child: Fiber,
observer: IntersectionObserver | ResizeObserver,
) {
if (enableFragmentRefsTextNodes) {
// Skip text nodes - they were never observed
// 跳过文本节点 - 它们从未被观察到
if (child.tag === HostText) {
return false;
}
}
const instance = getInstanceFromHostFiber<Instance>(child);
observer.unobserve(instance);
return false;
}
FragmentInstance.prototype.getClientRects = function (
this: FragmentInstanceType,
): Array<DOMRect> {
const rects: Array<DOMRect> = [];
traverseFragmentInstance(this._fragmentFiber, collectClientRects, rects);
return rects;
};
function collectClientRects(child: Fiber, rects: Array<DOMRect>): boolean {
if (enableFragmentRefsTextNodes && child.tag === HostText) {
const textNode: Text = child.stateNode;
const range = textNode.ownerDocument.createRange();
range.selectNodeContents(textNode);
rects.push.apply(rects, range.getClientRects());
} else {
const instance = getInstanceFromHostFiber<Instance>(child);
rects.push.apply(rects, instance.getClientRects());
}
return false;
}
FragmentInstance.prototype.getRootNode = function (
this: FragmentInstanceType,
getRootNodeOptions?: { composed: boolean },
): Document | ShadowRoot | FragmentInstanceType {
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
if (parentHostFiber === null) {
return this;
}
const parentHostInstance =
getInstanceFromHostFiber<Instance>(parentHostFiber);
const rootNode = parentHostInstance.getRootNode(getRootNodeOptions) as
| Document
| ShadowRoot;
return rootNode;
};
FragmentInstance.prototype.compareDocumentPosition = function (
this: FragmentInstanceType,
otherNode: Instance,
): number {
const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber);
if (parentHostFiber === null) {
return Node.DOCUMENT_POSITION_DISCONNECTED;
}
const children: Array<Fiber> = [];
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
const parentHostInstance =
getInstanceFromHostFiber<Instance>(parentHostFiber);

if (children.length === 0) {
return compareDocumentPositionForEmptyFragment(
this._fragmentFiber,
parentHostInstance,
otherNode,
getInstanceFromHostFiber,
);
}

const firstNode = getInstanceFromHostFiber<Instance>(children[0]);
const lastNode = getInstanceFromHostFiber<Instance>(
children[children.length - 1],
);

// If the fragment has been portaled into another host instance, we need to
// our best guess is to use the parent of the child instance, rather than
// the fiber tree host parent.
// 如果片段已被传送到另一个宿主实例,我们需要我们最好的猜测是使用子实例的父级,而不是
// fiber 树的宿主父级。
const parentHostInstanceFromDOM = fiberIsPortaledIntoHost(this._fragmentFiber)
? (firstNode.parentElement as ?Instance)
: parentHostInstance;

if (parentHostInstanceFromDOM == null) {
return Node.DOCUMENT_POSITION_DISCONNECTED;
}

// Check if first and last node are actually in the expected document position
// before relying on them as source of truth for other contained node
// 在依赖首尾节点作为其他包含节点的依据之前,先检查它们是否确实在预期的文档位置
const firstNodeIsContained =
parentHostInstanceFromDOM.compareDocumentPosition(firstNode) &
Node.DOCUMENT_POSITION_CONTAINED_BY;
const lastNodeIsContained =
parentHostInstanceFromDOM.compareDocumentPosition(lastNode) &
Node.DOCUMENT_POSITION_CONTAINED_BY;
const firstResult = firstNode.compareDocumentPosition(otherNode);
const lastResult = lastNode.compareDocumentPosition(otherNode);

const otherNodeIsFirstOrLastChild =
(firstNodeIsContained && firstNode === otherNode) ||
(lastNodeIsContained && lastNode === otherNode);
const otherNodeIsFirstOrLastChildDisconnected =
(!firstNodeIsContained && firstNode === otherNode) ||
(!lastNodeIsContained && lastNode === otherNode);
const otherNodeIsWithinFirstOrLastChild =
firstResult & Node.DOCUMENT_POSITION_CONTAINED_BY ||
lastResult & Node.DOCUMENT_POSITION_CONTAINED_BY;
const otherNodeIsBetweenFirstAndLastChildren =
firstNodeIsContained &&
lastNodeIsContained &&
firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
lastResult & Node.DOCUMENT_POSITION_PRECEDING;

let result = Node.DOCUMENT_POSITION_DISCONNECTED;
if (
otherNodeIsFirstOrLastChild ||
otherNodeIsWithinFirstOrLastChild ||
otherNodeIsBetweenFirstAndLastChildren
) {
result = Node.DOCUMENT_POSITION_CONTAINED_BY;
} else if (otherNodeIsFirstOrLastChildDisconnected) {
// otherNode has been portaled into another container
// otherNode 已经被传送到另一个容器中
result = Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
} else {
result = firstResult;
}

if (
result & Node.DOCUMENT_POSITION_DISCONNECTED ||
result & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
) {
return result;
}

// Now that we have the result from the DOM API, we double check it matches
// the state of the React tree. If it doesn't, we have a case of portaled or
// otherwise injected elements and we return DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC.
// 现在我们已经从 DOM API 得到了结果,我们会再次检查它是否与 React 树的状态匹配。
// 如果不匹配,则说明存在通过 portal 或其他方式注入的元素,我们会返回 DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC。
const documentPositionMatchesFiberPosition =
validateDocumentPositionWithFiberTree(
result,
this._fragmentFiber,
children[0],
children[children.length - 1],
otherNode,
);
if (documentPositionMatchesFiberPosition) {
return result;
}
return Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
};

function validateDocumentPositionWithFiberTree(
documentPosition: number,
fragmentFiber: Fiber,
precedingBoundaryFiber: Fiber,
followingBoundaryFiber: Fiber,
otherNode: Instance,
): boolean {
const otherFiber = getClosestInstanceFromNode(otherNode);
if (documentPosition & Node.DOCUMENT_POSITION_CONTAINED_BY) {
return (
!!otherFiber && isFiberContainedByFragment(otherFiber, fragmentFiber)
);
}
if (documentPosition & Node.DOCUMENT_POSITION_CONTAINS) {
if (otherFiber === null) {
// otherFiber could be null if its the document or body element
// 如果是文档或 body 元素,otherFiber 可能为 null
const ownerDocument = otherNode.ownerDocument;
return otherNode === ownerDocument || otherNode === ownerDocument.body;
}
return isFragmentContainedByFiber(fragmentFiber, otherFiber);
}
if (documentPosition & Node.DOCUMENT_POSITION_PRECEDING) {
return (
!!otherFiber &&
(otherFiber === precedingBoundaryFiber ||
isFiberPreceding(precedingBoundaryFiber, otherFiber))
);
}
if (documentPosition & Node.DOCUMENT_POSITION_FOLLOWING) {
return (
!!otherFiber &&
(otherFiber === followingBoundaryFiber ||
isFiberFollowing(followingBoundaryFiber, otherFiber))
);
}

return false;
}

if (enableFragmentRefsScrollIntoView) {
FragmentInstance.prototype.scrollIntoView = function (
this: FragmentInstanceType,
alignToTop?: boolean,
): void {
if (typeof alignToTop === 'object') {
throw new Error(
'FragmentInstance.scrollIntoView() does not support ' +
'scrollIntoViewOptions. Use the alignToTop boolean instead.',
);
}
// First, get the children nodes
// 首先,获取子节点
const children: Array<Fiber> = [];
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);

const resolvedAlignToTop = alignToTop !== false;

// If there are no children, we can use the parent and siblings to determine a position
// 如果没有子元素,我们可以使用父元素和兄弟元素来确定位置
if (children.length === 0) {
const hostSiblings = getFragmentInstanceSiblings(this._fragmentFiber);
const targetFiber = resolvedAlignToTop
? hostSiblings[1] ||
hostSiblings[0] ||
getFragmentParentHostFiber(this._fragmentFiber)
: hostSiblings[0] || hostSiblings[1];

if (targetFiber === null) {
if (__DEV__) {
console.warn(
'You are attempting to scroll a FragmentInstance that has no ' +
'children, siblings, or parent. No scroll was performed.',
);
}
return;
}
const target = getInstanceFromHostFiber<Instance>(targetFiber);
target.scrollIntoView(alignToTop);
return;
}

let i = resolvedAlignToTop ? children.length - 1 : 0;
while (i !== (resolvedAlignToTop ? -1 : children.length)) {
const child = children[i];
// For text nodes, use Range API to scroll to their position
// 对于文本节点,使用 Range API 滚动到它们的位置
if (enableFragmentRefsTextNodes && child.tag === HostText) {
const textNode: Text = child.stateNode;
const range = textNode.ownerDocument.createRange();
range.selectNodeContents(textNode);
const rect = range.getBoundingClientRect();
const scrollY = resolvedAlignToTop
? window.scrollY + rect.top
: window.scrollY + rect.bottom - window.innerHeight;
window.scrollTo(window.scrollX + rect.left, scrollY);
i += resolvedAlignToTop ? -1 : 1;
continue;
}
const instance = getInstanceFromHostFiber<Instance>(child);
instance.scrollIntoView(alignToTop);
i += resolvedAlignToTop ? -1 : 1;
}
};
}

26. 将 Fragment 句柄添加到 Fiber

备注
function addFragmentHandleToFiber(
child: Fiber,
fragmentInstance: FragmentInstanceType,
): boolean {
if (enableFragmentRefsInstanceHandles) {
const instance =
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
if (instance != null) {
addFragmentHandleToInstance(instance, fragmentInstance);
}
}
return false;
}

27. 将 Fragment 句柄添加到实例

备注
function addFragmentHandleToInstance(
instance: InstanceWithFragmentHandles,
fragmentInstance: FragmentInstanceType,
): void {
if (enableFragmentRefsInstanceHandles) {
if (instance.reactFragments == null) {
instance.reactFragments = new Set();
}
instance.reactFragments.add(fragmentInstance);
}
}

28. 少量清空容器

备注
function clearContainerSparingly(container: Node) {
let node;
let nextNode: ?Node = container.firstChild;
if (nextNode && nextNode.nodeType === DOCUMENT_TYPE_NODE) {
nextNode = nextNode.nextSibling;
}
while (nextNode) {
node = nextNode;
nextNode = nextNode.nextSibling;
switch (node.nodeName) {
case 'HTML':
case 'HEAD':
case 'BODY': {
const element: Element = node as any;
clearContainerSparingly(element);
// If these singleton instances had previously been rendered with React they
// may still hold on to references to the previous fiber tree. We detatch them
// prospectively to reset them to a baseline starting state since we cannot create
// new instances.
// 如果这些单例实例之前已经用 React 渲染过,它们可能仍然保留对之前 fiber 树的
// 引用。我们会主动将它们分离,以将它们重置为基础的初始状态,因为我们无法创建新的
// 实例。
detachDeletedInstance(element);
continue;
}
// Script tags are retained to avoid an edge case bug. Normally scripts will execute if they
// are ever inserted into the DOM. However when streaming if a script tag is opened but not
// yet closed some browsers create and insert the script DOM Node but the script cannot execute
// yet until the closing tag is parsed. If something causes React to call clearContainer while
// this DOM node is in the document but not yet executable the DOM node will be removed from the
// document and when the script closing tag comes in the script will not end up running. This seems
// to happen in Chrome/Firefox but not Safari at the moment though this is not necessarily specified
// behavior so it could change in future versions of browsers. While leaving all scripts is broader
// than strictly necessary this is the least amount of additional code to avoid this breaking
// edge case.
//
// Style tags are retained because they may likely come from 3rd party scripts and extensions
// 保留脚本标签以避免一个边缘情况的错误。通常,如果脚本被插入到 DOM 中,它们会执
// 行。然而,在流式处理时,如果一个脚本标签被打开但尚未关闭,一些浏览器会创建并插入
// 脚本的 DOM 节点,但脚本在关闭标签被解析之前无法执行。如果有东西导致 React 在这
// 个 DOM 节点在文档中但尚不可执行时调用 clearContainer,该 DOM 节点会从文档中
// 移除,当脚本关闭标签到来时,脚本就不会执行。这似乎目前在 Chrome/Firefox 中会
// 发生,但 Safari 不会,尽管这并不是必然规定的行为,因此未来浏览器版本可能会改
// 变。虽然保留所有脚本比严格需要的更宽泛,但这是避免这个破坏性边缘情况的最少额外代
// 码。

// 保留样式标签是因为它们很可能来自第三方脚本和扩展。
case 'SCRIPT':
case 'STYLE': {
continue;
}
// Stylesheet tags are retained because they may likely come from 3rd party scripts and extensions
// 保留样式表标签,因为它们很可能来自第三方脚本和扩展
case 'LINK': {
if (
(node as any as HTMLLinkElement).rel.toLowerCase() === 'stylesheet'
) {
continue;
}
}
}
container.removeChild(node);
}
return;
}

29. 清理头

function clearHead(head: Element): void {
let node = head.firstChild;
while (node) {
const nextNode = node.nextSibling;
const nodeName = node.nodeName;
if (
isMarkedHoistable(node) ||
nodeName === 'SCRIPT' ||
nodeName === 'STYLE' ||
(nodeName === 'LINK' &&
(node as any as HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
) {
// retain these nodes
// 保留这些节点
} else {
head.removeChild(node);
}
node = nextNode;
}
return;
}

30. 可以水合水化边界

function canHydrateHydrationBoundary(
instance: HydratableInstance,
inRootOrSingleton: boolean,
): null | SuspenseInstance | ActivityInstance {
while (instance.nodeType !== COMMENT_NODE) {
if (
instance.nodeType === ELEMENT_NODE &&
instance.nodeName === 'INPUT' &&
(instance as any).type === 'hidden'
) {
// If we have extra hidden inputs, we don't mismatch. This allows us to
// embed extra form data in the original form.
// 如果我们有额外的隐藏输入,就不会出错。这允许我们在原始表单中嵌入额外的表单数据
} else if (!inRootOrSingleton) {
return null;
}
const nextInstance = getNextHydratableSibling(instance);
if (nextInstance === null) {
return null;
}
instance = nextInstance;
}
// This has now been refined to a hydration boundary node.
// 这现在已经被优化为一个水合边界节点。
return instance as any;
}

31. 获取下一个可水化对象

function getNextHydratable(node: ?Node) {
// Skip non-hydratable nodes.
// 跳过不可水化的节点。
for (; node != null; node = (node as any as Node).nextSibling) {
const nodeType = node.nodeType;
if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
break;
}
if (nodeType === COMMENT_NODE) {
const data = (node as any).data;
if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === ACTIVITY_START_DATA ||
data === FORM_STATE_IS_MATCHING ||
data === FORM_STATE_IS_NOT_MATCHING
) {
break;
}
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
return null;
}
}
}
return node as any;
}

32. 在水合边界之后获取下一个可水合实例

function getNextHydratableInstanceAfterHydrationBoundary(
hydrationInstance: SuspenseInstance | ActivityInstance,
): null | HydratableInstance {
let node = hydrationInstance.nextSibling;
// Skip past all nodes within this suspense boundary.
// There might be nested nodes so we need to keep track of how
// deep we are and only break out when we're back on top.
// 跳过此 suspense 边界内的所有节点。
// 可能存在嵌套节点,因此我们需要跟踪我们的深度
// 只有当我们回到最顶层时才停止。
let depth = 0;
while (node) {
if (node.nodeType === COMMENT_NODE) {
const data = (node as any).data as string;
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
if (depth === 0) {
return getNextHydratableSibling(node as any);
} else {
depth--;
}
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === ACTIVITY_START_DATA
) {
depth++;
}
}
node = node.nextSibling;
}
// TODO: Warn, we didn't find the end comment boundary.
// 待办:警告,我们没有找到结束注释边界。
return null;
}

33. 获取当前资源根

function getCurrentResourceRoot(): null | HoistableRoot {
const currentContainer = getCurrentRootHostContainer();
return currentContainer ? getHoistableRoot(currentContainer) : null;
}

34. 从根获取文档

function getDocumentFromRoot(root: HoistableRoot): Document {
return root.ownerDocument || root;
}

35. 同步刷新工作

备注
function flushSyncWork() {
if (disableLegacyMode) {
const previousWasRendering = previousDispatcher.f(); /* flushSyncWork */
const wasRendering = flushSyncWorkOnAllRoots();
// Since multiple dispatchers can flush sync work during a single flushSync call
// we need to return true if any of them were rendering.
// 由于在一次 flushSync 调用中,多个调度器可能会刷新同步工作
// 如果其中任何一个正在渲染,我们就需要返回 true。
return previousWasRendering || wasRendering;
} else {
throw new Error(
'flushSyncWork should not be called from builds that support legacy mode. This is a bug in React.',
);
}
}

36. 请求表单重置

备注
function requestFormReset(form: HTMLFormElement) {
const formInst = getInstanceFromNodeDOMTree(form);
if (
formInst !== null &&
formInst.tag === HostComponent &&
formInst.type === 'form'
) {
requestFormResetOnFiber(formInst);
} else {
// This form was either not rendered by this React renderer (or it's an
// invalid type). Try the next one.
//
// The last implementation in the sequence will throw an error.
// 这个表单要么没有被这个 React 渲染器渲染(或者类型无效)。
// 尝试下一个。
//
// 序列中的最后一个实现将会抛出一个错误。
previousDispatcher.r(/* requestFormReset */ form);
}
}

37. 获取全局文档

function getGlobalDocument(): ?Document {
return globalDocument;
}

38. 预连接为

备注
function preconnectAs(
rel: 'preconnect' | 'dns-prefetch',
href: string,
crossOrigin: ?CrossOriginEnum,
) {
const ownerDocument = getGlobalDocument();
if (ownerDocument && typeof href === 'string' && href) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
if (typeof crossOrigin === 'string') {
key += `[crossorigin="${crossOrigin}"]`;
}
if (!preconnectsSet.has(key)) {
preconnectsSet.add(key);

const preconnectProps = { rel, crossOrigin, href };
if (null === ownerDocument.querySelector(key)) {
const instance = ownerDocument.createElement('link');
setInitialProperties(instance, 'link', preconnectProps);
markNodeAsHoistable(instance);
(ownerDocument.head as any).appendChild(instance);
}
}
}
}

39. 预取DNS

function prefetchDNS(href: string) {
previousDispatcher.D(/* prefetchDNS */ href);
preconnectAs('dns-prefetch', href, null);
}

40. 预连接

function preconnect(href: string, crossOrigin?: ?CrossOriginEnum) {
previousDispatcher.C(/* preconnect */ href, crossOrigin);
preconnectAs('preconnect', href, crossOrigin);
}

41. 预加载

备注
function preload(href: string, as: string, options?: ?PreloadImplOptions) {
previousDispatcher.L(/* preload */ href, as, options);
const ownerDocument = getGlobalDocument();
if (ownerDocument && href && as) {
let preloadSelector = `link[rel="preload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes(
as,
)}"]`;
if (as === 'image') {
if (options && options.imageSrcSet) {
preloadSelector += `[imagesrcset="${escapeSelectorAttributeValueInsideDoubleQuotes(
options.imageSrcSet,
)}"]`;
if (typeof options.imageSizes === 'string') {
preloadSelector += `[imagesizes="${escapeSelectorAttributeValueInsideDoubleQuotes(
options.imageSizes,
)}"]`;
}
} else {
preloadSelector += `[href="${escapeSelectorAttributeValueInsideDoubleQuotes(
href,
)}"]`;
}
} else {
preloadSelector += `[href="${escapeSelectorAttributeValueInsideDoubleQuotes(
href,
)}"]`;
}
// Some preloads are keyed under their selector. This happens when the preload is for
// an arbitrary type. Other preloads are keyed under the resource key they represent a preload for.
// Here we figure out which key to use to determine if we have a preload already.
// 一些预加载项是根据它们的选择器进行键控的。当预加载项是针对任意类型时,会发生这种情况。其他预加载项是根据它们所代表
// 的资源键进行键控的。这里我们确定使用哪个键来判断我们是否已经有了预加载项。
let key = preloadSelector;
switch (as) {
case 'style':
key = getStyleKey(href);
break;
case 'script':
key = getScriptKey(href);
break;
}
if (!preloadPropsMap.has(key)) {
const preloadProps = Object.assign(
{
rel: 'preload',
// There is a bug in Safari where imageSrcSet is not respected on preload links
// so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
// This harms older browers that do not support imageSrcSet by making their preloads not work
// but this population is shrinking fast and is already small so we accept this tradeoff.
// 在 Safari 中有一个 bug,会导致 preload 链接的 imageSrcSet 不被使用
// 因此如果我们有 imageSrcSet,这里就省略 href,因为 Safari 会加载错误的图片。
// 这会影响那些不支持 imageSrcSet 的旧浏览器,使它们的预加载无法正常工作
// 但这部分用户正在快速减少,数量已经很少,所以我们接受这个权衡。
href:
as === 'image' && options && options.imageSrcSet ? undefined : href,
as,
} as PreloadProps,
options,
);
preloadPropsMap.set(key, preloadProps);

if (null === ownerDocument.querySelector(preloadSelector)) {
if (
as === 'style' &&
ownerDocument.querySelector(getStylesheetSelectorFromKey(key))
) {
// We already have a stylesheet for this key. We don't need to preload it.
// 我们已经有这个 key 对应的样式表了。我们不需要预加载它。
return;
} else if (
as === 'script' &&
ownerDocument.querySelector(getScriptSelectorFromKey(key))
) {
// We already have a stylesheet for this key. We don't need to preload it.
// 我们已经有这个 key 对应的样式表了。我们不需要预加载它。
return;
}
const instance = ownerDocument.createElement('link');
setInitialProperties(instance, 'link', preloadProps);
markNodeAsHoistable(instance);
(ownerDocument.head as any).appendChild(instance);
}
}
}
}

42. 预加载模块

备注
function preloadModule(href: string, options?: ?PreloadModuleImplOptions) {
previousDispatcher.m(/* preloadModule */ href, options);
const ownerDocument = getGlobalDocument();
if (ownerDocument && href) {
const as =
options && typeof options.as === 'string' ? options.as : 'script';
const preloadSelector = `link[rel="modulepreload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes(
as,
)}"][href="${escapeSelectorAttributeValueInsideDoubleQuotes(href)}"]`;
// Some preloads are keyed under their selector. This happens when the preload is for
// an arbitrary type. Other preloads are keyed under the resource key they represent a preload for.
// Here we figure out which key to use to determine if we have a preload already.
// 一些预加载项是根据它们的选择器进行键控的。当预加载项是针对任意类型时,会发生这种情况。其他预加载项是根据它们所代表
// 的资源键进行键控的。这里我们确定使用哪个键来判断是否已经有预加载项。
let key = preloadSelector;
switch (as) {
case 'audioworklet':
case 'paintworklet':
case 'serviceworker':
case 'sharedworker':
case 'worker':
case 'script': {
key = getScriptKey(href);
break;
}
}

if (!preloadPropsMap.has(key)) {
const props: PreloadModuleProps = Object.assign(
{
rel: 'modulepreload',
href,
} as PreloadModuleProps,
options,
);
preloadPropsMap.set(key, props);

if (null === ownerDocument.querySelector(preloadSelector)) {
switch (as) {
case 'audioworklet':
case 'paintworklet':
case 'serviceworker':
case 'sharedworker':
case 'worker':
case 'script': {
if (ownerDocument.querySelector(getScriptSelectorFromKey(key))) {
return;
}
}
}
const instance = ownerDocument.createElement('link');
setInitialProperties(instance, 'link', props);
markNodeAsHoistable(instance);
(ownerDocument.head as any).appendChild(instance);
}
}
}
}

43. 预初始化样式

备注
function preinitStyle(
href: string,
precedence: ?string,
options?: ?PreinitStyleOptions,
) {
previousDispatcher.S(/* preinitStyle */ href, precedence, options);

const ownerDocument = getGlobalDocument();
if (ownerDocument && href) {
const styles = getResourcesFromRoot(ownerDocument).hoistableStyles;

const key = getStyleKey(href);
precedence = precedence || 'default';

// Check if this resource already exists
// 检查此资源是否已存在
let resource = styles.get(key);
if (resource) {
// We can early return. The resource exists and there is nothing
// more to do
// 我们可以提前返回。资源已存在,没有其他操作需要执行
return;
}

const state = {
loading: NotLoaded,
preload: null,
};

// Attempt to hydrate instance from DOM
// 尝试从 DOM 中挂载实例
let instance: null | Instance = ownerDocument.querySelector(
getStylesheetSelectorFromKey(key),
);
if (instance) {
state.loading = Loaded | Inserted;
} else {
// Construct a new instance and insert it
// 构建一个新实例并插入它
const stylesheetProps = Object.assign(
{
rel: 'stylesheet',
href,
'data-precedence': precedence,
} as StylesheetProps,
options,
);
const preloadProps = preloadPropsMap.get(key);
if (preloadProps) {
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
}
const link = (instance = ownerDocument.createElement('link'));
markNodeAsHoistable(link);
setInitialProperties(link, 'link', stylesheetProps);

(link as any)._p = new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = reject;
});
link.addEventListener('load', () => {
state.loading |= Loaded;
});
link.addEventListener('error', () => {
state.loading |= Errored;
});

state.loading |= Inserted;
insertStylesheet(instance, precedence, ownerDocument);
}

// Construct a Resource and cache it
// 构建一个资源并缓存它
resource = {
type: 'stylesheet',
instance,
count: 1,
state,
};
styles.set(key, resource);
return;
}
}

44. 预初始化脚本

备注
function preinitScript(src: string, options?: ?PreinitScriptOptions) {
previousDispatcher.X(/* preinitScript */ src, options);

const ownerDocument = getGlobalDocument();
if (ownerDocument && src) {
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;

const key = getScriptKey(src);

// Check if this resource already exists
// 检查此资源是否已存在
let resource = scripts.get(key);
if (resource) {
// We can early return. The resource exists and there is nothing
// more to do
// 我们可以提前返回。资源已存在,没有其他操作需要执行
return;
}

// Attempt to hydrate instance from DOM
// 尝试从 DOM 中挂载实例
let instance: null | Instance = ownerDocument.querySelector(
getScriptSelectorFromKey(key),
);
if (!instance) {
// Construct a new instance and insert it
// 构建一个新实例并插入它
const scriptProps = Object.assign(
{
src,
async: true,
} as ScriptProps,
options,
);
// Adopt certain preload props
// 采用某些预加载属性
const preloadProps = preloadPropsMap.get(key);
if (preloadProps) {
adoptPreloadPropsForScript(scriptProps, preloadProps);
}
instance = ownerDocument.createElement('script');
markNodeAsHoistable(instance);
setInitialProperties(instance, 'link', scriptProps);
(ownerDocument.head as any).appendChild(instance);
}

// Construct a Resource and cache it
// 构建一个资源并缓存它
resource = {
type: 'script',
instance,
count: 1,
state: null,
};
scripts.set(key, resource);
return;
}
}

45. 预初始化模块脚本

备注
function preinitModuleScript(
src: string,
options?: ?PreinitModuleScriptOptions,
) {
previousDispatcher.M(/* preinitModuleScript */ src, options);

const ownerDocument = getGlobalDocument();
if (ownerDocument && src) {
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;

const key = getScriptKey(src);

// Check if this resource already exists
// 检查此资源是否已存在
let resource = scripts.get(key);
if (resource) {
// We can early return. The resource exists and there is nothing
// more to do
// 我们可以提前返回。资源已存在,没有其他操作需要执行
return;
}

// Attempt to hydrate instance from DOM
// 尝试从 DOM 中挂载实例
let instance: null | Instance = ownerDocument.querySelector(
getScriptSelectorFromKey(key),
);
if (!instance) {
// Construct a new instance and insert it
// 构建一个新实例并插入它
const scriptProps = Object.assign(
{
src,
async: true,
type: 'module',
} as ScriptProps,
options,
);
// Adopt certain preload props
// 采用某些预加载属性
const preloadProps = preloadPropsMap.get(key);
if (preloadProps) {
adoptPreloadPropsForScript(scriptProps, preloadProps);
}
instance = ownerDocument.createElement('script');
markNodeAsHoistable(instance);
setInitialProperties(instance, 'link', scriptProps);
(ownerDocument.head as any).appendChild(instance);
}

// Construct a Resource and cache it
// 构建一个资源并缓存它
resource = {
type: 'script',
instance,
count: 1,
state: null,
};
scripts.set(key, resource);
return;
}
}

46. 描述资源链接错误(开发)

function describeLinkForResourceErrorDEV(props: any) {
if (__DEV__) {
let describedProps = 0;

let description = '<link';
if (typeof props.rel === 'string') {
describedProps++;
description += ` rel="${props.rel}"`;
} else if (hasOwnProperty.call(props, 'rel')) {
describedProps++;
description += ` rel="${
props.rel === null ? 'null' : 'invalid type ' + typeof props.rel
}"`;
}
if (typeof props.href === 'string') {
describedProps++;
description += ` href="${props.href}"`;
} else if (hasOwnProperty.call(props, 'href')) {
describedProps++;
description += ` href="${
props.href === null ? 'null' : 'invalid type ' + typeof props.href
}"`;
}
if (typeof props.precedence === 'string') {
describedProps++;
description += ` precedence="${props.precedence}"`;
} else if (hasOwnProperty.call(props, 'precedence')) {
describedProps++;
description += ` precedence={${
props.precedence === null
? 'null'
: 'invalid type ' + typeof props.precedence
}}`;
}
if (Object.getOwnPropertyNames(props).length > describedProps) {
description += ' ...';
}
description += ' />';
return description;
}
return '';
}

47. 从原始属性获取样式标签属性

function styleTagPropsFromRawProps(
rawProps: StyleTagQualifyingProps,
): StyleTagProps {
return {
...rawProps,
'data-href': rawProps.href,
'data-precedence': rawProps.precedence,
href: null,
precedence: null,
};
}

48. 获取样式键

备注
function getStyleKey(href: string) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
return `href="${limitedEscapedHref}"`;
}

49. 获取样式标签选择器

备注
function getStyleTagSelector(href: string) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
return `style[data-href~="${limitedEscapedHref}"]`;
}

50. 从键获取样式表选择器

function getStylesheetSelectorFromKey(key: string) {
return `link[rel="stylesheet"][${key}]`;
}

51. 从键获取预加载样式表选择器

function getPreloadStylesheetSelectorFromKey(key: string) {
return `link[rel="preload"][as="style"][${key}]`;
}

52. 从原始属性生成样式表属性

function stylesheetPropsFromRawProps(
rawProps: StylesheetQualifyingProps,
): StylesheetProps {
return {
...rawProps,
'data-precedence': rawProps.precedence,
precedence: null,
};
}

53. 预加载样式表

备注
function preloadStylesheet(
ownerDocument: Document,
key: string,
preloadProps: PreloadProps,
state: StylesheetState,
) {
const preloadEl = ownerDocument.querySelector(
getPreloadStylesheetSelectorFromKey(key),
);
if (preloadEl) {
// If we find a preload already it was SSR'd and we won't have an actual
// loading state to track. For now we will just assume it is loaded
// 如果我们发现已经有预加载,那就是服务器端渲染了,我们将不会有实际的加载状态可以追踪。
// 暂时我们只是假设它已经加载完成
state.loading = Loaded;
} else {
const instance = ownerDocument.createElement('link');
state.preload = instance;
instance.addEventListener('load', () => (state.loading |= Loaded));
instance.addEventListener('error', () => (state.loading |= Errored));
setInitialProperties(instance, 'link', preloadProps);
markNodeAsHoistable(instance);
(ownerDocument.head as any).appendChild(instance);
}
}

54. 从样式表预加载属性

function preloadPropsFromStylesheet(
props: StylesheetQualifyingProps,
): PreloadProps {
return {
rel: 'preload',
as: 'style',
href: props.href,
crossOrigin: props.crossOrigin,
integrity: props.integrity,
media: props.media,
hrefLang: props.hrefLang,
referrerPolicy: props.referrerPolicy,
};
}

55. 获取脚本密钥

备注
function getScriptKey(src: string): string {
const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(src);
return `[src="${limitedEscapedSrc}"]`;
}

56. 从键获取脚本选择器

function getScriptSelectorFromKey(key: string): string {
return 'script[async]' + key;
}

57. 插入样式表

function insertStylesheet(
instance: Element,
precedence: string,
root: HoistableRoot,
): void {
const nodes = root.querySelectorAll(
'link[rel="stylesheet"][data-precedence],style[data-precedence]',
);
const last = nodes.length ? nodes[nodes.length - 1] : null;
let prior = last;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const nodePrecedence = node.dataset.precedence;
if (nodePrecedence === precedence) {
prior = node;
} else if (prior !== last) {
break;
}
}

if (prior) {
// We get the prior from the document so we know it is in the tree.
// We also know that links can't be the topmost Node so the parentNode
// must exist.
// 我们从文档中获取先前的节点,因此我们知道它在树中。
// 我们也知道链接不能是最顶层的节点,所以 parentNode 必须存在。
(prior.parentNode as any as Node).insertBefore(instance, prior.nextSibling);
} else {
const parent =
root.nodeType === DOCUMENT_NODE
? ((root as any as Document).head as any as Element)
: (root as any as ShadowRoot);
parent.insertBefore(instance, parent.firstChild);
}
}

58. 为样式表采用预加载属性

function adoptPreloadPropsForStylesheet(
stylesheetProps: StylesheetProps,
preloadProps: PreloadProps | PreloadModuleProps,
): void {
if (stylesheetProps.crossOrigin == null)
stylesheetProps.crossOrigin = preloadProps.crossOrigin;
if (stylesheetProps.referrerPolicy == null)
stylesheetProps.referrerPolicy = preloadProps.referrerPolicy;
if (stylesheetProps.title == null) stylesheetProps.title = preloadProps.title;
}

59. 为脚本采用预加载属性

function adoptPreloadPropsForScript(
scriptProps: ScriptProps,
preloadProps: PreloadProps | PreloadModuleProps,
): void {
if (scriptProps.crossOrigin == null)
scriptProps.crossOrigin = preloadProps.crossOrigin;
if (scriptProps.referrerPolicy == null)
scriptProps.referrerPolicy = preloadProps.referrerPolicy;
if (scriptProps.integrity == null)
scriptProps.integrity = preloadProps.integrity;
}

60. 可水化可提升缓存

function getHydratableHoistableCache(
type: HoistableTagType,
keyAttribute: string,
ownerDocument: Document,
): KeyedTagCache {
let cache: KeyedTagCache;
let caches: DocumentTagCaches;
if (tagCaches === null) {
cache = new Map();
caches = tagCaches = new Map();
caches.set(ownerDocument, cache);
} else {
caches = tagCaches;
const maybeCache = caches.get(ownerDocument);
if (!maybeCache) {
cache = new Map();
caches.set(ownerDocument, cache);
} else {
cache = maybeCache;
}
}

if (cache.has(type)) {
// We use type as a special key that signals that this cache has been seeded for this type
// 我们使用 type 作为一个特殊的键,用来标识此缓存已为该类型初始化
return cache;
}

// Mark this cache as seeded for this type
// 将此缓存标记为此类型的已种子
cache.set(type, null as any);

const nodes = ownerDocument.getElementsByTagName(type);
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (
!isOwnedInstance(node) &&
(type !== 'link' || node.getAttribute('rel') !== 'stylesheet') &&
node.namespaceURI !== SVG_NAMESPACE
) {
const nodeKey = node.getAttribute(keyAttribute) || '';
const key = type + nodeKey;
const existing = cache.get(key);
if (existing) {
existing.push(node);
} else {
cache.set(key, [node]);
}
}
}

return cache;
}

61. 估算图像字节数

function estimateImageBytes(instance: HTMLImageElement): number {
const width: number = instance.width || 100;
const height: number = instance.height || 100;
const pixelRatio: number =
typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;
const pixelsToDownload = width * height * pixelRatio;
const AVERAGE_BYTE_PER_PIXEL = 0.25;
return pixelsToDownload * AVERAGE_BYTE_PER_PIXEL;
}

62. 检查是否已完全解除暂停

function checkIfFullyUnsuspended(state: SuspendedState) {
if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) {
if (state.stylesheets) {
// If we haven't actually inserted the stylesheets yet we need to do so now before starting the commit.
// The reason we do this after everything else has finished is because we want to have all the stylesheets
// load synchronously right before mutating. Ideally the new styles will cause a single recalc only on the
// new tree. When we filled up stylesheets we only inlcuded stylesheets with matching media attributes so we
// wait for them to load before actually continuing. We expect this to increase the count above zero
// 如果我们还没有实际插入样式表,现在在开始提交之前需要这样做。我们之所以在其他所有操作完成后再做,是
// 因为我们希望在进行修改之前让所有样式表同步加载。理想情况下,新样式只会在新树上触发一次重计算。当我
// 们填充样式表时,我们只包含具有匹配媒体属性的样式表,因此在实际继续之前会等待它们加载完成。我们预计
// 这会使计数增加到零以上
insertSuspendedStylesheets(state, state.stylesheets);
} else if (state.unsuspend) {
const unsuspend = state.unsuspend;
state.unsuspend = null;
unsuspend();
}
}
}

63. 解除暂停

function onUnsuspend(this: SuspendedState) {
this.count--;
checkIfFullyUnsuspended(this);
}

64. 取消暂停图像

function onUnsuspendImg(this: SuspendedState) {
this.imgCount--;
checkIfFullyUnsuspended(this);
}

65. 插入挂起的样式表

function insertSuspendedStylesheets(
state: SuspendedState,
resources: Map<StylesheetResource, HoistableRoot>,
): void {
// We need to clear this out so we don't try to reinsert after the stylesheets have loaded
// 我们需要清理这个,这样在样式表加载后我们就不会尝试重新插入
state.stylesheets = null;

if (state.unsuspend === null) {
// The suspended commit was cancelled. We don't need to insert any stylesheets.
// 挂起的提交已被取消。我们不需要插入任何样式表。
return;
}

// Temporarily increment count. we don't want any synchronously loaded stylesheets to try to unsuspend
// before we finish inserting all stylesheets.
// 暂时增加计数。我们不希望任何同步加载的样式表在我们完成插入所有样式表之前尝试取消挂起。
state.count++;

precedencesByRoot = new Map();
resources.forEach(insertStylesheetIntoRoot, state);
precedencesByRoot = null as any;

// We can remove our temporary count and if we're still at zero we can unsuspend.
// If we are in the synchronous phase before deciding if the commit should suspend and this
// ends up hitting the unsuspend path it will just invoke the noop unsuspend.
// 我们可以移除临时计数,如果仍为零,我们可以取消挂起。
// 如果我们处于同步阶段,在决定提交是否应挂起之前,
// 并且这最终触发取消挂起路径,它只会调用无操作的取消挂起。
onUnsuspend.call(state);
}

66. 将样式表插入根元素

function insertStylesheetIntoRoot(
this: SuspendedState,
root: HoistableRoot,
resource: StylesheetResource,
map: Map<StylesheetResource, HoistableRoot>,
) {
if (resource.state.loading & Inserted) {
// This resource was inserted by another root committing. we don't need to insert it again
// 这个资源是由另一个根提交插入的,我们不需要再次插入它
return;
}

let last;
let precedences = precedencesByRoot.get(root);
if (!precedences) {
precedences = new Map();
precedencesByRoot.set(root, precedences);
const nodes = root.querySelectorAll(
'link[data-precedence],style[data-precedence]',
);
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (
node.nodeName === 'LINK' ||
// We omit style tags with media="not all" because they are not in the right position
// and will be hoisted by the Fizz runtime imminently.
// 我们省略 media="not all" 的样式标签,因为它们位置不对
// 并且会很快被 Fizz 运行时提升。
node.getAttribute('media') !== 'not all'
) {
precedences.set(node.dataset.precedence, node);
last = node;
}
}
if (last) {
precedences.set(LAST_PRECEDENCE, last);
}
} else {
last = precedences.get(LAST_PRECEDENCE);
}

// We only call this after we have constructed an instance so we assume it here
// 我们只在构建实例后才调用它,所以这里假设它已经存在
const instance: HTMLLinkElement = resource.instance as any;
// We will always have a precedence for stylesheet instances
// 我们将始终优先考虑样式表实例
const precedence: string = instance.getAttribute('data-precedence') as any;

const prior = precedences.get(precedence) || last;
if (prior === last) {
precedences.set(LAST_PRECEDENCE, instance);
}
precedences.set(precedence, instance);

this.count++;
const onComplete = onUnsuspend.bind(this);
instance.addEventListener('load', onComplete);
instance.addEventListener('error', onComplete);

if (prior) {
(prior.parentNode as any).insertBefore(instance, prior.nextSibling);
} else {
const parent =
root.nodeType === DOCUMENT_NODE
? ((root as any as Document).head as any as Element)
: (root as any as ShadowRoot);
parent.insertBefore(instance, parent.firstChild);
}
resource.state.loading |= Inserted;
}

幺肆四、转导

幺肆五、类型

1. 行参数

type RawProps = {
[string]: mixed;
};

2. 带片段句柄的实例

type InstanceWithFragmentHandles = Instance & {
reactFragments?: Set<FragmentInstanceType>;
};

3. 活动接口

declare class ActivityInterface extends Comment {}

4. 挂起界面实例

declare class SuspenseInterface extends Comment {
_reactRetry: void | (() => void);
}

5. 表单状态标记实例

type FormStateMarkerInstance = Comment;

6. 宿主环境生产

type HostContextProd = HostContextNamespace;

7. 宿主上下文命名空间

type HostContextNamespace = 0 | 1 | 2;

8. 视图过渡伪元素类型

interface ViewTransitionPseudoElementType extends mixin$Animatable {
_scope: HTMLElement;
_selector: string;
getComputedStyle(): CSSStyleDeclaration;
}

9. 自定义时间线

interface CustomTimeline {
currentTime: number;
animate(animation: Animation): void | (() => void);
}

10. 存储事件监听器

type StoredEventListener = {
type: string;
listener: EventListener;
optionsOrUseCapture: void | EventListenerOptionsOrUseCapture;
};

11. 矩形比例

type RectRatio = {
ratio: number;
rect: BoundingRect;
};

12. 可提升标签类型

// 可提升标签类型
type HoistableTagType = 'link' | 'meta' | 'title';
// 资源
type TResource<
T: 'stylesheet' | 'style' | 'script' | 'void',
S: null | {...},
> = {
type: T,
instance: null | Instance,
count: number,
state: S,
};
// 样式表资源
type StylesheetResource = TResource<'stylesheet', StylesheetState>;
// 样式标签资源
type StyleTagResource = TResource<'style', null>;
// 样式资源
type StyleResource = StyleTagResource | StylesheetResource;
// 脚本资源
type ScriptResource = TResource<'script', null>;
// 无效资源
type VoidResource = TResource<'void', null>;

13. 加载状态

type LoadingState = number;

14. 样式表状态

// 样式表状态
type StylesheetState = {
loading: LoadingState;
preload: null | HTMLLinkElement;
};

// 样式标签属性
type StyleTagProps = {
'data-href': string;
'data-precedence': string;
[string]: mixed;
};
// 样式表属性
type StylesheetProps = {
rel: 'stylesheet';
href: string;
'data-precedence': string;
[string]: mixed;
};
// 脚本属性
type ScriptProps = {
src: string;
async: true;
[string]: mixed;
};
// 预加载属性
type PreloadProps = {
rel: 'preload';
href: ?string;
[string]: mixed;
};
// 预加载模块属性
type PreloadModuleProps = {
rel: 'modulepreload';
href: string;
[string]: mixed;
};

15. 样式标签限定属性

// 样式标签限定属性
type StyleTagQualifyingProps = {
href: string;
precedence: string;
[s: string]: mixed;
};
// 样式表限定属性
type StylesheetQualifyingProps = {
rel: 'stylesheet';
href: string;
precedence: string;
[s: string]: mixed;
};

16. 键控标签缓存

// 键控标签缓存
type KeyedTagCache = Map<string, Array<Element>>;
// 文档标签缓存
type DocumentTagCaches = Map<Document, KeyedTagCache>;