一、前言
又到了金三银四的求职季,最近也是抱着检验自己学习成果的心态投了某厂,看自己现在是几斤几两,下面就分享一个面试中的一个问题,个人觉得还是挺有研究价值的,通过这个问题可以检测你对 React 的渲染到底了解多少,先把题目放到下面:
const Child = () => {
console.log("Child");
return (
<div className="child" title="child">
Child
</div>
);
};
const Parent = (props) => {
const [count, setCount] = useState(0);
return (
<div
className="parent"
title="parent"
onClick={() => setCount((pre) => pre + 1)}
>
{props.children}
</div>
);
};
function App() {
return (
<div className="App">
<Parent>
<Child />
</Parent>
</div>
);
}
问题就是从一开始进入页面到触发3次 onClick 事件, Child 组件中的 console.log 会执行几次,换句话说 Child 组件会 render 几次?
答案:1次
二、前置知识
1、React.CreateElement
我们知道对于 JSX 代码来说它最终会被 babel 编译为 React.createElement(type, config, children) 这种形式(在React17 之后已经用了新的 JSX 转换方式),在 render 阶段会执行这些函数,它的返回是一个 JSX 对象,比如下面的 JSX 代码:
<div className='class' title='title'>
<p>xiling</p>
</div>
经过 babel 编译后如下:
React.createElement("div", {
className: "class",
title: "title"
},React.createElement("p", null, "xiling"));
执行 React.createElement 的返回值如下:
其中
type 属性代表的当前元素的类型,这里就是 div 标签,props 属性种保存了元素中定义的一些属性,比如 className、title、children等信息。那这里有个疑问了这个 JSX 对象这么复杂,他有啥作用呢?其实这个 JSX 是实现 diff 算法的关键要素之一,现在我们只要知道我们书写的 JSX 代码最终会被转换成一个用来描述代码信息的 JSX 对象,在 diff 算法中 React 会通过对比 fiber 节点和 JSX 对象上的信息来决定是否需要更新。
2、fiber 节点创建方式
对于 fiber 节点来说它有两种方式去创建:
- 在首次渲染时通过 render 返回的 JSX 对象生成 fiber 节点。
这里拿 App 组件中的 div 元素来说,左边展示的是 div 对应的 JSX 对象,右边是最终的 fiber 节点,其中最重要的一点我用红框框画出来了,这里提供的信息就是说我 fiber 节点的 pendingProps 属性保存的信息和 JSX 对象中 props 属性的信息是一样的,即 fiber.pendingProps === JSX.props,这一点是我们解题的关键。我们可以通过查看 createFiberFromElement 方法来验证:
function createFiberFromElement(element, mode, expirationTime) {
var owner = null;
{
owner = element._owner;
}
var type = element.type;
var key = element.key;
// 赋值操作
var pendingProps = element.props;
var fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, expirationTime);
{
fiber._debugSource = element._source;
fiber._debugOwner = element._owner;
}
return fiber;
}
-
在 update 时如果命中 Bailout,那么就可以直接复用上一次更新的 fiber 节点,此时组件就不会去执行他的 render 方法。
到这里我猜大家应该知道原因了,Child 组件肯定是走的这条路径,确实是的,下面我们要弄清楚什么情况下才会走这条路径。
首先我们要知道在 render 阶段有两个子阶段,其中 beginWork 被称为递阶段,它会从应用的根节点开始然后去创建根节点的 child 节点,再从 child 节点开始去创建它的 child 节点直到叶子结点,这个阶段执行完就会生成一棵完整的 fiber 树,在 beginWork方法中展示了进入 Bailout 的几个条件:
-
oldProps === newProps 这里使用的是全等运算符,通过上面介绍我们知道 pendingProps 其实就是我们 JSX 中的 props,那么oldProps 和 newProps 什么时候会不想等呢?其实很简单就是组件/元素执行了对应的 React.CreateElement 之后就不同了,因为每次执行 React.CreateElement 就会生成一个新的 props 即使里面的数据都不变也是不行的。
-
Context 没有改变 当我们使用 createContext 来进行数据传输时,如果里面的数据发生改变,那么消费了这些数据的组件就会重新渲染。
-
updateExpirationTime < renderExpirationTime 这一点其实就是判断当前 fiber 节点上是否存在更新,如果当前 fiber 节点更新的优先级(updateExpirationTime)和本次 React 调度的优先级一样那么就会重新渲染。这里我们点击onClick后看下Parent这个fiber节点对应的优先级:
我们发现这两个优先级时一样的,说明 Parent 组件要进入 render 逻辑。
三、Bailout 阶段
首先要知道 Bailout 肯定是发生在 update 的时候,而对于 mounted 时期创建 fiber 节点全都是上面说的第一种方式,下面我们按照我们给出的例子将 mounted 之后的 fiber 树画出来:
此时我们的 workInProgress 树上只有一个根节点,当我们触发 onClick 事件后会进行一次更新,更新意味着要创建新的 fiber 树,但是在进入 beginwork 之前我们需要调用一次 createWorkInProgress 方法,这个方法就是创建 workInprogress fiber 树的入口,那调用他的目的是啥呢?我们现看看它内部干了啥。
我们看到他其实就是去定义 workInProgress 的一些属性,此时 workInProgress 指的就是 workInProgress fiber 树上的 rootFiber 节点,其中比较关键的一步就是将 current.child 赋值给workInProgress,那此时的 fiber 树就如下所示:
接下来就是根据 current fiber 树 去创建 WorkInProgress fiber 树,首先进入 beginWork 的是 WorkInProgress树的 rootFiber 节点,也就是当前应用的根节点,此时他的alternte 指针指向的是 current 树的 rootFiber 节点,如下图所示:
对于这个节点来说它是满足 Bailout 的三个条件,所以进入 Bailout 阶段,在 beginWork 函数中最终会进入到
bailoutOnAlreadyFinishedWork 函数里面,他的工作就是创建 rootFiber 的 child 节点,让我们看看他的实现:
function bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime) {
//....不重要的先忽略
cloneChildFibers(current$$1, workInProgress);
return workInProgress.child;
}
}
这里我们看到他先调用了cloneChildFibers方法,然后再返回 workInProgress.child,从方法名我们大概就知道这个函数的作用了,就是将 current 子节点上的属性赋值给 workInProgress 的子节点我们进去看看:
function cloneChildFibers(current$$1, workInProgress) {
//...
//workInProgress.child指的其实就是current.child,因为首次进去 update 时会将 rootFiber的child复制一份给 workInprogress.child
var currentChild = workInProgress.child;
// 创建一个新的 fiber 节点,此时的 pendingProps 就是 current 上的 pendingProps,即更新前的 pendingProps
var newChild = createWorkInProgress(currentChild, currentChild.pendingProps, currentChild.expirationTime);
// 将新创建的 newChild 赋值给 workInProgress.child
workInProgress.child = newChild;
newChild.return = workInProgress;
//...
}
让我们看看执行完这个函数之后我们的 fiber 树变成什么了
也就是说这两个 div 之间通过 alternate 建立了链接,同时他们的 pendingProps 是相同的,这里我们要注意的一点是
current.memoizedProps 其实就是 current.pendingProps ,所以接下来 div 对应的 fiber 也会走 Bailout,直到 Parent 组件对应的 fiber 进入 beginWork ,此时因为 Parent 组件存在更新,所以他不能走 Bailout ,他需要调用 React.CreateElement 重新生成对应的 JSX 对象然后进行 Diff 操作,具体就不介绍了。
但是这个过程和我们的问题有什么关系呢?其实答案已经出来了,对于这种形式来说:
{props.children}
他代表的其实就是 Child 组件对应的 JSX 对象,这里的 props 对应着 Parent 组件的 fiber 节点上的 pendingProps 属性,它在更新前后是没有变的,所以对于 Child 组件来说,他的 fiber 节点也满足oldProps === newProps,这样就满足了 Bailout 条件,自然就不会 render了。
传送门