今天本来正在工位上写着一段很普通的业务代码,将其简化后大致如下:

function App(props: any) {		// 父组件
const subRef = useRef<any>(null)
const [forceUpdate, setForceUpdate] = useState<number>(0) const callRef = () => {
subRef.current.sayName() // 调用子组件的方法
} const refreshApp = () => { // 模拟父组件刷新的方法
setForceUpdate(forceUpdate + 1)
} return <div>
<SubCmp1 refreshApp={refreshApp} callRef={callRef} />
<SubCmp2 ref={subRef} />
</div>
} class SubCmp1 extends React.Component<any, any> { // 子组件1
constructor(props: any) {
super(props)
this.state = {
count: 0
}
} add = () => {
this.props.refreshApp() // 会导致父组件重渲染的操作 // 修改自身数据,并在回调函数中调用外部方法
this.setState({ count: this.state.count + 1 }, () => {
this.props.callRef()
})
} render() {
return <div>
<button onClick={this.add}>Add</button>
<span>{this.state.count}</span>
</div>
}
} const SubCmp2 = forwardRef((props: any, ref) => { // 子组件2 useImperativeHandle(ref, () => {
return {
sayName: () => {
console.log('SubCmp2')
}
}
}) return <div>SubCmp2</div>
})

代码结构其实非常简单,一个父组件包含有两个子组件。其中的组件2因为要在父组件中调用它的内部方法,所以用forwardRef包裹,并通过useImperativeHandle向外暴露方法。组件1则是通过props传递了两个父组件的方法,一个是用于间接地访问组件2中的方法,另一个则是可能导致父组件重渲染的方法(当然这种结构的安排明显是不太合理的,但由于项目历史包袱的原因咱就先不考虑这个问题了\doge)。

然后当我满心欢喜地Click组件时,一片红色的Error映入眼帘:

在几个关键位置加上打印:

const callRef = (str) => {
console.log(str, ' --- ', subRef.current)
} add = () => {
this.props.callRef('打印1') this.props.refreshApp()
this.setState({ count: this.state.count + 1 }, () => {
this.props.callRef('打印2') setTimeout(() => {
this.props.callRef('打印3')
}, 0)
})
}

结果:

有点amazing啊。在调用前ref.current是有正确值的,在setState的回调中ref.current变为null了,而在setState的回调中加上一个异步后,立即又变为正确值了。

要debug这个问题,一个非常关键的位置就在setState的回调函数。熟悉React内部渲染流程的同学,应该知道,在React触发更新之后的commit阶段,也就是在React更新完DOM之后,针对fiber节点的类型分别做不同的处理(位于commitLifeCycles方法)。例如class组件中,会同步地执行setState的回调;函数组件的话,则会同步地执行useLayoutEffect的回调函数。

带着这个前提知识的情况下,我们给useImperativeHandle加个断点。因为对于其他常见的hookclass组件生命周期在React更新渲染中的执行时机都是比较熟悉的,唯独这个useImperativeHandle内部机制还不太了解,然我们看看代码在进入该断点时的执行栈是怎样的:

首先,在左侧的callstack面板里看到了commitLifeCycles方法,说明 useImperativeHandle这个hook也是在更新渲染后的commit同步执行的。接着我们进去impreativeHandleEffect,也就是useImperativeHandle回调函数的上一层:

方法体里先判断父组件传入的ref的类型。如果是一个函数,则将执行useImperativeHandle回调函数执行后的对象传入去并执行;否则将对象赋值到ref.current上。但这两种情况都会返回一个清理副作用的函数,而这个清理函数的任务就是——把我的ref.current给置为null !?

抓到这个最重要的线索了,赶紧给这个清理函数打个断点,然后再触发一次更新看下:

这个清理函数是在commitMutationEffects时期执行的;commitMutationEffects里做的主要工作就是就是fiber节点的类型执行需要操作的副作用(位于commitWork方法),例如对DOM的增删改,以及我们熟知的useLayoutEffect的清理函数也是在这时候完成的。

到目前为止,引发报错问题的整条链路就清晰了:

在触发更新后,在commit阶段的commitMutationEffects部分会先执行useImperativeHandle的清理函数,自这之后ref.current就被置为了null

接着才到commitLayoutEffects,该部分会执行setStateuseLayoutEffectuseImpreativeHandle这些方法的回调。

依据React以深度优先遍历方式生成fiber树且边生成边收集副作用的规则,子组件1中setState回调会比useImpreativeHandle的回调先执行,那么此时ref.current仍然还为null

