React:为什么需要Fragment

243 阅读3分钟
什么是Fragment?

React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。

Fragment使用:

例如我们有两个组件,Container 和 ButtonList;

image.png

Container 组件接受align 来控制 ButtonList 在内部的对齐;

我们定义ButtonList组件如下:

function ButtonList (list) {
    // return list.map((text) => <Button>{text}</Button>)
    // 只做演示,莫在意细节
    return <Button> 确定</Button> <Button>取消</Button>
}

定义完发现报错了:

Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?

我们知道,react是不能直接返回多个节点的(在 React 16.0 中已经支持了在组件中渲染数组,return [xx, el, xx], 跟返回多个jsx根节点还是有区别的);

为什么不能返回多个节点?

我们知道jsx会转码,例如:

function ButtonList () {
    return <Button>确定</Button>
}

转译为

function ButtonList() {
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(Button, {
    children: "确定"
  });
}

那么我们返回两个会转译为什么呢?

function ButtonList() {
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(Button, {
    children: "确定"
  })  /*#__PURE__*/(0, _jsxRuntime.jsx)(Button, {
    children: "取消"
  })
}

这这js语法是不支持的;我们写的jsx其实是jsx函数(React.createElement)的语法糖,从转码后的结果我们知道了为啥jsx表达式必须只能有一个父节点;

为什么不直接用一个html标签包裹多节点?

例如上面的需求,

  • 如果我们用div来包裹,那么会改变ButtonList的布局方式,例如如果container使用text-align 来控制它的对齐,那么用div来包裹就会破坏布局结构
  • 即使我们用其他标签来包裹,那么还有性能问题,例如要多进行的创建dom节点的操作,并且dom tree会出现冗余节点,例如ul 跟 li之间多了其他节点;

那么这就需要Fragment节点了,它会在fiber树中作为这些被包裹的节点的父fiber节点;最终渲染后不会在dom中出现冗余节点,创建fiber 比 创建 dom更快,并且在fiber树中更省内存;

综上,由于jsx表达式需要返回单一父节点并且多个节点的片段不需要冗余元素包裹的就需要一个Fragment来解决问题了;

Fragment 在fiber树中的形态

example1:

const App = () => (
    <React.Fragment key="random">
      <button>确认</button>
      <button>确认</button>
    </React.Fragment>
  );

渲染完后我们选中确认的button,发现它的fiber父节点是Fragement;

image.png

example2:

我们去掉Fragment 的 key,再次执行,发现它的父节点变为了Function fiber(App), 也就是说这种没key,并且没有兄弟的Fragment,实际不生成fiber节点;

image.png

example3:

function App() {
  const buttonList = [<button>确认</button>, <button>确认</button>];
  return (
    <div>
      <p>hi</p>
      {buttonList}
    </div>
  );
}

再次选中确定按钮,执行如下图,这种情况react为我们自动包裹了一个fragment;

image.png

对于fragment,在生成fiber树的阶段有一些特殊的处理

  1. 对于example1,源码处理如下, 即对于这种 child element,会取出它的chidren来作为child element同 current.fiber diff 生成 当前 fiber(App) 的 child fiber;
const isUnkeyedTopLevelFragment =
  typeof newChild === 'object' &&
  newChild !== null &&
  newChild.type === REACT_FRAGMENT_TYPE &&
  newChild.key === null;
if (isUnkeyedTopLevelFragment) {
  newChild = newChild.props.children;
}
  1. 对于example2, 跟正常的beginWork流程基本相同,只是创建新fiber的逻辑调用不同Api而已(fragment 使用createFiberFromFragment(element.props.children, key));

  2. 对于exmple3, react 在多节点diff时,当 element[i]时Array时,使用如下代码,可知在此中情况自动生成的fragment默认使用的index作为key;

if (isArray(newChild) || getIteratorFn(newChild)) {
    const matchedFiber = existingChildren.get(newIdx) || null;
    return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
}

 function updateFragment(
    returnFiber: Fiber,
    current: Fiber | null, // 代表预处理的map(key->fiber)中是否有相同key的fiber;
    fragment: Iterable<React$Node>,
    lanes: Lanes,
    key: null | string,
  ): Fiber {
    if (current === null || current.tag !== Fragment) {
      // Insert
      const created = createFiberFromFragment(
        fragment,
        returnFiber.mode,
        lanes,
        key,
      );
      created.return = returnFiber;
      return created;
    } else {
      // Update
      const existing = useFiber(current, fragment);
      existing.return = returnFiber;
      return existing;
    }
  }

附一下带fragment的fiber树生成

function App() {
	const [num, setNum] = useState(100);
	const arr =
		num % 2 === 0
			? [
                            <span key="1">1</span>,
                            <span key="2">2</span>, 
                            <span key="3">3</span>
                          ]
			: [
                            <span key="3">3</span>,
                            <span key="2">2</span>,
                            <span key="1">1</span>
			  ];
	return (
		<div onClick={() => setNum(num + 1)}>
			<span>1</span>,<span>2</span>
			{arr}
		</div>
	);
}

mount:

image.png

update:

image.png

对于fiber树生成有疑问可以查看我的这篇文章