React HOOK re-render 开销大吗?
How do I implement getDerivedStateFromProps?
While you probably don’t need it, in rare cases that you do (such as implementing a <Transition>
component), you can update the state right during rendering. React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.
Here, we store the previous value of the row
prop in a state variable so that we can compare:
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
这是一个官方文档的 例子,大概意思就是说利用react的re-render机制,通过hook实现Class中的getDerivedStateFromProps,在描述的最后一句话中提到了 React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive. 翻译过来的意思为 “React将在退出第一次渲染后立即重新运行状态更新的组件,这样不会太昂贵。”,这里 我有大概两个疑问 :
- 什么时候会出现re-render的情况。
- 为什么认为这样的开销是不会太昂贵的
首先 我们知道React 是采用双缓存机制的,一个应用同时存在两颗虚拟DOM树,通常一颗为current 树,和workInprogress 树,(当然为什么 我这么叫这两个树其实是因为他的源码里面的变量是这么命名的),我们的页面其实就是基于current 树 生成的,同时在Update时期基于 current 通过diff算法 生成workInprogress 后,用workInprogress 替换之前的 current ,此时wokInporgress 就成了 current。
什么时候会出现re-render的情况
首先 我们来聊一聊在那种情况下会出现 hook re-render的情况。
目前我只知道一种出现的情况,也就是上面的React 官方给出的例子,其中 如果名字if(row !== prevRow) 这个逻辑 执行其中的 两个 dispatchSetState 方法的时候就会 发生 当前hook 的re-render机制,也就是官方所说的 渲染阶段更新
。说了半天 到底什么是re-render 机制呢。
什么是hook的 re-render 机制
总结一句话而言就是,就是在 hook 执行阶段 调用 useState 产生的 dispatchSetState 方法导致 重新执行且只执行当前hook的过程。以下方代码为例
function Animal() {
const [name,setName] = useSate('tiger')
setName('cat')
setName('dog')
return <div>{name}</div>
}
首先 我们都知道 在hook中可以通过 useState() 方法返回的 对应的 dispatchSetState (useState 返回数组的第二个) 方法来触发更新,然而每次更新都要从头到脚 更新 整棵树,不是类似Vue那种有目标型,而对于 渲染阶段的更新 React 采用 re-render 来优化 ,也就是上面的 Animal 组件中 也就是函数,在执行过程中 调用了两次 setName() 方法改变状态,这种情况下这两次的 状态更新 导致重新执行了 Animal () 方法,而不会 导致整个应用 全部比较更新
我们通过源码来看一下。
对于hook的执行发生在 renderWithHooks 这个方法中,通过 精简代码大概如下。
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes)
{ renderLanes = nextRenderLanes;
currentlyRenderingFiber$1 = workInProgress; //第一步 通过
{
if (current !== null && current.memoizedState !== null) {//第二部
ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
ReactCurrentDispatcher$1.current = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
}
}
var children = Component(props, secondArg); // 第三部 这里的Component 就对应上面例子的 Animal
if (didScheduleRenderPhaseUpdateDuringThisPass) {//第四部
var numberOfReRenders = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
numberOfReRenders += 1;
children = Component(props, secondArg); //在渲染阶段 发生了 更新 所以 从新执行当前的function Component方法
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher; //解绑
renderLanes = NoLanes;
currentlyRenderingFiber$1 = null; //Function Component 执行完后 置空
currentHook = null;
workInProgressHook = null;
return children; //返回 Function Component 结果
}
大致讲一下上面代码在做什么
-
将workInProgress fiber 复制给currentlyRenderingFiber$1,即当前正在操作的fiber 。
-
通过一些条件判断现在应该调用 官方提供的 hooks的,mount方法还是update方法,在react源码中 两个时期同一个hook调用的是不同的方法,举个例子,useState mount时期调用 mountState ,update时期调用 updateState 实际上是调用的updateReducer 方法,其他hook 同理,这个就通过ReactCurrentDispatcher$1 的current 属性来表明 之后调用mount 方法还是update方法
-
执行 当前的函数组件,返回jsx 生成的对象 ,(执行完后也就相当于 ‘退出第一次渲染’)
-
通过 didScheduleRenderPhaseUpdateDuringThisPass 变量判断是不是在渲染阶段进行了更新,命中的话 通过一个do. while 循环 开始我们的re-render ,也就是从新执行 Function Component 函数。(这个do while 中做的也正是我们所谓的 re-render)
-
对一些变量进行初始化,同时这里将currentlyRenderingFiber$1 赋值为空
-
返回结果children
接下里看看 didScheduleRenderPhaseUpdateDuringThisPass 这个变量是在什么时候被复制为true的
之前我们就说过 ,hook改变状态通过 每个useState 返回的对应的 dispatchSetState 方法来实现。
function dispatchSetState(fiber, queue, action) {
var lane = requestUpdateLane(fiber);//确立优先级的
var update = { //生成update 对象
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null
};
if (isRenderPhaseUpdate(fiber)) { //判断 是否存在 渲染阶段更新的情况
enqueueRenderPhaseUpdate(queue, update);
} else {
enqueueUpdate$1(fiber, queue, update);
var alternate = fiber.alternate;
var eventTime = requestEventTime();
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane);
}
这里也大致讲一下做了什么
- 生成update 对象
- 通过 isRenderPhaseUpdate(fiber) 方法判断是否在渲染阶段发生了更新,然后调用enqueueRenderPhaseUpdate(queue, update)
isRenderPhaseUpdate 方法中很简单,看一下代码
function isRenderPhaseUpdate(fiber) {
var alternate = fiber.alternate;
return fiber === currentlyRenderingFiber$1 ||
alternate !== null &&
alternate === currentlyRenderingFiber$1;}
这里的参数fiber 到底是个什么动西我们通过调用栈来看一下
function mountState(initialState) {
var hook = mountWorkInProgressHook();
var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
//mountState 中dispatchSetState
mountState 中dispatchSetState 传入 currentlyRenderingFiber$1, 这个不陌生吧 当前执行操作的fiber
所以dispatchSetState(fiber, queue, action) 中fiber 就是currentlyRenderingFiber$1
同时在 dispatchSetState 中调用 isRenderPhaseUpdate(fiber) 这个fiber 也就是currentlyRenderingFiber$1 ,那么刚刚在isRenderPhaseUpdate 的判断逻辑中 第一个就成立了,
其中的alternate 属性使用与在 将current 树 和 workInprogress 树中的fiber节点进行映射链接一个指针
在 一般情况下,也就是 不存在渲染阶段更新的情况的话,isRenderPhaseUpdate 函数中 currentlyRenderingFiber1变量应该等于null, 因为在renderWithHooks里面对应的函数组件方法执行以及re−render执行完后对一些变量进行了初始化,也就是在这个时候currentlyRenderingFiber1变量应该 等于null , 因为在renderWithHooks 里面 对应的函数组件方法执行以及 re-render 执行完后 对一些变量进行了初始化 ,也就是在这个时候 currentlyRenderingFiber1变量应该等于null, 因为在renderWithHooks里面对应的函数组件方法执行以及re−render执行完后对一些变量进行了初始化,也就是在这个时候currentlyRenderingFiber1 = null。
而 didScheduleRenderPhaseUpdateDuringThisPass 这个变量 是在enqueueRenderPhaseUpdate 方法中被赋值为true
function enqueueRenderPhaseUpdate(queue, update) {
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
var pending = queue.pending;
if (pending === null) {
update.next = update; //初始环转链表
} else {
update.next = pending.next;
pending.next = update; //环转链表 末尾追加 update
}
queue.pending = update;
}
使用实例
这个有待补充,目前开发中还没正式使用这种方法过,但是大致的一个使用场景我的理解 比如:当前组件的某一个 props 值 改变会引起 state 的变化,同时state 变化后 需要立即 执行render .
总结
react 的re-render机制开销 不是很大。。。。。。。
转载自:https://juejin.cn/post/7075298386454249479