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
目前我们实现的更新流程中,从触发更新到 Render 阶段和 Commit 阶段,都是同步的,多次触发更新会重复多次执行更新流程。
为了提升性能,React 使用了批处理(Batched Updates)机制,批处理的目标是将多个更新操作合并成一个单一的更新,从而减少不必要的计算和渲染次数,提高应用的性能和响应速度。主要包括以下几个方面:
合并更新: 当调用 setState() 函数时,React 不会立即执行更新操作,而是将更新请求放入一个队列中等待执行,并将多个更新请求合并成一个更新。例如,如果一个组件在同一个事件循环中多次调用 setState(),React 只会执行一次更新,并以最新的状态进行渲染
setState()
异步执行: React 将更新操作延迟到浏览器的下一个事件循环中执行,以确保 UI 渲染时是最新的状态。
优先级调度: React 会根据更新的优先级和时间戳等因素进行调度,以保证在有限的时间内完成尽可能多的更新。这样可以确保及时响应用户操作,并保持良好的用户体验。
Lane 是一个二进制的数字,表示一种优先级或者更新的类型,是用于调度和执行更新的一种机制,Lane 的数值越小,代表优先级越高;Lanes 可以看作是多个 Lane 的集合,用于表示系统中所有可能的更新任务的优先级。
通常情况下,可以将更新任务划分为不同的优先级,例如:高优先级更新、中等优先级更新和低优先级更新等。每个优先级都对应一个 Lane,当有更新任务产生时,会根据任务的优先级选择合适的 Lane,并将任务放入该 Lane 中,然后会根据 Lanes 中 Lane 的优先级顺序在合适的时机依次处理。
新建一个 fiberLanes.ts 文件,在里面实现 Lane 相关的定义和方法:
fiberLanes.ts
import { FiberRootNode } from './fiber'; // 代表 update 的优先级 export type Lane = number; // 代表 lane 的集合 export type Lanes = number; export const NoLane = 0b0000; export const NoLanes = 0b0000; export const SyncLane = 0b0001; // 获取 Lane 的集合 export function mergeLanes(laneA: Lane, laneB: Lane): Lanes { return laneA | laneB; } // 获取更新的优先级 export function requestUpdateLanes() { return SyncLane; } // 获取 lanes 中优先级最高的 lane export function getHighestPriorityLane(lanes: Lanes): Lane { // 默认规则:数值越小,优先级越高 return lanes & -lanes; } // 从根节点的 pendingLanes 中移除某个 lane export function markRootFinished(root: FiberRootNode, lane: Lane): void { root.pendingLanes &= ~lane; }
每个 Fiber 节点的更新都保存在 updateQueue 属性中,当有新的更新时,会调用 enqueueUpdate 函数将 update 添加到 updateQueue 中。
updateQueue
enqueueUpdate
update
但是目前我们实现的 enqueueUpdate 函数是直接将 update 赋值给 updateQueue,这样会覆盖掉之前的 update。
因此我们需要对 enqueueUpdate 函数进行修改,以确保新的更新能够正确地添加到更新队列中,可以采用一种链表的方式来组织更新队列,每个更新对象都保存着下一个更新对象的引用,从而形成一个链表结构,确保更新操作能够按顺序执行:
// packages/react-reconciler/src/updateQueue.ts // 将 Update 添加到 UpdateQueue 中的方法 export const enqueueUpdate = <State>( updateQueue: UpdateQueue<State>, update: Update<State> ) => { const pending = updateQueue.shared.pending; if (pending === null) { update.next = update; } else { update.next = pending.next; pending.next = update; } // pending 指向 update 环状链表的最后一个节点 updateQueue.shared.pending = update; };
在这段代码中,我们将新的 update 添加到了更新链表的末尾,并将 update.next 指针指向链表中的第一个更新对象,最后将 updateQueue.shared.pending 指向 update,也就是链表的最后一个更新,这样可以方便我们添加和获取更新。
update.next
updateQueue.shared.pending
接着,我们为 Update 数据结构增加 next 和 lane 两个字段,同时修改创建 Update 实例的方法:
Update
next
lane
// packages/react-reconciler/src/updateQueue.ts import { Lane } from './fiberLanes'; // 定义 Update 数据结构 export interface Update<State> { action: Action<State>; next: Update<any> | null; lane: Lane; } // 创建 Update 实例的方法 export const createUpdate = <State>( action: Action<State>, lane: Lane ): Update<State> => { return { action, next: null, lane }; };
显然,所有调用 createUpdate 函数创建 Update 的地方,我们都需要增加 lane 参数,目前有两个方法可以触发更新: updateContainer 和 dispatchSetState,分别对应了首屏渲染阶段和组件更新阶段。
createUpdate
updateContainer
dispatchSetState
// packages/react-reconciler/src/fiberHooks.ts import { requestUpdateLanes } from './fiberLanes'; // 用于触发状态更新的逻辑 function dispatchSetState<State>( fiber: FiberNode, updateQueue: UpdateQueue<State>, action: Action<State> ) { const lane = requestUpdateLanes(); const update = createUpdate(action, lane); enqueueUpdate(updateQueue, update); // 调度更新 scheduleUpdateOnFiber(fiber, lane); }
// packages/react-reconciler/src/fiberReconciler.ts import { requestUpdateLanes } from './fiberLanes'; export function updateContainer( element: ReactElementType | null, root: FiberRootNode ) { const hostRootFiber = root.current; const lane = requestUpdateLanes(); const update = createUpdate<ReactElementType | null>(element, lane); enqueueUpdate( hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>, update ); scheduleUpdateOnFiber(hostRootFiber, lane); return element; }
这两个方法调用 createUpdate 函数创建 update 之后,会将更新加入 updateQueue 链表中,然后调用 scheduleUpdateOnFiber 函数开始调度更新。
scheduleUpdateOnFiber
接着,我们还需要对从 updateQueue 中消费 update 的流程进行改造。
processUpdateQueue
renderLane
result.memoizedState
// 从 UpdateQueue 中消费 Update 的方法 export const processUpdateQueue = <State>( baseState: State, pendingUpdate: Update<State> | null, renderLane: Lane ): { memoizedState: State } => { const result: ReturnType<typeof processUpdateQueue<State>> = { memoizedState: baseState }; if (pendingUpdate !== null) { // 第一个 update let first = pendingUpdate.next; let pending = first as Update<any>; do { const updateLane = pending.lane; if (updateLane == renderLane) { const action = pending.action; if (action instanceof Function) { // action 是回调函数 baseState = action(baseState); } else { // action 是状态值 baseState = action; } } else { if (__DEV__) { console.error('不应该进入 updateLane !== renderLane 逻辑'); } } pending = pending.next as Update<any>; } while (pending !== first); } result.memoizedState = baseState; return result; };
同样,每个调用 processUpdateQueue 函数消费 Update 的地方,都需要增加一个 renderLane 参数。有两个方法可以消费更新:updateHostRoot 和 updateState,分别对应了首屏渲染阶段和组件更新阶段。
updateHostRoot
updateState
这里不一一列举改造细节了,详见 代码。
在之前的更新流程中,每次触发更新后,scheduleUpdateOnFiber 函数会从当前节点向上遍历到根节点,然后调用 Render 阶段的入口 renderRoot 函数, 构建 Fiber 树、处理新旧虚拟 DOM 树之间的差异、生成更新计划;在 renderRoot 函数执行完之后又会调用 Commit 阶段的入口 commitRoot 函数,将更新同步到实际的 DOM 中。
renderRoot
commitRoot
现在为了实现批处理机制,将多个更新操作合并成一个更新,我们需要在 Render 阶段和 Commit 阶段的基础上增加 Schedule 阶段(调度阶段)。
为了方便调度优先级,我们将优先级队列保存在根节点上。根节点作为整个应用的入口,将更新的状态信息存储在根节点上统一管理,可以更快地访问和检查哪些更新需要被处理,减少在整个组件树中查找的时间。
为 FiberRootNode 数据结构增加 pendingLanes 和 finishedLane 两个字段,其中 pendingLanes 用于标识当前有哪些更新需要被处理,finishedLane 用于表示已经完成处理的更新。
FiberRootNode
pendingLanes
finishedLane
// packages/react-reconciler/src/fiber.ts import { Lane, Lanes, NoLane, NoLanes } from './fiberLanes'; export class FiberRootNode { container: Container; current: FiberNode; finishedWork: FiberNode | null; pendingLanes: Lanes; finishedLane: Lane; constructor(container: Container, hostRootFiber: FiberNode) { // ... this.pendingLanes = NoLanes; this.finishedLane = NoLane; } }
接着,我们回到 scheduleUpdateOnFiber 函数,为函数增加 lane 参数。
因为所有更新的 lane 都被保存在根节点的 pendingLanes 中,所以每当触发更新时,我们都需要将此次更新的 lane 记录到 FiberRootNode.pendingLanes 中。这个工作由 markRootUpdated 函数完成:
FiberRootNode.pendingLanes
markRootUpdated
// packages/react-reconciler/src/workLoop.ts export function scheduleUpdateOnFiber(fiber: FiberNode, lane: Lane) { const root = markUpdateFromFiberToRoot(fiber); markRootUpdated(root, lane); renderRoot(root); } // 将更新的优先级(lane)记录到根节点上 function markRootUpdated(root: FiberRootNode, lane: Lane) { root.pendingLanes = mergeLanes(root.pendingLanes, lane); }
接着就要调用 renderRoot 函数,进入 Render 阶段了。
但是我们不希望每次触发更新都执行一次更新流程,因此,要在这里插入一个调度流程,根据某种判断机制选出一个优先级,然后在一个宏任务或微任务中合并更新,只执行一次 renderRoot。
接下来,我们就来实现调度流程的入口 ensureRootIsScheduled 函数,实现原理如下:
ensureRootIsScheduled
获取最高优先级的更新:首先,从根节点的 pendingLanes 中选择一个最高优先级的更新,这里使用 getHighestPriorityLane 函数来选择优先级最高的更新;
getHighestPriorityLane
根据优先级调度任务:根据选择的优先级,有两种不同的调度方式:
setTimeout
安排微任务:调用 scheduleMicroTask 函数安排一个微任务,在微任务中执行更新。
scheduleMicroTask
// packages/react-reconciler/src/workLoop.ts import { getHighestPriorityLane } from './fiberLanes' import { scheduleSyncCallback, flushSyncCallback } from './syncTaskQueue'; import { scheduleMicroTask } from 'hostConfig'; export function scheduleUpdateOnFiber(fiber: FiberNode, lane: Lane) { const root = markUpdateFromFiberToRoot(fiber); markRootUpdated(root, lane); ensureRootIsScheduled(root); } // Schedule 阶段入口 function ensureRootIsScheduled(root: FiberRootNode) { const updateLane = getHighestPriorityLane(root.pendingLanes); if (updateLane == NoLane) return; if (updateLane === SyncLane) { // 同步优先级,用微任务调度 scheduleSyncCallback(renderRoot.bind(null, root, updateLane)); scheduleMicroTask(flushSyncCallback); } else { // 其他优先级,用宏任务调度 } }
现在,我们来实现 scheduleSyncCallback 函数和 flushSyncCallback 函数。
scheduleSyncCallback
flushSyncCallback
新建一个 syncTaskQueue.ts 文件,这个文件中保存了与同步任务相关的状态和函数。
syncTaskQueue.ts
callback
syncQueue
null
isFlushingSyncQueue
// packages/react-reconciler/src/syncTaskQueue.ts // 同步的任务队列 let syncQueue: ((...args: any) => void)[] | null = null; let isFlushingSyncQueue: boolean = false; // 调度同步的回调函数 export function scheduleSyncCallback(callback: (...args: any) => void) { if (syncQueue === null) { syncQueue = [callback]; } syncQueue.push(callback); } // 遍历执行同步的回调函数 export function flushSyncCallback() { if (!isFlushingSyncQueue && syncQueue) { isFlushingSyncQueue = true; try { syncQueue.forEach((callback) => callback()); } catch (e) { if (__DEV__) { console.error('flushSyncCallback 报错', e); } } finally { isFlushingSyncQueue = false; syncQueue = null; } } }
scheduleMicroTask 函数的作用是安排一个微任务,在当前任务执行完成后立即执行。
为了确保在各种不同的环境下都能够正确地安排微任务,并确保任务的及时执行,需要对不同的环境做出兼容,具体实现原理是:
queueMicrotask
Promise
Promise.resolve(null).then(callback)
// packages/react-dom/src/hostConfig.ts export const scheduleMicroTask = typeof queueMicrotask === 'function' ? queueMicrotask : typeof Promise === 'function' ? (callback: (...args: any) => void) => Promise.resolve(null).then(callback) : setTimeout;
现在我们已经实现了 Schedule 阶段,将多个更新合并到一个微任务中执行,现在我们来回顾一下 Schedule 阶段的流程,以下面这个函数组件为例:
function App() { const [count, setCount] = useState(1210); return ( <div onClick={() => { setCount((count) => count + 1); setCount((count) => count + 1); setCount((count) => count + 1); }} > {count} </div> ); }
div
root.pendingLanes
mergeLanes
SyncLane
updateLane === SyncLane
[renderRoot, renderRoot, renderRoot]
也就是说,在微任务中,renderRoot 函数还是会被执行三次,这显然不符合预期,因此,我们还需要对 renderRoot 函数及之后的更新流程做一些改造。
要解决上述问题,我们只需要在 renderRoot 函数第一次执行之后,也就是在进入 commitRoot 函数时,立即从根节点的 pendingLanes 中移除该更新的 lane,此时 root.pendingLanes 就为 NoLane 了。
NoLane
然后在进入 renderRoot 函数时加一个判断:如果根节点的 pendingLanes 中最高优先级的 lane 不是 SyncLane,代表此时的 pendingLanes 中只剩一些比 SyncLane 低的 lane 或 NoLane 了, 直接 return 即可。
return
这样,我们就解决了 renderRoot 函数多次执行的问题,具体的代码实现如下:
function renderRoot(root: FiberRootNode, lane: Lane) { const nextLane = getHighestPriorityLane(root.pendingLanes); if (nextLane !== SyncLane) { // 其他比 SyncLane 低的优先级或 NoLane,重新调度 ensureRootIsScheduled(root); return; } // ... } function commitRoot(root: FiberRootNode) { // ... const lane = root.finishedLane; markRootFinished(root, lane); // 重置 root.finishedLane = NoLane; // ...
至此,我们就实现了同步调度流程,可以将多个更新合并,按照优先级在一个微任务中执行。
同步调度流程能够即时响应状态变化和更新,一些高优先级的任务会在微任务中立即执行,从而优化了应用的性能和响应速度。通过将多个状态更新或 DOM 操作合并为一次更新执行,减少了浏览器执行的重排(reflow)和重绘(repaint)次数,这可以大大提高性能,减少页面的闪烁和卡顿。
相关代码可在 git tag v1.14 查看,地址:https://github.com/2xiao/my-react/tree/v1.14
git tag v1.14
The text was updated successfully, but these errors were encountered:
No branches or pull requests
目前我们实现的更新流程中,从触发更新到 Render 阶段和 Commit 阶段,都是同步的,多次触发更新会重复多次执行更新流程。
为了提升性能,React 使用了批处理(Batched Updates)机制,批处理的目标是将多个更新操作合并成一个单一的更新,从而减少不必要的计算和渲染次数,提高应用的性能和响应速度。主要包括以下几个方面:
合并更新: 当调用
setState()
函数时,React 不会立即执行更新操作,而是将更新请求放入一个队列中等待执行,并将多个更新请求合并成一个更新。例如,如果一个组件在同一个事件循环中多次调用setState()
,React 只会执行一次更新,并以最新的状态进行渲染异步执行: React 将更新操作延迟到浏览器的下一个事件循环中执行,以确保 UI 渲染时是最新的状态。
优先级调度: React 会根据更新的优先级和时间戳等因素进行调度,以保证在有限的时间内完成尽可能多的更新。这样可以确保及时响应用户操作,并保持良好的用户体验。
1. 实现 Lane 模型
Lane 是一个二进制的数字,表示一种优先级或者更新的类型,是用于调度和执行更新的一种机制,Lane 的数值越小,代表优先级越高;Lanes 可以看作是多个 Lane 的集合,用于表示系统中所有可能的更新任务的优先级。
通常情况下,可以将更新任务划分为不同的优先级,例如:高优先级更新、中等优先级更新和低优先级更新等。每个优先级都对应一个 Lane,当有更新任务产生时,会根据任务的优先级选择合适的 Lane,并将任务放入该 Lane 中,然后会根据 Lanes 中 Lane 的优先级顺序在合适的时机依次处理。
新建一个
fiberLanes.ts
文件,在里面实现 Lane 相关的定义和方法:2. 改造更新链表
每个 Fiber 节点的更新都保存在
updateQueue
属性中,当有新的更新时,会调用enqueueUpdate
函数将update
添加到updateQueue
中。但是目前我们实现的
enqueueUpdate
函数是直接将update
赋值给updateQueue
,这样会覆盖掉之前的update
。因此我们需要对
enqueueUpdate
函数进行修改,以确保新的更新能够正确地添加到更新队列中,可以采用一种链表的方式来组织更新队列,每个更新对象都保存着下一个更新对象的引用,从而形成一个链表结构,确保更新操作能够按顺序执行:在这段代码中,我们将新的
update
添加到了更新链表的末尾,并将update.next
指针指向链表中的第一个更新对象,最后将updateQueue.shared.pending
指向update
,也就是链表的最后一个更新,这样可以方便我们添加和获取更新。接着,我们为
Update
数据结构增加next
和lane
两个字段,同时修改创建Update
实例的方法:显然,所有调用
createUpdate
函数创建Update
的地方,我们都需要增加lane
参数,目前有两个方法可以触发更新:updateContainer
和dispatchSetState
,分别对应了首屏渲染阶段和组件更新阶段。这两个方法调用
createUpdate
函数创建update
之后,会将更新加入updateQueue
链表中,然后调用scheduleUpdateOnFiber
函数开始调度更新。接着,我们还需要对从
updateQueue
中消费update
的流程进行改造。processUpdateQueue
函数增加第三个参数renderLane
,代表当前更新的优先级;updateQueue
是一个链表,因此需要遍历链表,依次消费符合当前更新优先级的update
;update
之后,将计算出的最终值赋值给result.memoizedState
。同样,每个调用
processUpdateQueue
函数消费Update
的地方,都需要增加一个renderLane
参数。有两个方法可以消费更新:updateHostRoot
和updateState
,分别对应了首屏渲染阶段和组件更新阶段。这里不一一列举改造细节了,详见 代码。
3. 实现调度阶段
在之前的更新流程中,每次触发更新后,
scheduleUpdateOnFiber
函数会从当前节点向上遍历到根节点,然后调用 Render 阶段的入口renderRoot
函数, 构建 Fiber 树、处理新旧虚拟 DOM 树之间的差异、生成更新计划;在renderRoot
函数执行完之后又会调用 Commit 阶段的入口commitRoot
函数,将更新同步到实际的 DOM 中。现在为了实现批处理机制,将多个更新操作合并成一个更新,我们需要在 Render 阶段和 Commit 阶段的基础上增加 Schedule 阶段(调度阶段)。
为了方便调度优先级,我们将优先级队列保存在根节点上。根节点作为整个应用的入口,将更新的状态信息存储在根节点上统一管理,可以更快地访问和检查哪些更新需要被处理,减少在整个组件树中查找的时间。
为
FiberRootNode
数据结构增加pendingLanes
和finishedLane
两个字段,其中pendingLanes
用于标识当前有哪些更新需要被处理,finishedLane
用于表示已经完成处理的更新。接着,我们回到
scheduleUpdateOnFiber
函数,为函数增加lane
参数。因为所有更新的
lane
都被保存在根节点的pendingLanes
中,所以每当触发更新时,我们都需要将此次更新的lane
记录到FiberRootNode.pendingLanes
中。这个工作由markRootUpdated
函数完成:接着就要调用
renderRoot
函数,进入 Render 阶段了。但是我们不希望每次触发更新都执行一次更新流程,因此,要在这里插入一个调度流程,根据某种判断机制选出一个优先级,然后在一个宏任务或微任务中合并更新,只执行一次
renderRoot
。接下来,我们就来实现调度流程的入口
ensureRootIsScheduled
函数,实现原理如下:获取最高优先级的更新:首先,从根节点的
pendingLanes
中选择一个最高优先级的更新,这里使用getHighestPriorityLane
函数来选择优先级最高的更新;根据优先级调度任务:根据选择的优先级,有两种不同的调度方式:
renderRoot
函数会被添加到微任务队列中调度执行,以确保它在当前任务执行完毕后立即执行。setTimeout
的机制来安排任务,在主事件循环的下一个循环周期内执行更新。安排微任务:调用
scheduleMicroTask
函数安排一个微任务,在微任务中执行更新。现在,我们来实现
scheduleSyncCallback
函数和flushSyncCallback
函数。新建一个
syncTaskQueue.ts
文件,这个文件中保存了与同步任务相关的状态和函数。scheduleSyncCallback
scheduleSyncCallback
函数负责将传入的回调函数callback
添加到syncQueue
中,syncQueue
中保存了当前的同步任务队列。具体步骤如下:syncQueue
为null
,表示当前没有待执行的回调函数,需要创建一个新的队列来存储回调函数;syncQueue
已经存在,则直接将回调函数添加到队列的末尾。flushSyncCallback
flushSyncCallback
函数会遍历syncQueue
中的同步任务队列,并依次执行队列中的回调函数;isFlushingSyncQueue
标志位来表示当前是否正在执行队列中的回调函数;flushSyncCallback
函数,并且在同步执行期间防止其他异步任务的插入,以保证同步执行的正确性和稳定性;syncQueue
和isFlushingSyncQueue
。scheduleMicroTask
函数的作用是安排一个微任务,在当前任务执行完成后立即执行。为了确保在各种不同的环境下都能够正确地安排微任务,并确保任务的及时执行,需要对不同的环境做出兼容,具体实现原理是:
queueMicrotask
函数,如果存在,则使用queueMicrotask
函数来安排微任务;Promise
构造函数是否存在并可用。如果可用,则创建一个Promise
对象并立即将其解析(resolve),然后执行传入的回调函数。通过Promise.resolve(null).then(callback)
的方式,可以将传入的回调函数作为微任务安排在微任务队列中;setTimeout
函数来安排任务。在这种情况下,会将传入的回调函数安排在下一个宏任务队列中,而不是微任务队列中。现在我们已经实现了 Schedule 阶段,将多个更新合并到一个微任务中执行,现在我们来回顾一下 Schedule 阶段的流程,以下面这个函数组件为例:
div
都会触发三次dispatchSetState
更新,然后进入scheduleUpdateOnFiber
函数中调度更新;markRootUpdated
函数会将每次更新的lane
存入root.pendingLanes
中,经过三次mergeLanes
后的root.pendingLanes
值为SyncLane
;ensureRootIsScheduled
函数,开始调度阶段,因为此时的updateLane === SyncLane
,为同步任务,因此会安排一个微任务来执行更新;scheduleSyncCallback
函数会被执行三次,所以此时的syncQueue
值为三个renderRoot
回调函数数组([renderRoot, renderRoot, renderRoot]
);flushSyncCallback
函数同样会被调用三次,但是因为有isFlushingSyncQueue
标志位,所以函数只会执行一次;scheduleMicroTask
创建的微任务中,syncQueue
中的三个renderRoot
回调函数会被遍历执行。也就是说,在微任务中,
renderRoot
函数还是会被执行三次,这显然不符合预期,因此,我们还需要对renderRoot
函数及之后的更新流程做一些改造。4. 改造更新流程
要解决上述问题,我们只需要在
renderRoot
函数第一次执行之后,也就是在进入commitRoot
函数时,立即从根节点的pendingLanes
中移除该更新的lane
,此时root.pendingLanes
就为NoLane
了。然后在进入
renderRoot
函数时加一个判断:如果根节点的pendingLanes
中最高优先级的lane
不是SyncLane
,代表此时的pendingLanes
中只剩一些比SyncLane
低的lane
或NoLane
了, 直接return
即可。这样,我们就解决了
renderRoot
函数多次执行的问题,具体的代码实现如下:至此,我们就实现了同步调度流程,可以将多个更新合并,按照优先级在一个微任务中执行。
同步调度流程能够即时响应状态变化和更新,一些高优先级的任务会在微任务中立即执行,从而优化了应用的性能和响应速度。通过将多个状态更新或 DOM 操作合并为一次更新执行,减少了浏览器执行的重排(reflow)和重绘(repaint)次数,这可以大大提高性能,减少页面的闪烁和卡顿。
相关代码可在
git tag v1.14
查看,地址:https://github.com/2xiao/my-react/tree/v1.14The text was updated successfully, but these errors were encountered: