本文共 7581 字,大约阅读时间需要 25 分钟。
之前回看React文档时,发现React已经更新到了v16.7.0版本,想起之前官方所提过的将会在未来v17.0 版本中移除componentWillMount,componentWillReceiveProps,componentWillUpdate,因此在这里简单记录一下移除的三个生命周期函数,和学习新引入的两个生命周期函数 getDerivedStateFromProps,getSnapshotBeforeUpdate。
componentWillMount 对于这个生命周期函数,以前的我最喜欢拿来干两件事,一是在这个函数里进行状态的赋值,和请求异步加载的数据,但是前者完全可以在constructor中完成,因为本身他就是用于状态的初始化的,而后者由于componentWillMount 之后会立刻执行render
,所以在第一次render
中并不能拿到异步数据。
使用getDerivedStateFromProps配合componentDidUpdate可以替代componentWillReceiveProps的所有功能,this.setState并不会触发这个函数,每次得到了新的props后会触发这个生命周期,用于更新state。值得注意的是getDerivedStateFromProps是一个static
方法,因此我们在其内部并不能使用this.setState方法去更新状态。 如果需要对比 prevProps
需要单独在 state
中维护
componentWillReceiveProps(nextProps) { if (nextProps.cards !== this.props.cards) { this.setState({ filterTableData: this.GetAllCard(nextProps.cards), }) } } static getDerivedStateFromProps(nextProps, prevState){ if(nextProps.cards !== prevState.filterTableData){ return{ filterTableData :nextProps.cards } } return null }复制代码
我在使用componentWillReceiveProps的另一个场景是在props改变使执行某些回调,在新的生命周期函数中,我们可以将这一部分功能加在componentDidUpdate中
我们可以使用getSnapshotBeforeUpdate和componentDidUpdate替代componentWillUpdate,其实在平常工作中我基本不用这个生命周期函数,不过既然遇到就在这简单记录一下 由于getSnapshotBeforeUpdate在render之后,但在节点挂载前,可以通过getSnapshotBeforeUpdate方法,获取dom信息,然后通过componentDidUpdate反映,前者返回的dom可以在componentDidUpdate的第三个参数(prevProps, prevState, snapshot)中获取到。
附上新的
上面介绍了React16带来的新的生命周期,那么为什么需要修改之前的生命周期呢? 是因为新的react引入了异步渲染机制,主要的功能是,在渲染完成前,可以中断任务,中断之后不会继续执行生命周期,而是重头开始执行生命周期。这导致上述的componentWillMount,componentWillReceiveProps,componentWillUpdate可能会被中断,导致执行多次,带来意想不到的情况。
我们都知道在react16之前,react对virtual dom 的渲染是同步的,即每次将所有操作累加起来,统计对比出所有的变化后,统一更新一次DOM树(),随着组件层级的深入,由于渲染更新一旦开始就无法停止,导致主线程长时间被占用,这也是react在动画,布局和手势等区域会有造成掉帧、延迟响应(甚至无响应)等不佳体验。
目的
为了解决这个问题,react推出了Fiber,它能够将渲染工作分割成块并将其分散到多个帧中。同时加入了在新更新进入时暂停,中止或重复工作的能力和为不同类型的更新分配优先级的能力。
分割什么
react渲染大抵可以分为 reconciler
(调度阶段)和 commit
(渲染阶段),前者用于对比,后者用于操作dom,reconciler
阶段可以算是一个从顶向下的递归算法,主要工作是对current tree
和 new tree
做计算,找出变化部分。commit
阶段是对在reconciler
阶段获取到的变化部分应用到真实的DOM树中,在绝大部分运用场景中,reconciler
阶段的时间远远超过commit
,因此fiber
选择将reconciler
阶段进行分割。
分割成什么
Fiber的拆分单位是fiber(fiber tree上的一个节点),这里引入了fiber tree的概念,fiber tree实际上是个单链表树结构,由fiber构成,fiber tree是根据和之前的virtual dom tree的树结构一模一样,只是节点携带的信息有差异。
export type Fiber = { return: Fiber|null, // 父节点 child: Fiber|null, // 子节点 sibling: Fiber|null, // 兄弟节点 alternate: Fiber|null, //diff后差异信息 .....};复制代码
完整树结构可以见
而这就是Fiber解决问题的思路,把之前无法中断的diff递归(持续占据主线程)拆分成一系列小任务,每次检查树上的一小部分,由于树上挂载了更多的上下文信息,因此可以选择是否继续下一个任务(时间是否允许当前帧(16ms)),而上面提到的alternate,则是一开始指向一个fiber tree的clone对象,每次任务完成会先将变化更新到这个克隆体上,等到diff结束,在进行commit。
export function createWorkInProgress( current: Fiber, pendingProps: any, expirationTime: ExpirationTime,): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { workInProgress = createFiber( current.tag, pendingProps, current.key, current.mode, ); workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; if (__DEV__) { // DEV-only fields workInProgress._debugID = current._debugID; workInProgress._debugSource = current._debugSource; workInProgress._debugOwner = current._debugOwner; } workInProgress.alternate = current; current.alternate = workInProgress; } else { workInProgress.pendingProps = pendingProps; workInProgress.effectTag = NoEffect; workInProgress.nextEffect = null; workInProgress.firstEffect = null; workInProgress.lastEffect = null; } workInProgress.expirationTime = expirationTime; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref; return workInProgress;}}复制代码
我们通过源码可以看见,这其实就是之前我们提到的那个克隆体,workInProgress tree上每个节点都有一个effect list,用来存放需要更新的内容。而它的alternate则指向当前的fiber节点,这一点很关键,它可以复用之前的fiber树,下一次更新时不用重新创建fiber树,而可以更新fiber树,然后对应到相应的workInProgress tree上,完成了缓存的工作。
浏览器提供的requestIdleCallback API中的Cooperative Scheduling可以让浏览器在空闲时间执行回调(开发者传入的方法),在回调参数中可以获取到当前帧剩余的时间。利用这个信息可以合理的安排当前帧需要做的事情,如果时间足够,那继续做下一个任务,如果时间不够就歇一歇,调用requestIdleCallback来获知主线程不忙的时候,再继续做任务。
源码中的computeExpirationForFiber函数,该方法用于计算fiber更新任务的最晚执行时间,进行比较后,决定是否继续做下一个任务。
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) { let expirationTime; if (expirationContext !== NoWork) { // An explicit expiration context was set; expirationTime = expirationContext; } else if (isWorking) { if (isCommitting) { // Updates that occur during the commit phase should have sync priority // by default. expirationTime = Sync; } else { // Updates during the render phase should expire at the same time as // the work that is being rendered. expirationTime = nextRenderExpirationTime; } } else { // No explicit expiration context was set, and we're not currently // performing work. Calculate a new expiration time. if (fiber.mode & ConcurrentMode) { if (isBatchingInteractiveUpdates) { // This is an interactive update expirationTime = computeInteractiveExpiration(currentTime); } else { // This is an async update expirationTime = computeAsyncExpiration(currentTime); } // If we're in the middle of rendering a tree, do not update at the same // expiration time that is already rendering. if (nextRoot !== null && expirationTime === nextRenderExpirationTime) { expirationTime -= 1; } } else { // This is a sync update expirationTime = Sync; } } if (isBatchingInteractiveUpdates) { // This is an interactive update. Keep track of the lowest pending // interactive expiration time. This allows us to synchronously flush // all interactive updates when needed. if ( lowestPriorityPendingInteractiveExpirationTime === NoWork || expirationTime < lowestPriorityPendingInteractiveExpirationTime ) { lowestPriorityPendingInteractiveExpirationTime = expirationTime; } } return expirationTime;}复制代码
源码中的中描述了任务的优先级,用ExpirationTime的到期时间方式表示任务的优先级。
export type ExpirationTime = number;export const NoWork = 0;export const Never = 1;export const Sync = MAX_SIGNED_31_BIT_INT;const UNIT_SIZE = 10;const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;// 1 unit of expiration time represents 10ms.export function msToExpirationTime(ms: number): ExpirationTime { // Always add an offset so that we don't clash with the magic number for NoWork. return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);}复制代码
取代了15版本对优先级的简单划分
module.exports = { NoWork: 0, // No work is pending. SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects. AnimationPriority: 2, // Needs to complete before the next frame. HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive. LowPriority: 4, // Data fetching, or result from updating stores. OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.};复制代码
我们知道如果需要实现组件的异步更新,肯定需要在更新前将更新任务进行存储,然后异步任务开始的时候读取更新并实现组件更新,存储更新任务就需要一个数据结构,最常见的就是栈和队列,Fiber的实现方式就是队列。
这一部分内容在源码有详细描述,感兴趣的可以继续研究下去,包括如何根据优先级插入和更新队列
对fiber源码的研究先告一段落了,之后有时间会继续研究实现原理,也会更新在下方。你的下一句话是?
1.
2.
作者:TimCope 链接:https://juejin.im/post/5da92a5af265da5b7415124a 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。