千家信息网

React Hooks的原理分析

发表于:2026-11-01 作者:千家信息网编辑
千家信息网最后更新 2026年11月01日,今天小编给大家分享一下React Hooks的原理分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一
千家信息网最后更新 2026年11月01日React Hooks的原理分析

今天小编给大家分享一下React Hooks的原理分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

0x00 React中的useEffect

在React中有非常多的Hooks,其中useEffect使用非常频繁,针对一些具有副作用的函数进行包裹处理,使用Hook的收益有:增强可复用性、使函数组件有状态

数据获取、订阅或手动修改DOM都属于副作用(side effects)。

effect会在React的每次render之后执行,如果是有一些需要同步的副作用代码,则可以借助useLayoutEffect来包裹,它的用法和useEffect类似

useEffect有两个参数,第一个传递一个函数,第二个参数是作为effect是否执行第一个参数中的函数是否执行的标准,换句话说,第二个参数数组中的变量是否变化来决定函数是否执行,函数是否执行依赖于第二个参数的值是否变化。在React中的比较是一个shallow equal(浅比较),对于深层次的对象嵌套,无法准确判断是否发生变化。

useEffect借助了JS的闭包机制,可以说第一个参数就是一个闭包函数,它处在函数组件的作用域中,同时可以访问其中的局部变量和函数。

多个useEffect串联,根据是否执行函数(依赖项值是否变化),依次挂载到执行链上

在类组件中,有生命周期的概念,在一些讲react hooks的文章中常常会看到如何借助useEffect来模拟 componentDidmount和 componentUnmount的例子,其第二个参数是一个空数组[],这样effect在组件挂载时候执行一次,卸载的时候执行一下return的函数。也同样是闭包的关系,通过return一个函数,来实现闭包以及在React下次运行effect之前执行该return的函数,用于清除副作用。

0x01 构建React Hooks的心智模型

个人在一开始接触react hooks的时候,觉得代码的执行有点违背常识,在对react构建合理的心智模型花了不少时间。函数组件(Functional Component)没有生命周期的概念,React控制更新,频繁的更新但是值有的会变,有的不变,反而使得程序的可理解性变差了。

不过在后来不断地学习以及运用之后,我个人觉得hooks其实是一种非常轻量的方式,在项目构建中,开发自定义的hooks,然后在应用程序中任意地方调用hook,类似于插件化(可插拔)开发,降低了代码的耦合度。但随之也带来了一些麻烦的事情,有的同学在一个hook里写了大量的代码,分离的effect也冗杂在一起,再加上多维度的变量控制,使得其他同学难以理解这个hook到底在干嘛。

针对hook的内部代码冗杂的问题,首先得明确当前hook的工作,是否可拆分工作,在hook里可以调用其他的hook,所以是否可以进行多个hook拆分?或者组织(梳理)好代码的运行逻辑?

  • React中每次渲染都有自己的effect

React中的hooks更新,笔者认为可以把其看作是一个"快照",每一次更新都是一次"快照",这个快照里的变量值是不变的,每个快照会因为react的更新而产生串行(可推导的)差异,而effect中的函数每一次都是一个新的函数。

我对于hooks的心智模型,简单来讲,就是一种插件式、有状态、有序的工具函数。

0x02 useEffect

针对useEffect,React每一次更新都会根据useEffect的第二个参数中依赖项去判断是否决定执行包裹的函数。

React会记住我们编写的effect function,effect function每次更新都会在作用于DOM,并且让浏览器在绘制屏幕,之后还会调用effect function。

整个执行过程可以简单总结如下:

1.组件被点击,触发更新count为1,通知React,"count值更新为1了"

2.React响应,向组件索要count为1的UI

3.组件:

a.给count为1时候的虚拟DOM

b.告知react完成渲染时,记得调用一下effect中的函数() => {document.title = 'you click' + 1 + 'times!'}

4.React通知浏览器绘制DOM,更新UI

5.浏览器告知ReactUI已经更新到屏幕

6.React收到屏幕绘制完成的消息后,执行effect中的函数,使得网页标题变成了"you click 1 times!"。

0x03 useRef

假如已经对上面的思想和流程已经烂熟于心,对于"快照"的概念也十分认同。

有时候,我们想在effect中拿到最新的值,而不是通过事件捕获,官方提供了useRef的hook,useRef在"生命周期"阶段是一个"同步"的变量,我们可以将值存放到其current里,以保证其值是最新的。

