您的位置 首页 > 腾讯云社区

React源码解析之Commit第二子阶段「mutation」(下)---进击的小进进

前言

在上篇文章 React源码解析之Commit第二子阶段「mutation」(中) 中,我们讲了 「mutation」 子阶段的更新(Update)操作,接下来我们讲删除(Deletion)操作:

case Deletion: { //删除节点 commitDeletion(nextEffect); break; }一、commitDeletion()

作用:删除 DOM 节点

源码:

function commitDeletion(current: Fiber): void { //因为是 DOM 操作,所以supportsMutation为 true if (supportsMutation) { // Recursively delete all host nodes from the parent. // Detach refs and call componentWillUnmount() on the whole subtree. //删除该节点的时候,还会删除子节点 //如果子节点是 ClassComponent 的话,需要执行生命周期 API——componentWillUnmount() unmountHostComponents(current); } else { // Detach refs and call componentWillUnmount() on the whole subtree. //卸载 ref commitNestedUnmounts(current); } //重置 fiber 属性 detachFiber(current); }

解析: (1) 执行unmountHostComponents(),删除目标节点及其子节点,如果目标节点或子节点是类组件ClassComponent的话,会执行内部的生命周期 API——componentWillUnmount()

(2) 执行detachFiber(),重置fiber属性

detachFiber()的源码如下:

//重置 fiber 对象,释放内存(注意是属性值置为 null,不会删除属性) function detachFiber(current: Fiber) { // Cut off the return pointers to disconnect it from the tree. Ideally, we // should clear the child pointer of the parent alternate to let this // get GC:ed but we don't know which for sure which parent is the current // one so we'll settle for GC:ing the subtree of this child. This child // itself will be GC:ed when the parent updates the next time. //重置目标 fiber对象,理想情况下,也应该清除父 fiber的指向(该 fiber),这样有利于垃圾回收 //但是 React确定不了父节点,所以会在目标 fiber 下生成一个子 fiber,代表垃圾回收,该子节点 //会在父节点更新的时候,成为垃圾回收 current.return = null; current.child = null; current.memoizedState = null; current.updateQueue = null; current.dependencies = null; const alternate = current.alternate; //使用的doubleBuffer技术,Fiber在更新后,不用再重新创建对象,而是复制自身,并且两者相互复用,用来提高性能 //相当于是当前 fiber 的一个副本,用来节省内存用的,也要清空属性 if (alternate !== null) { alternate.return = null; alternate.child = null; alternate.memoizedState = null; alternate.updateQueue = null; alternate.dependencies = null; } }

接下来看下unmountHostComponents()

二、unmountHostComponents()

作用: 删除目标节点及其子节点,如果目标节点或子节点是类组件ClassComponent的话,会执行内部的生命周期 API——componentWillUnmount()

源码:

function unmountHostComponents(current): void { // We only have the top Fiber that was deleted but we need to recurse down its // children to find all the terminal nodes. let node: Fiber = current; // Each iteration, currentParent is populated with node's host parent if not // currentParentIsValid. let currentParentIsValid = false; // Note: these two variables *must* always be updated together. let currentParent; let currentParentIsContainer; //从上至下,遍历兄弟节点、子节点 while (true) { if (!currentParentIsValid) { //获取父节点 let parent = node.return; //将此 while 循环命名为 findParent //此循环的目的是找到是 DOM 类型的父节点 findParent: while (true) { invariant( parent !== null, 'Expected to find a host parent. This error is likely caused by ' + 'a bug in React. Please file an issue.', ); switch (parent.tag) { case HostComponent: //获取父节点对应的 DOM 元素 currentParent = parent.stateNode; currentParentIsContainer = false; break findParent; case HostRoot: currentParent = parent.stateNode.containerInfo; currentParentIsContainer = true; break findParent; case HostPortal: currentParent = parent.stateNode.containerInfo; currentParentIsContainer = true; break findParent; } parent = parent.return; } //执行到这边,说明找到了符合条件的父节点 currentParentIsValid = true; } //如果是 DOM 元素或文本元素的话(主要看这个) if (node.tag === HostComponent || node.tag === HostText) { //在目标节点被删除前,从该节点开始深度优先遍历,卸载 ref 和执行 componentWillUnmount()/effect.destroy() commitNestedUnmounts(node); // After all the children have unmounted, it is now safe to remove the // node from the tree. //我们只看 false 的情况,也就是操作 DOM 标签的情况 if (currentParentIsContainer) { removeChildFromContainer( ((currentParent: any): Container), (node.stateNode: Instance | TextInstance), ); } else { //源码:parentInstance.removeChild(child); removeChild( ((currentParent: any): Instance), (node.stateNode: Instance | TextInstance), ); } // Don't visit children because we already visited them. } //suspense 组件不看 else if ( enableSuspenseServerRenderer && node.tag === DehydratedSuspenseComponent ) { //不看这部分 } //portal 不看 else if (node.tag === HostPortal) { //不看这部分 } //上述情况都不符合,可能是一个 Component 组件 else { //卸载 ref 和执行 componentWillUnmount()/effect.destroy() commitUnmount(node); // Visit children because we may find more host components below. if (node.child !== null) { node.child.return = node; node = node.child; continue; } } //子树已经遍历完 if (node === current) { return; } while (node.sibling === null) { //如果遍历回顶点 或 遍历完子树,则直接 return if (node.return === null || node.return === current) { return; } //否则向上遍历,向兄弟节点遍历 node = node.return; if (node.tag === HostPortal) { // When we go out of the portal, we need to restore the parent. // Since we don't keep a stack of them, we will search for it. currentParentIsValid = false; } } // 向上遍历,向兄弟节点遍历 node.sibling.return = node.return; node = node.sibling; } }

解析: 我们还是只考虑HostComponent和ClassCpmonent的情况,该方法也是一个深度优先遍历的算法逻辑,所以你必须知道该算法逻辑,才能看得懂while (true) { }里面做了什么。

关于「ReactDOM里的深度优先遍历」请看: React源码解析之Commit第二子阶段「mutation」(上) 中的 「 二、ReactDOM里的深度优先遍历 」

优先遍历子节点,然后再遍历兄弟节点 (1) 如果当前节点是DOM 标签HostComponent或文本节点HostText的话

if (node.tag === HostComponent || node.tag === HostText) {

① 执行commitNestedUnmounts()

commitNestedUnmounts(node);

commitNestedUnmounts()的作用是: 在目标节点被删除前,从该节点开始深度优先遍历,卸载ref和执行 componentWillUnmount()/effect.destroy()

注意: commitNestedUnmounts()方法,不会执行removeChild()删除节点的操作

② 执行removeChild(),删除当前节点

removeChild( ((currentParent: any): Instance), (node.stateNode: Instance | TextInstance), );

removeChild()的源码如下:

export function removeChild( parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance, ): void { parentInstance.removeChild(child); }

就是调用 DOM API——removeChild,请参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/removeChild

(2) 如果当前节点是类组件ClassComponent或函数组件FunctionComponent的话(也就是最后的 else 情况),则执行commitUnmount(),卸载ref和执行componentWillUnmount()/effect.destroy()

else { //卸载 ref 和执行 componentWillUnmount()/effect.destroy() commitUnmount(node); // Visit children because we may find more host components below. if (node.child !== null) { node.child.return = node; node = node.child; continue; } }

然后就是一直循环,直到调用return,跳出无限循环。

unmountHostComponents()的逻辑其实和commitPlacement()类似,关于commitPlacement(),请看: React源码解析之Commit第二子阶段「mutation」(上)

接下来,我们讲下commitNestedUnmounts()和commitUnmount()源码

三、commitNestedUnmounts()

作用: 深度优先遍历,循环执行: 在目标节点被删除前,从该节点开始深度优先遍历,卸载该节点及其子节点 ref 和执行该节点及其子节点 componentWillUnmount()/effect.destroy()

源码:

function commitNestedUnmounts(root: Fiber): void { // While we're inside a removed host node we don't want to call // removeChild on the inner nodes because they're removed by the top // call anyway. We also want to call componentWillUnmount on all // composites before this host node is removed from the tree. Therefore // we do an inner loop while we're still inside the host node. //当在被删除的目标节点的内部时,我们不想在内部调用removeChild,因为子节点会被父节点给统一删除 //但是 React 要在目标节点被删除的时候,执行componentWillUnmount,这就是commitNestedUnmounts的目的 let node: Fiber = root; while (true) { // 卸载 ref 和执行 componentWillUnmount()/effect.destroy() commitUnmount(node); // Visit children because they may contain more composite or host nodes. // Skip portals because commitUnmount() currently visits them recursively. if ( node.child !== null && // If we use mutation we drill down into portals using commitUnmount above. // If we don't use mutation we drill down into portals here instead. (!supportsMutation || node.tag !== HostPortal) ) { node.child.return = node; node = node.child; continue; } if (node === root) { return; } while (node.sibling === null) { if (node.return === null || node.return === root) { return; } node = node.return; } node.sibling.return = node.return; node = node.sibling; } }

解析: 深度优先遍历执行commitUnmount()方法

四、commitUnmount()

作用: 同上

源码:

function commitUnmount(current: Fiber): void { //执行onCommitFiberUnmount(),查了下是个空 function onCommitUnmount(current); switch (current.tag) { //如果是 FunctionComponent 的话 case FunctionComponent: case ForwardRef: case MemoComponent: case SimpleMemoComponent: { //下面代码结构和[React源码解析之Commit第一子阶段「before mutation」](https://mp.weixin.qq.com/s/YtgEVlZz1i5Yp87HrGrgRA)中的「三、commitHookEffectList()」相似 //大致思路是循环 effect 链,执行每个 effect 上的 destory() const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any); if (updateQueue !== null) { const lastEffect = updateQueue.lastEffect; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { const destroy = effect.destroy; if (destroy !== undefined) { //安全(try...catch)执行 effect.destroy() safelyCallDestroy(current, destroy); } effect = effect.next; } while (effect !== firstEffect); } } break; } //如果是 ClassComponent 的话 case ClassComponent: { //安全卸载 ref safelyDetachRef(current); const instance = current.stateNode; //执行生命周期 API—— componentWillUnmount() if (typeof instance.componentWillUnmount === 'function') { safelyCallComponentWillUnmount(current, instance); } return; } //如果是 DOM 标签的话 case HostComponent: { //安全卸载 ref safelyDetachRef(current); return; } //portal 不看 case HostPortal: { // TODO: this is recursive. // We are also not using this parent because // the portal will get pushed immediately. if (supportsMutation) { unmountHostComponents(current); } else if (supportsPersistence) { emptyPortalContainer(current); } return; } //事件组件 的更新,暂未找到相关资料 case EventComponent: { if (enableFlareAPI) { const eventComponentInstance = current.stateNode; unmountEventComponent(eventComponentInstance); current.stateNode = null; } } } }

解析: 主要看三种情况: (1) 如果是FunctionComponent的话,则循环updateQueue上的effect链,执行每个effect 上的destory()方法

safelyCallDestroy()源码如下:

//安全(try...catch)执行 effect.destroy() function safelyCallDestroy(current, destroy) { if (__DEV__) { //删除了 dev 代码 } else { try { destroy(); } catch (error) { captureCommitPhaseError(current, error); } } }

(2) 如果是ClassComponent的话 ① 执行safelyDetachRef(),安全卸载ref

safelyDetachRef()源码如下:

function safelyDetachRef(current: Fiber) { const ref = current.ref; //ref 不为 null,如果是 function,则 ref(null),否则 ref.current=null if (ref !== null) { if (typeof ref === 'function') { if (__DEV__) { //删除了 dev 代码 } else { try { ref(null); } catch (refError) { captureCommitPhaseError(current, refError); } } } else { ref.current = null; } } }

② 执行safelyCallComponentWillUnmount(),安全调用safelyCallComponentWillUnmount()

safelyCallComponentWillUnmount()源码如下:

// Capture errors so they don't interrupt unmounting. //执行生命周期 API—— componentWillUnmount() function safelyCallComponentWillUnmount(current, instance) { if (__DEV__) { //删除了 dev 代码 } else { try { //执行生命周期 API—— componentWillUnmount() callComponentWillUnmountWithTimer(current, instance); } catch (unmountError) { captureCommitPhaseError(current, unmountError); } } }

callComponentWillUnmountWithTimer()源码如下:

//执行生命周期 API—— componentWillUnmount() const callComponentWillUnmountWithTimer = function(current, instance) { startPhaseTimer(current, 'componentWillUnmount'); instance.props = current.memoizedProps; instance.state = current.memoizedState; instance.componentWillUnmount(); stopPhaseTimer(); };

本质就是调用componentWillUnmount()方法,有一点需要注意的是,执行componentWillUnmount()时,state和props都是老state和props:

instance.props = current.memoizedProps; instance.state = current.memoizedState; instance.componentWillUnmount();

(3) 如果是HostComponent,也就是 DOM 标签的话,则执行safelyDetachRef(),安全卸载 ref

流程图GitHub

commitDeletion()/unmountHostComponents()/commitNestedUnmounts()/commitUnmount(): https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberCommitWork.js

(完)

---来自腾讯云社区的---进击的小进进

关于作者: 瞎采新闻

这里可以显示个人介绍!这里可以显示个人介绍!

热门文章

留言与评论(共有 0 条评论)
   
验证码: