前言
使用React框架的开发过程中,我们常常会使用两种方式调用组件:一种是组件式,另一种是函数式,但是这两种用法究竟有何不同?
一、代码示例
App.jsx
jsx
// 当作普通组件使用
function MyComponent() {
return <div>Hello Guming!</div>
};
function App() {
return (
<div className="App">
<MyComponent />
</div>
);
}
export default App;
jsx
// 当作函数使用
function MyComponent() {
return <div>Hello Guming!</div>
};
function App() {
return (
<div className="App">
{MyComponent()}
</div>
);
}
export default App;
这是一个简单的例子,还没有涉及到状态。在App.jsx文件中,声明了一个MyComponent组件,并在App入口函数中使用。我们知道,上述代码并不能直接执行,需要先被babel插件编译。
编译后的结果:
jsx
// 当作普通组件使用
function MyComponent() {
return React.createElement("div", null, "Hello Guming!");
};
function App() {
return React.createElement("div", {
className: "App"
}, React.createElement(MyComponent, null));
}
export default App;
jsx
// 当作函数使用
function MyComponent() {
return React.createElement("div", null, "Hello Guming!");
};
function App() {
return React.createElement("div", {
className: "App"
}, MyComponent());
}
export default App;
React.createElement的作用是创建ReactElement对象,它接收三个参数:
- type --- 元素类型;
- config --- 元素属性;
- children --- 元素子节点;
在两种用法当中,我们可以看到经过babel插件编译后的区别在于函数的最后一个参数。
当我们使用组件的方式时,babel插件会将""编译成React.createElement(MyComponent, null);而对于函数的用法,则变成了一次函数调用,而执行后,会返回
csharp
React.createElement("div", null, "Hello Guming!")
二、生成Fiber树结构的不同
我们知道,React会创建Fiber树,而Fiber节点创建的依据就是ReactElement对象。每次更新时,React都会创建一个新的workInProgress树,期间包d含diff和复用的逻辑(后面会提到),然后根据workInProgress树依次创建dom节点,并将根节点的current指向新建的树的根节点(RootFiber)上。
render阶段的工作,可以分为"递"(beginWork)与"归"(completeWork)。在"递"阶段,React会根据依次根据当前节点创建其子节点,直至构建出完整的Fiber树;而在"归"阶段,则是进行一些元素标记和标记的"冒泡"。在mount和update时,这"递"与"归"流程也有所不同,大体如下图所示:
"递"阶段
"归"阶段
回到例子当中来,在创建好React Element之后,接下来需要构建workInProgress树。相比函数式的使用方式,使用组件的方法多创建了一个Fiber节点:
MyComponent组件的Fiber节点
从上图可以看出来,这个节点的type(类型)就是MyComponent函数地址。
在completeWork时,会根据Fiber创建对应的dom节点,由于MyComponent对应fiberNode的tag为FunctionComponent,因此在执行完bubbleProperties函数后会不会创建dom element而是直接返回null。bubbleProperties函数主要作用是节点属性的冒泡。
三、组件的函数执行的时机不同
对于FunctionComponent,生成的fiberNode的type就是组件的执行函数本身。
在beginWork时,如果是在mount时,则会调用mountIndeterminateComponent函数;如果是在update时,则会调用updateFunctionComponent函数,而在两个函数执行过程中,都会调用renderWithHooks这个函数。而在renderWithHooks函数中,有这样一段代码:
而Component就是组件函数本身。在renderWithHooks函数打上断点,我们可以发现:
对于组件使用的方式,当前处于beginWork的节点是MyComponent的fiberNode:
而对于函数使用的方式,当前处于beginWork的节点是App对应的fiberNode:
四、进行某些算法比较时的流程不同
1.bailout策略-节点复用
在update时,出于性能考虑,React会进行一些优化。在update时,如果满足以下条件:
- 新旧节点的属性相同
- 当前节点的类型没有发生改变(例如没有从div变成p)
- 当前节点不存在更新
- context没有发生变化(由于代码中没有使用Context相关API,因此这个条件一直满足)
那么React会对子节点进行复用,如果当前节点的子节点不存在任何更新,则会跳过整个子树的beginWork。
有一个点需要注意,在对比新旧节点属性时,由于每次render时,创建的JSX对象都是新的地址,因此新旧节点属性比较并不相同,除非当前节点的父节点满足bailout逻辑,复用了当前节点。
接下来改造一下代码,引入状态更新:
jsx
import { useState } from "React";
// 当作普通组件使用
function MyComponent({count}) {
return <div>I has drunk {count} guming milky tea!</div>
};
function App() {
const [num, setNum] = useState(0);
return (
<div className="App" onClick={() => {setNum(1)}}>
<MyComponent count={num}/>
</div>
);
}
export default App;
jsx
import { useState } from "React";
// 当作函数使用
function MyComponent(count) {
return <div>I has drunk {count} guming milky tea!</div>
};
function App() {
const [num, setNum] = useState(0);
return (
<div className="App" onClick={() => {setNum(1)}}>
{MyComponent(num)}
</div>
);
}
export default App;
(1).我们第一次点击文字时,会时num值加1,此时第一个进入beginWork的节点是HostFiberRooter,此时肯定满足以下条件:
- 新旧节点的属性相同(全等)
- 当前节点的类型没有发生改变(例如没有从div变成p)
- 当前节点不存在更新
- context没有发生变化
因此App对应的fiberNode被复用。
(2). 接下来进入App对应fiberNode的beginWork,由于我们点击文字触发了更新,因此App存在更新,不满足bailout逻辑。
(3). 再创建完div对应的fiberNode后,进入div的beginWork。由于其父节点App没有被复用,因此在比较当前节点的新旧属性时不相等,不满足bailout逻辑。
(4). 对于组件使用的方式 ,由于多了一个MyComnonet节点,因此下一个进入beginWork的是MyComponent。由于其父节点div没有被复用,并且节点属性发生变化(count属性由0变为1),因此不会复用子节点;对于函数调用方式 ,下一个beginWork的节点则是div,同样由于副节点未被复用,导致在比较oldProps和newProps时不相同。
(5).除了MyComponent,由于两者之后的fiber树的构造相同,后面节点的beginWork流程也是一样,同样无法被复用;
2.diff算法
在beginWork时,还设计到diff,在React源码中,主要diff逻辑reconcileSingleElement方法中。网上已经存在很多关于React diff算法的文章,这里不过多讲解,有兴趣的读者可以参考这篇:
【动图+大白话🍓解析React源码】Render阶段中Fiber树的初始化与对比更新~ - 掘金
以下是reconcileSingleElement函数的内容
javascript
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
var key = element.key;
var child = currentFirstChild;
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) {
var elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
var existing = useFiber(child, element.props.children);
existing.return = returnFiber;
{
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
} else {
if (child.elementType === elementType || ( // Keep this check inline so it only runs on the false path:
isCompatibleFamilyForHotReloading(child, element) ) || // Lazy types should reconcile their resolved type.
// We need to do this after the Hot Reloading check above,
// because hot reloading has different semantics than prod because
// it doesn't resuspend. So we can't let the call below suspend.
typeof elementType === 'object' && elementType !== null && elementType.$$typeof === REACT_LAZY_TYPE && resolveLazy(elementType) === child.type) {
deleteRemainingChildren(returnFiber, child.sibling);
var _existing = useFiber(child, element.props);
_existing.ref = coerceRef(returnFiber, child, element);
_existing.return = returnFiber;
{
_existing._debugSource = element._source;
_existing._debugOwner = element._owner;
}
return _existing;
}
} // Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
var created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key);
created.return = returnFiber;
return created;
} else {
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
_created4.ref = coerceRef(returnFiber, currentFirstChild, element);
_created4.return = returnFiber;
return _created4;
}
}
当执行到MyComponent对应fiberNode的beginWork时,我们可以看到,其child属性为null,并不会进入下面的while循环,因此MyComponent是不参与diff比较的。
在update时,completeWork工作主要是针对前后属性有变化的节点进行标记,MyComponent对diff的结果没有影响,所以对最后diff的结果并没有影响。
五、结论
本文通过几个例子,简单讲解了用组件方式和函数方式使用自定义组件的三个区别:
- 在创建workInProgress树时,组件使用的方式会比函数使用的方式多一个fiber节点;
- 在组件函数执行的时机不同;
- 在进行diff算法比较时,MyComponent对应的节点会被忽略;
- 在bailout的复用逻辑中,尽管复用时多了一次节点比较,但是并不会影响最终的结果;
React的流程都是基于Fiber的,而上述差异都是基于此点。尽管两种使用方式生成的Fiber树结构不相同,但是对于React核心的流程并没有太大影响。当然,实际的使用情况远远比本文中提出的例子更复杂,本文并没有针对所有场景和流程逐一分析。
最后
📚 小茗文章推荐:
关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~