对于上面描述,为什么说其值是捕获而不是最新的,可以通过 setState(x => x + 1),来理解。传入的x是前一个值,x+1是新的值,在一些setTimeout异步代码里,我们想获取到最新的值,以便于同步最新的状态,所以用ref来帮助存储最新更新的值。

这种打破范式的做法,让程序有一丝丝的dirty,但确实解决了很多问题,这样做的好处,也可以表明哪些代码是脆弱的,是需要依赖时间次序的。

而在类组件中,通过 this.setState() 的做法每次拿到的也是最新的值

0x04 effect的清理

在前面的描述中或多或少涉及到对于effect的清理,只是为了便于一个理解,但描述并不完全准确。

例如下面的例子:

useEffect(() => {   ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);   return () => {     ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);   }; });

假设第一次渲染的时候props是{id: 10},第二次渲染的时候是{id: 20}。你可能会认为发生了下面的这些事:

  • React 清除了 {id: 10}的effect。

  • React 渲染{id: 20}的UI。

  • React 运行{id: 20}的effect。

但是实际情况并非如此,如果按照这种心智模型来理解,那么在清除时候,获取的值是之前的旧值,因为清除是在渲染新UI之前完成的。这和之前说到的React只会在浏览器绘制之后执行effects矛盾。

React这样做的好处是不会阻塞浏览器的一个渲染(屏幕更新)。当然,按照这个规则,effect的清除也被延迟到了浏览器绘制UI之后。那么正确的执行顺序应该是:

  • React渲染了id 20 的UI

  • React清除了id 10的effect

  • React运行id 20的effect

那么为啥effect里清除的是旧的呐?

  • 组件内的每一个函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获定义它们的那次渲染中的props和state。

那么,effect的清除并不会读取到"最新"的props,它只能读取到定义它那次渲染中props的值

人类发展的进程中淘汰的永远都是不思进取的守旧派。React中亦是如此思想,或许激进,但大多数人们总期待"新桃换旧符"。

0x05 effect的更新依赖

useEffect中的第二个参数,可以是一个参数数组(依赖数组)。React更新DOM的思想,不管过程怎样,只将结果展示给世人。

React在更新组件的时候,会对比props,通过AST等方式比较,然后仅需更新变化了的DOM。

第二个参数相当于告诉了useEffect,只要我给你的这些参数任中之一发生了改变,你就执行effect就好了。如此,便可以减少每次render之后调用effect的情况,减少了无意义的性能浪费。

那么在开发过程中,我们会尝试在组件载入时候,通过api获取远程数据,并运用于组件的数据渲染,所以我们使用了如下的一个简单例子:

useEffect(() => {   featchData(); }, []);

由于是空数组,所以只有在组件挂载(mount)时获取一遍远程数据,之后将不再执行。如果effect中有涉及到局部变量,那么都会根据当前的状态发生改变,函数是每次都会创建(每次都是创建的新的匿名函数)。

function Counter() {   const [count, setCount] = useState(0);    useEffect(() => {     const id = setInterval(() => {       setCount(count + 1);     }, 1000);     return () => clearInterval(id);   }, []);    return 

{count}

; }

你可能会认为上面的例子,会在组件加载后,每秒UI上count+1,但实际情况是只会执行一次。为什么呐?是不是觉得有些违反直觉了?

因为,并没有给effect的依赖项加入count,effect只会在第一次渲染时候,创建了一个匿名函数,尽管通过了setInterval包裹,每秒去执行count + 1,但是count的值始终是为0,所以在UI表现上永远渲染的是1。

当然,通过一些规则,我们可以通过加上count来改变其值,或者通过useRef,或者通过setState(x => x+1),模式来实现获取最新的值。例如下面的黑科技操作:

// useRef function Example() {   const [count, setCount] = useState(0);   const countRef = useRef(count);   countRef.current = count; // 假如这一行代码放到effect函数中会怎么样呐?可以思考下!   // answer: 在effect中count是effect匿名函数声明时就有了,值就是0,那么拿到的count值自然也是渲染前(本次props中的值)的count(值为0,再次复盘理解下快照的概念),但由于依赖数组中并不存在任何依赖,所以该匿名函数不会二次执行。   // 但,由于setInterval的原因,函数会不停地setCount,关键是其中的参数了,countRef.current = count;取到的值是第一次快照时候的值0,所以其更新的值永远为0+1 = 1。这样的结果是符合预期规则的。   // 那为什么放在外面就好了呐?因为countRef.current同步了count的最新值,每次render前就拿到了新的count值,并且赋值给countRef.current,由于ref的同步特性(及时性、统一性),所以循环中获取的countRef.current也是最新的值,故而能实现计数效果    useEffect(() => {     const id = setInterval(() => {       setCount(countRef.current + 1);     }, 1000);     return () => clearInterval(id);   }, []);    return 

{count}

; } // setState传入函数 function Example() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(x => x + 1); // 传递参数为一个函数时候,默认传递的第一个参数是之前的值,这是useState的hook在处理 }, 1000); return () => clearInterval(id); }, []); return

{count}

; } // 使用useReducer function Counter({ step }) { const [count, dispatch] = useReducer(reducer, 0); function reducer(state, action) { if (action.type === 'tick') { return state + step; } else { throw new Error(); } } useEffect(() => { const id = setInterval(() => { dispatch({ type: 'tick' }); }, 1000); return () => clearInterval(id); }, [dispatch]); return

{count}

; }

上面的做法其实有些自欺欺人了,可以看到如下图中的log,在setInterval匿名函数中count变量的值并没有发生改变,这可能会给我们的业务带来一些风险。

demo示例

不过一般情况下,如果不是对业务或程序有充分的了解,我并不建议大家这样做。

对于依赖,首先得诚实地写入相关联的参数,其次,可以优化effect,考虑是否真的需要某参数,是否可以替换?

依赖项中dispatch、setState、useRef包裹的值都是不变的,这些参数都可以在依赖项中去除。

依赖项是函数

可以把函数定义到useEffect中,这样添加的依赖变成了函数的参数,这样子,useEffect就无需添加xxx函数名作为依赖项了。

另外如果单纯把函数名放到依赖项中,如果该函数在多个effects中复用,那么在每一次render时,函数都是重新声明(新的函数),那么effects就会因新的函数而频繁执行,这与不添加依赖数组一样,并没有起到任何的优化效果,那么该如何改善呐?

方法一:

如果该函数没有使用组件内的任何值,那么就把该函数放到组件外去定义,该函数就不在渲染范围内,不受数据流影响,所以其永远不变

方法二:

用useCallback hook来包装函数,与useEffect类似,其第二个参数也是作为函数是否更新的依赖项

0x06 竞态

常见于异步请求数据,先发后到,后发先到的问题,这就叫做竞态,如果该异步函数支持取消,则直接取消即可

那么更简单的做法,给异步加上一个boolean类型的标记值,就可以实现取消异步请求

function Article({ id }) {   const [article, setArticle] = useState(null);    useEffect(() => {     let didCancel = false;      async function fetchData() {       const article = await API.fetchArticle(id);       if (!didCancel) {         setArticle(article);       }     }      fetchData();      return () => {       didCancel = true;     };   }, [id]);    // ... }

按照之前的规则,例如id=19,并且获取数据的时间为30s,变成了id=20,其获取数据的时间仅需5s,那么执行顺序应该如下:

  1. 鸿蒙官方战略合作共建--HarmonyOS技术社区

  2. id=19组件卸载,didCancle=true,当id=19异步请求收到数据时30s后,由于!didCancle === false,则不执行数据更新

  3. id=20,因id改变,首先设置了didCancle=false,请求获取数据,5s后拿到了数据,然后更新数据,最后将更新后数据渲染到屏幕

以上就是"React Hooks的原理分析"这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注行业资讯频道。

函数 更新 参数 组件 数据 时候 代码 快照 数组 面的 变量 浏览器 浏览 包裹 屏幕 变化 同步 例子 做法 副作用 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 互联网科技巨头监管 腾讯阿里互联网科技 与互联网科技公司 互联网金融公司科技 万易互联网科技有限公司 远信互联网科技 银优互联网科技有限公司 郑州华宇互联网科技 一五八零互联网科技 上海亿帐通互联网科技 卓越科技互联网 招聘 中国最先进互联网科技公司 北京大学互联网科技怎么样 顶级互联网科技成果有哪些 本届互联网博览会上的黑科技 伊宁互联网科技做什么的 互联网公司高科技产品 上海互联网科技公司ppt 萌小萌互联网科技河南有限公司 华人运通互联网科技公司 重庆互联网络科技 互联网科技公司电话 安徽鑫晟辉互联网科技 伴你行互联网科技有限公司 老年人脱离互联网科技的作文 成都互联网科技公司工作好不好 翼展科技 首家互联网医院 启辰互联网科技 广州百诚互联网科技有限公司官网 半导体行业属于互联网高科技类吗
0