React组件仅渲染一次时onload函数为何被调用两次?
问题根本原因
- 副作用执行位置错误:创建并插入script标签属于和外部系统交互的副作用逻辑,直接写在组件函数的渲染执行流中,组件每次重渲染都会重复执行该DOM操作。
- React严格模式的双执行机制:React 18及以上版本开发环境默认开启严格模式,组件首次挂载时会主动执行「渲染→模拟卸载→二次渲染挂载」流程,用来检测未正确清理的副作用。手动插入到body下的script标签属于原生DOM操作,完全脱离React的虚拟DOM管控,模拟卸载阶段不会被自动清除,两次组件执行就会向body插入两个相同的谷歌GSI脚本标签,和排查到的「script标签被重复追加两次」的现象完全吻合。
- 拦截标记完全失效:代码中定义的
flag是组件函数内部的普通局部变量,每次组件执行时都会被重新初始化为true;两个script标签各自绑定的injectWindowInterval回调,闭包引用的是不同执行上下文中独立的flag变量,互相不影响,判断拦截逻辑根本无法跨实例生效,两个脚本加载完成后都会触发回调执行。 - 日志判断偏差:控制台仅显示一次"fire"是浏览器控制台的默认日志聚合规则导致的——连续输出的完全相同的日志会被自动折叠合并,实际组件渲染函数已经执行了两次。
修复方案
将脚本加载逻辑移入useEffect副作用钩子,增加清理逻辑避免重复插入脚本,使用useRef存储加载状态保证标记跨渲染周期持久化:
import { useEffect, useRef } from 'react'; const Header = () => { const scriptLoadedRef = useRef(false); useEffect(() => { // 已存在加载标记或谷歌对象已初始化时直接跳过 if (scriptLoadedRef.current || window.google) return; scriptLoadedRef.current = true; const script = document.createElement("script"); script.src = "https://accounts.google.com/gsi/client"; script.async = true; script.onload = () => { console.log(window.google); }; document.body.appendChild(script); // 组件卸载时清理手动插入的script标签,重置加载标记 return () => { if (script.parentNode) { script.parentNode.removeChild(script); } scriptLoadedRef.current = false; }; }, []); return ( <div className="header"> <h3 className="header__title">RE</h3> </div> ); };
补充:如果生产环境也出现相同问题,说明
<Header />组件在路由/页面中被重复挂载,上述修复方案同样可以解决该场景下的重复加载问题。
内容的提问来源于stack exchange,提问作者Rajat Aggarwal
相关产品推荐
相关产品推荐

