We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fragment 是一种特殊的组件,用于在不引入额外 DOM 元素的情况下,包裹多个子元素。使用 Fragment 可以让组件结构更加清晰,而不引入不必要的父级 DOM 节点,以免影响页面布局和性能。
上一节我们实现了单节点和多节点的 Diff 算法,现在我们就来处理节点为 Fragment 和嵌套数组的情况,主要分为以下三种情况:
// ReactElement const element = ( <> <div>1</div> <div>2</div> </> ); // 对应 DOM <div>1</div> <div>2</div> // JSX 转换结果 var element = jsxs(Fragment, { children: [ jsx("div", {children: "1"}) jsx("div", {children: "2"}) ] })
其中,<></> 是 Fragment 的简写,完整的写法是 <React.Fragment>。可以看到,在上面的例子中,element 是一个 type 为 Fragment 的 ReactElement,因此,在 Diff 算法中,需要考虑单一节点为 Fragment 的情况。
<></>
<React.Fragment>
element
type
ReactElement
先在 ReactSymbols.ts 文件中定义一下 REACT_FRAGMENT_TYPE,表示 Fragment 组件:
ReactSymbols.ts
REACT_FRAGMENT_TYPE
// packages/shared/ReactSymbols.ts // 表示 Fragment 组件,即 <React.Fragment> 或短语法 <></> 创建的 Fragment export const REACT_FRAGMENT_TYPE = supportSymbol ? Symbol.for('react.fragment') : 0xeacb;
接着在 reconcileChildFibers 函数中增加对顶层无 key 的 Fragment 情况的处理,以下是代码的主要逻辑:
reconcileChildFibers
判断是否为顶层的无 key 的 Fragment:
newChild
null
props.children
判断当前 Fiber 的类型:
reconcileChildrenArray
reconcileSingleElement
// packages/react-reconciler/src/childFiber.ts return function reconcileChildFibers( returnFiber: FiberNode, currentFiber: FiberNode | null, newChild?: any ) { // 判断 Fragment const isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null; if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children; } // 判断当前 fiber 的类型 // ReactElement 节点 if (typeof newChild == 'object' && newChild !== null) { // 处理多个 ReactElement 节点的情况 if (Array.isArray(newChild)) { return reconcileChildrenArray(returnFiber, currentFiber, newChild); } // 处理单个 ReactElement 节点的情况 switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( reconcileSingleElement(returnFiber, currentFiber, newChild) ); default: if (__DEV__) { console.warn('未实现的 reconcile 类型', newChild); } break; } } // ... };
// ReactElement const element = ( <ul> <> <li>1</li> <li>2</li> </> <li>3</li> <li>4</li> </ul> ); // 对应 DOM <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul> // JSX 转换结果 var element = jsxs("ul", { children: [ jsxs(Fragment, { children: [ jsx("li", {children: "1"}) jsx("li", {children: "2"}) ] }), jsx("li", {children: "3"}) jsx("li", {children: "4"}) ] })
这种情况中,若 newChild.children 为单个 Fragment 节点,则需要进入 reconcileSingleElement 方法进行处理;若 newChild.children 为数组类型,数组中的某一项为 Fragment,与其他组件同级,则需要进入 reconcileChildrenArray 方法进行处理。
newChild.children
我们先在 reconcileSingleElement 函数中增加对 Fragment 组件的处理:
// packages/react-reconciler/src/childFiber.ts // 处理单个 Element 节点的情况 // 对比 currentFiber 与 ReactElement,生成 workInProgress FiberNode function reconcileSingleElement( returnFiber: FiberNode, currentFiber: FiberNode | null, element: ReactElementType ) { // 组件的更新阶段 while (currentFiber !== null) { if (currentFiber.key === element.key) { if (element.$$typeof === REACT_ELEMENT_TYPE) { if (currentFiber.type === element.type) { // key 和 type 都相同,当前节点可以复用旧的 Fiber 节点 // 处理 Fragment 的情况 let props: Props = element.props; if (element.type === REACT_FRAGMENT_TYPE) { props = element.props.children; } const existing = useFiber(currentFiber, props); existing.return = returnFiber; // 剩下的兄弟节点标记删除 deleteRemainingChildren(returnFiber, currentFiber.sibling); return existing; } // ... // 创建新的 Fiber 节点 let fiber; if (element.type === REACT_FRAGMENT_TYPE) { fiber = createFiberFromFragment(element.props.children, element.key); } else { fiber = createFiberFromElement(element); } fiber.return = returnFiber; return fiber; }
如果可以复用旧的 Fiber 节点,对于 Fragment 类型的节点来说,需要将 element.props.children 作为 props 参数传给旧节点;
element.props.children
props
如果不能复用,对于 Fragment 类型的节点来说,则需要调用 createFiberFromFragment 函数,来创建新的 Fragment 类型的 Fiber 节点:
createFiberFromFragment
// packages/react-reconciler/src/fiber.ts export function createFiberFromFragment(elements: any[], key: Key): FiberNode { const fiber = new FiberNode(Fragment, elements, key); return fiber; }
这里要注意,我们在判断节点类型时,用到了 $$typeof 属性和 type 属性两个属性,这两个属性在 React 中有不同的含义: $$typeof 属性: $$typeof 属性是 React 内部用于标识元素类型的一个特殊属性。它通常用于区分不同类型的 React 元素,例如普通的 React 元素和 Fragment 元素。例如,$$typeof 的值为 Symbol.for('react.element') 表示一个普通的 React 元素,而值为 Symbol.for('react.fragment') 表示一个 Fragment 元素。 type 属性: type 属性表示 React 元素的类型。对于普通的 React 组件元素,type 是组件函数或类;对于 DOM 元素,type 是字符串(例如,div、span);对于 Fragment 元素,type 是特殊的 Fragment 标识。 以下是一个简单的例子,演示了 $$typeof 和 type 属性的不同: import React from 'react'; const MyComponent = () => { return ( <div> <span>Hello</span> </div> ); }; const MyFragment = () => { return ( <> <span>Fragment</span> </> ); }; const element = <MyComponent />; console.log(element.type); // 输出组件函数或类 console.log(element.$$typeof); // 输出 Symbol.for('react.element') const fragmentElement = <MyFragment />; console.log(fragmentElement.type); // 输出 Symbol.for('react.fragment') console.log(fragmentElement.$$typeof); // 输出 Symbol.for('react.element')
这里要注意,我们在判断节点类型时,用到了 $$typeof 属性和 type 属性两个属性,这两个属性在 React 中有不同的含义:
$$typeof
$$typeof 属性:
$$typeof 属性是 React 内部用于标识元素类型的一个特殊属性。它通常用于区分不同类型的 React 元素,例如普通的 React 元素和 Fragment 元素。例如,$$typeof 的值为 Symbol.for('react.element') 表示一个普通的 React 元素,而值为 Symbol.for('react.fragment') 表示一个 Fragment 元素。
Symbol.for('react.element')
Symbol.for('react.fragment')
type 属性:
type 属性表示 React 元素的类型。对于普通的 React 组件元素,type 是组件函数或类;对于 DOM 元素,type 是字符串(例如,div、span);对于 Fragment 元素,type 是特殊的 Fragment 标识。
div
span
以下是一个简单的例子,演示了 $$typeof 和 type 属性的不同:
import React from 'react'; const MyComponent = () => { return ( <div> <span>Hello</span> </div> ); }; const MyFragment = () => { return ( <> <span>Fragment</span> </> ); }; const element = <MyComponent />; console.log(element.type); // 输出组件函数或类 console.log(element.$$typeof); // 输出 Symbol.for('react.element') const fragmentElement = <MyFragment />; console.log(fragmentElement.type); // 输出 Symbol.for('react.fragment') console.log(fragmentElement.$$typeof); // 输出 Symbol.for('react.element')
接着我们来处理多节点 Diff 的情况,reconcileChildrenArray 方法调用了 updateFromMap 函数来遍历 newChild 数组,判断是否可复用,我们需要在其中增加逻辑,判断是否复用或者新建 Fragment:
updateFromMap
如果 element.type === REACT_FRAGMENT_TYPE,当前节点为 Fragment 类型,调用 updateFragment 函数,判断旧节点中是否有 Fragment 类型的节点,若没有就新建一个 Fragment 节点,有的话就直接复用旧节点。
element.type === REACT_FRAGMENT_TYPE
updateFragment
// packages/react-reconciler/src/childFiber.ts function updateFromMap( returnFiber: FiberNode, existingChildren: ExistingChildren, index: number, element: any ): FiberNode | null { const keyToUse = element.key !== null ? element.key : index.toString(); const before = existingChildren.get(keyToUse); // ReactElement if (typeof element === 'object' && element !== null) { switch (element.$$typeof) { case REACT_ELEMENT_TYPE: if (element.type === REACT_FRAGMENT_TYPE) { return updateFragment( returnFiber, before, element, keyToUse, existingChildren ); } // ... } } // ... } // 复用或新建 Fragment function updateFragment( returnFiber: FiberNode, current: FiberNode | undefined, elements: any[], key: Key, existingChildren: ExistingChildren ) { let fiber; if (!current || current.tag !== Fragment) { fiber = createFiberFromFragment(elements, key); } else { existingChildren.delete(key); fiber = useFiber(current, elements); } fiber.return = returnFiber; return fiber; }
// ReactElement const arr = [<li>1</li>, <li>2</li>]; const element = ( <ul> {arr} <li>3</li> <li>4</li> </ul> ); // 对应 DOM <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul> // JSX 转换结果 var arr = [ jsx("li", {children: "1"}), jsx("li", {children: "2"}) ] var element = jsxs("ul", { children: [ arr, jsx("li", {children: "3"}) jsx("li", {children: "4"}) ] })
这种情况中,newChild.children 是一个数组,数组中的某一项也是一个数组。由于newChild.children 是数组,所以会进入 reconcileChildrenArray 函数进行处理,因此我们需要在其中增加数组类型的判断,看是否可以复用 Fragment 类型的 Fiber 节点,逻辑和上面一样:
// packages/react-reconciler/src/childFiber.ts function updateFromMap( returnFiber: FiberNode, existingChildren: ExistingChildren, index: number, element: any ): FiberNode | null { const keyToUse = element.key !== null ? element.key : index.toString(); const before = existingChildren.get(keyToUse); // ... // 数组类型的 ReactElement,如:<ul>{[<li/>, <li/>]}</ul> if (Array.isArray(element)) { return updateFragment( returnFiber, before, element, keyToUse, existingChildren ); } return null; }
为了将 Fragment 类型的节点加入到 Reconciliation 阶段的更新和 Diff 流程中,我们需要在 beginWork 和 completeWork 两个函数加入对 Fragment 类型节点的处理逻辑。
beginWork
completeWork
在 beginWork 函数中,我们需要增加对 Fragment 类型节点的处理:
// packages/react-reconciler/src/beginWork.ts // 比较并返回子 FiberNode export const beginWork = (workInProgress: FiberNode) => { switch (workInProgress.tag) { // ... case Fragment: return updateFragment(workInProgress); // ... } }; function updateFragment(workInProgress: FiberNode) { const nextChildren = workInProgress.pendingProps; reconcileChildren(workInProgress, nextChildren); return workInProgress.child; }
在 completeWork 函数中,我们也需要增加对 Fragment 类型节点的处理,向上冒泡收集到的更新 flags:
// packages/react-reconciler/src/completeWork.ts // 生成更新计划,计算和收集更新 flags export const completeWork = (workInProgress: FiberNode) => { // ... switch (workInProgress.tag) { case HostRoot: case FunctionComponent: case Fragment: bubbleProperties(workInProgress); return null; // ... } };
增加了 Fragment 组件之后,在 Commit 阶段也需要做一些边界情况处理。例如删除节点时,需要考虑到 Fragment 组件可能会包含多个子节点,因此在删除节点时需要遍历处理其子节点。
commitDeletion 函数负责在协调过程中删除 Fiber 节点及其关联子树的操作,下面我们就来改造 commitDeletion 函数:
commitDeletion
初始化:
childToDelete
rootChildrenToDelete
递归遍历:
commitNestedUnmounts
onCommitUnmount
recordChildrenToDelete
遍历删除:
hostParent
removeChild
清理关联:
return
child
// 删除节点及其子树 const commitDeletion = (childToDelete: FiberNode) => { if (__DEV__) { console.log('执行 Deletion 操作', childToDelete); } // 子树的根节点 let rootChildrenToDelete: FiberNode[] = []; // 递归遍历子树 commitNestedUnmounts(childToDelete, (unmountFiber) => { switch (unmountFiber.tag) { case HostComponent: recordChildrenToDelete(rootChildrenToDelete, unmountFiber); // TODO 解绑ref return; case HostText: recordChildrenToDelete(rootChildrenToDelete, unmountFiber); return; case FunctionComponent: // TODO useEffect unmount return; default: if (__DEV__) { console.warn('未实现的 delete 类型', unmountFiber); } } }); // 移除 rootChildrenToDelete 的DOM if (rootChildrenToDelete.length !== 0) { // 找到待删除子树的根节点的 parent DOM const hostParent = getHostParent(childToDelete) as Container; rootChildrenToDelete.forEach((node) => { removeChild(node.stateNode, hostParent); }); } childToDelete.return = null; childToDelete.child = null; }; function recordChildrenToDelete( childrenToDelete: FiberNode[], unmountFiber: FiberNode ) { let lastOne = childrenToDelete[childrenToDelete.length - 1]; if (!lastOne) { childrenToDelete.push(unmountFiber); } else { let node = lastOne.sibling; while (node !== null) { if (unmountFiber == node) { childrenToDelete.push(unmountFiber); } node = node.sibling; } } }
因为我们使用 Fragment 组件时,是从 react 包引入的,如:import { Fragment } from 'react'; 或直接使用 <React.Fragment> 标签,因此还需要在 react 包中导出 Fragment 组件。
react
import { Fragment } from 'react';
在 jsx.ts 中导出 Fragment:
jsx.ts
// packages/react/src/jsx.ts import { REACT_FRAGMENT_TYPE } from 'shared/ReactSymbols'; export const Fragment = REACT_FRAGMENT_TYPE;
在 jsx-dev-runtime.ts 导出 Fragment:
jsx-dev-runtime.ts
// packages/react/jsx-dev-runtime.ts export { jsxDEV, Fragment } from './src/jsx';
至此,我们就实现了 Fragment,并将 Fragment 类型的组件加入到了 React 更新流程中。
相关代码可在 git tag v1.13 查看,地址:https://github.com/2xiao/my-react/tree/v1.13
git tag v1.13
The text was updated successfully, but these errors were encountered:
No branches or pull requests
Fragment 是一种特殊的组件,用于在不引入额外 DOM 元素的情况下,包裹多个子元素。使用 Fragment 可以让组件结构更加清晰,而不引入不必要的父级 DOM 节点,以免影响页面布局和性能。
1. Diff 算法支持 Fragment
上一节我们实现了单节点和多节点的 Diff 算法,现在我们就来处理节点为 Fragment 和嵌套数组的情况,主要分为以下三种情况:
1. Fragment 包裹其他组件
其中,
<></>
是 Fragment 的简写,完整的写法是<React.Fragment>
。可以看到,在上面的例子中,element
是一个type
为 Fragment 的ReactElement
,因此,在 Diff 算法中,需要考虑单一节点为 Fragment 的情况。先在
ReactSymbols.ts
文件中定义一下REACT_FRAGMENT_TYPE
,表示 Fragment 组件:接着在
reconcileChildFibers
函数中增加对顶层无 key 的 Fragment 情况的处理,以下是代码的主要逻辑:判断是否为顶层的无 key 的 Fragment:
newChild
是对象,且不为null
,且为顶层的 Fragment,且其 key 为null
,则为顶层的无 key 的 Fragment;newChild
重新赋值为 Fragment 的props.children
;判断当前 Fiber 的类型:
newChild
是 ReactElement 节点,则进一步判断其类型;newChild
是数组,说明存在多个子节点,调用reconcileChildrenArray
处理多节点的 Diff 逻辑。newChild
是单个 React 元素节点,调用reconcileSingleElement
处理单节点的 Diff 逻辑。newChild
是文本节点,则保持原逻辑,不用修改;2. Fragment 与其他组件同级
这种情况中,若
newChild.children
为单个 Fragment 节点,则需要进入reconcileSingleElement
方法进行处理;若newChild.children
为数组类型,数组中的某一项为 Fragment,与其他组件同级,则需要进入reconcileChildrenArray
方法进行处理。我们先在
reconcileSingleElement
函数中增加对 Fragment 组件的处理:如果可以复用旧的 Fiber 节点,对于 Fragment 类型的节点来说,需要将
element.props.children
作为props
参数传给旧节点;如果不能复用,对于 Fragment 类型的节点来说,则需要调用
createFiberFromFragment
函数,来创建新的 Fragment 类型的 Fiber 节点:接着我们来处理多节点 Diff 的情况,
reconcileChildrenArray
方法调用了updateFromMap
函数来遍历newChild
数组,判断是否可复用,我们需要在其中增加逻辑,判断是否复用或者新建 Fragment:如果
element.type === REACT_FRAGMENT_TYPE
,当前节点为 Fragment 类型,调用updateFragment
函数,判断旧节点中是否有 Fragment 类型的节点,若没有就新建一个 Fragment 节点,有的话就直接复用旧节点。3. 数组形式的 Fragment
这种情况中,
newChild.children
是一个数组,数组中的某一项也是一个数组。由于newChild.children
是数组,所以会进入reconcileChildrenArray
函数进行处理,因此我们需要在其中增加数组类型的判断,看是否可以复用 Fragment 类型的 Fiber 节点,逻辑和上面一样:2. 处理 Reconciliation 阶段
为了将 Fragment 类型的节点加入到 Reconciliation 阶段的更新和 Diff 流程中,我们需要在
beginWork
和completeWork
两个函数加入对 Fragment 类型节点的处理逻辑。1. 处理 beginWork
在
beginWork
函数中,我们需要增加对 Fragment 类型节点的处理:2. 处理 completeWork
在
completeWork
函数中,我们也需要增加对 Fragment 类型节点的处理,向上冒泡收集到的更新 flags:3. 处理 Commit 阶段
增加了 Fragment 组件之后,在 Commit 阶段也需要做一些边界情况处理。例如删除节点时,需要考虑到 Fragment 组件可能会包含多个子节点,因此在删除节点时需要遍历处理其子节点。
commitDeletion
函数负责在协调过程中删除 Fiber 节点及其关联子树的操作,下面我们就来改造commitDeletion
函数:初始化:
childToDelete
的参数,表示要删除的子树的根节点。rootChildrenToDelete
用于跟踪需要移除的子树中的 Fiber 节点。递归遍历:
commitNestedUnmounts
函数,深度优先遍历 Fiber 树,执行onCommitUnmount
。commitNestedUnmounts
的回调函数中,调用recordChildrenToDelete
函数,遍历所有兄弟节点,记录待删除的节点。遍历删除:
rootChildrenToDelete
,找到待删除子树的根节点的父级 DOM(hostParent
),并通过removeChild
移除每个节点的 DOM。清理关联:
childToDelete
的return
和child
属性置为null
,断开与父节点和子节点的关联。4. 处理 React 导出
因为我们使用 Fragment 组件时,是从
react
包引入的,如:import { Fragment } from 'react';
或直接使用<React.Fragment>
标签,因此还需要在react
包中导出 Fragment 组件。在
jsx.ts
中导出 Fragment:在
jsx-dev-runtime.ts
导出 Fragment:至此,我们就实现了 Fragment,并将 Fragment 类型的组件加入到了 React 更新流程中。
相关代码可在
git tag v1.13
查看,地址:https://github.com/2xiao/my-react/tree/v1.13The text was updated successfully, but these errors were encountered: