1 )概述
2 )源码
定位到 packages/react-dom/src/client/ReactDOM.js#L576
function createPortal(
children: ReactNodeList,
container: DOMContainer,
key: ?string = null,
) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
// TODO: pass ReactDOM portal implementation as third argument
return ReactPortal.createPortal(children, container, null, key);
}
这里调用的是 ReactPortal.createPortal
, 进入
// packages/shared/ReactPortal.js#L14
export function createPortal(
children: ReactNodeList,
containerInfo: any,
// TODO: figure out the API for cross-renderer implementation.
implementation: any,
key: ?string = null,
): ReactPortal {
return {
// This tag allow us to uniquely identify this as a React Portal
$$typeof: REACT_PORTAL_TYPE,
key: key == null ? null : '' + key,
children,
containerInfo, // dom 挂载节点
implementation,
};
}
$$typeof
和 containerInfo
需要的挂载点对于 REACT_PORTAL_TYPE
类型的组件在 reconcile 时, 看下具体操作, 找到 reconcileSinglePortal
// packages/react-reconciler/src/ReactChildFiber.js#L1171
function reconcileSinglePortal(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
portal: ReactPortal,
expirationTime: ExpirationTime,
): Fiber {
const key = portal.key; // 当前的 key
let child = currentFirstChild;
// 如果 存在 child, 对比 child.key === key
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
// 关注这里,有 containerInfo 的对比,portal 需要关心渲染到的节点是否有变化
// 如果节点有变化,那么这个 portal 的渲染过程也会有变化
// 都符合,说明老节点都可以复用
if (
child.tag === HostPortal &&
child.stateNode.containerInfo === portal.containerInfo &&
child.stateNode.implementation === portal.implementation
) {
deleteRemainingChildren(returnFiber, child.sibling); // 删除其他节点
// 通过 useFiber 复用这个节点
const existing = useFiber(
child,
portal.children || [],
expirationTime,
);
existing.return = returnFiber;
return existing;
} else {
// key相同,但是不符合上述if, 没法复用,删除干净
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
// key 不同,删除当前节点
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 创建一个 portal 的 fiber 对象
const created = createFiberFromPortal(
portal,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
}
对于更新一个portal节点,进入 updatePortalComponent
// packages/react-reconciler/src/ReactFiberBeginWork.js#L1322
function updatePortalComponent(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
// 挂载点处理
pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
const nextChildren = workInProgress.pendingProps; // 这里 pendingProps 是 children
if (current === null) {
// Portals are special because we don't append the children during mount
// but at commit. Therefore we need to track insertions which the normal
// flow doesn't do during mount. This doesn't happen at the root because
// the root always starts with a "current" with a null child.
// TODO: Consider unifying this with how the root works.
workInProgress.child = reconcileChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
}
return workInProgress.child;
}