从源码入手探究一个因useImperativeHandle引起的Bug的更多相关文章

  1. 从源码入手,一文带你读懂Spring AOP面向切面编程

    之前<零基础带你看Spring源码--IOC控制反转>详细讲了Spring容器的初始化和加载的原理,后面<你真的完全了解Java动态代理吗?看这篇就够了>介绍了下JDK的动态代 ...

  2. [源码分析] 从源码入手看 Flink Watermark 之传播过程

    [源码分析] 从源码入手看 Flink Watermark 之传播过程 0x00 摘要 本文将通过源码分析,带领大家熟悉Flink Watermark 之传播过程,顺便也可以对Flink整体逻辑有一个 ...

  3. 使用CEF(三)— 从CEF官方Demo源码入手解析CEF架构与CefApp、CefClient对象

    在上文<使用CEF(2)- 基于VS2019编写一个简单CEF样例>中,我们介绍了如何编写一个CEF的样例,在文章中提供了一些代码清单,在这些代码清单中提到了一些CEF的定义的类,例如Ce ...

  4. JVM源码分析之一个Java进程究竟能创建多少线程

    JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...

  5. 自己根据java的LinkedList源码编写的一个简单的LinkedList实现

    自己实现了一个简单的LinkedList /** * Create by andy on 2018-07-03 11:44 * 根据 {@link java.util.LinkedList}源码 写了 ...

  6. nginx源码层面探究request_time、upstream_response_time、upstream_connect_time与upstream_header_time指标具体含义

    背景概述 最近计划着重分析一下线上各api的HTTP响应耗时情况,检查是否有接口平均耗时.99分位耗时等相关指标过大的情况,了解到nginx统计请求耗时有四个指标:request_time.upstr ...

  7. [ASP.NET]分析MVC5源码,并实现一个ASP.MVC

    本节内容不是MVC入门教程,主要讲MVC原理,实现一个和ASP.NET MVC类似基本原理的项目. MVC原理是依赖于ASP.NET管道事件基础之上的.对于这块,可阅读上节内容 [ASP.NET]谈谈 ...

  8. Fresco源码解析 - 创建一个ImagePipeline(一)

    在Fresco源码解析 - 初始化过程分析章节中, 我们分析了Fresco的初始化过程,两个initialize方法中都用到了 ImagePipelineFactory类. ImagePipeline ...

  9. 关于FastDFS Java客户端源码中的一个不太明白的地方

    下面代码是package org.csource.fastdfs下TrackerGroup.java文件中靠近结束的一段代码,我下载的这个源码的版本是1.24. /** * return connec ...

随机推荐

  1. Luogu3694 邦邦的大合唱站队 (状压DP)

    状态由\(从前往后排好的长度\)和\(排好的团队\)决定,\(DP\)方程挺有思考价值的. #include <iostream> #include <cstdio> #inc ...

  2. 初次认识 Canvas

    画布的概念 Canvas(画布)可以用于动画.游戏画面.数据可视化.图片编辑以及实时视频处理等方面.画布在 HTML5 中是通过canvas标签来表现,通过 JavaScript 提供的画布 API, ...

  3. 轻松月薪过万,NISP证书含金量有多重|NISP管理中心|网安伴|nisp

    nisp一级证书含金量 NISP一级证书是面向各个行业工作人员信息安全意识普及化和网络信息安全基础培训的国家级验证.持NISP一级证书可以从信息安全保密较高的单位得到加分.证书由中国信息安全测评中心授 ...

  4. Downie for Mac最强视频下载工具(支持B站优酷土豆腾讯等)

    我搜集到的一款简单拖放链接到Downie,它就会下载该网站上的视频.理论可以下载各种视频网站上的视频! 应用介绍 Downie 是一款Mac平台上的优秀视频下载软件,使用非常简单,只需将下载链接放置D ...

  5. [CF1539F] Strange Array (线段树)

    题面 有一个长度为 n \tt n n 的序列 a \tt a a ,对于每一个位置 i ∈ [ 1 , n ] \tt i\in[1,n] i∈[1,n]: 选择一个区间 [ l , r ] \tt ...

  6. openstack中Keystone组件简解

    一.Keystone服务概述 在Openstack框架中,keystone(Openstack Identity Service)的功能是负责验证身份.校验服务规则和发布服务令牌的,它实现了Opens ...

  7. VS Code 之KoroFileHeader插件

    设置 在vscode左下角点击设置按钮,选择"设置",然后输入"fileheader", 文件头部注释:Fileheader:custom Made 函数注释: ...

  8. RTSP播放器开发填坑之道

    好多开发者提到,在目前开源播放器如此泛滥的情况下,为什么还需要做自研框架的RTSP播放器,自研和开源播放器,到底好在哪些方面?以下大概聊聊我们的一点经验,感兴趣的,可以关注 github: 1. 低延 ...

  9. 接口测试神器Apifox,亲测好用!

    自己关注的公众号比较多,之前有收到过有关 Apifox 的文章,自己也是大致看看,还没有用过它! 最近看到比较多有关 Apifox 的文章,所以自己就花了点时间去研究它,使用完后发现确实比Postma ...

  10. windows清理必看

    清理缓存 代码如下 介绍此文件夹都是缓存文件全选删除即可 ctrl+A全选shift+del强制删除(不会添加到回收站) %temp% 找到C盘右击属性选择想要删除的文件进行清理即可 清理完点击清理系 ...