如何在React中调用原生JS脚本并仅在Showcase组件生效
问题根因拆解
- 网上流传的useScript动态插脚本的方案不适用于自有JS脚本:这个逻辑本来是为加载第三方无法改源码的CDN脚本设计的,脚本是异步加载执行的,加载完成的时机完全不受React控制,经常出现脚本执行时canvas节点还没渲染到页面上,导致脚本找不到目标元素不生效;页面刷新时凑巧时序对上就显示正常,组件切换时时序错开就失效。
- 原脚本的全局逻辑和React的组件生命周期不匹配:原生粒子脚本一般是一加载就自启动动画循环、绑定全局事件,组件切走时只移除script标签根本不会停掉已经跑起来的
requestAnimationFrame循环,既会导致后台资源占用,切回组件时也会因为旧动画循环残留引发逻辑冲突。 - 原组件的JSX语法存在基础问题:用
class给div加类名,在React里是不生效的,React要求类名属性为className。
落地实现方案
第一步:改造自有粒子脚本为可受控的模块函数
把原来写在particles.js里的自执行全局逻辑,改造成接收canvas节点入参、返回销毁函数的可初始化函数,从根源上让脚本可以跟着React组件的生命周期走:
// 改造后的particles.js export function initParticles(canvasElement) { // 以下是原有粒子脚本的全部逻辑,和之前写的原生逻辑完全一致,只是不再直接拿全局DOM const ctx = canvasElement.getContext('2d') let rafId = null // 原有粒子初始化、移动、绘制的逻辑 function renderFrame() { // 动画绘制代码 rafId = requestAnimationFrame(renderFrame) } renderFrame() // 暴露销毁方法,供组件卸载时清理资源 return function destroy() { cancelAnimationFrame(rafId) // 把脚本里绑定的所有window、canvas事件监听全部在这里移除 } }
第二步:在Showcase组件中通过ref对接生命周期
完全不需要用动态插script的钩子,直接引入改造好的粒子模块,用React的ref拿到真实canvas节点,保证DOM渲染完成后再初始化脚本,组件卸载时自动清理动画:
import React, { useEffect, useRef } from 'react' import { initParticles } from './particles' const Showcase = () => { // 绑定canvas的真实DOM节点 const canvasRef = useRef(null) useEffect(() => { // 此时canvas已经完成挂载,节点存在,可直接初始化 const currentCanvas = canvasRef.current if (!currentCanvas) return const destroyAnim = initParticles(currentCanvas) // 组件卸载时执行清理 return () => { destroyAnim() } }, []) return ( <div className="container"> <h1><p>Learn more</p></h1> <canvas ref={canvasRef} id="canvas1"></canvas> </div> ) } export default Showcase
为什么这样能解决切换不生效的问题
React的useEffect回调是在真实DOM渲染完成后才触发的,不管是页面直接刷新进入,还是从问候轮播页切换到Showcase组件,只要组件被挂载,canvas节点就一定会先存在,粒子脚本的初始化逻辑就能正常找到目标元素运行,不会再出现脚本加载早于DOM渲染的时序问题。
同时因为组件卸载时会主动停掉动画循环、清理事件监听,重复切换组件也不会出现残留动画、内存占用的问题。
常见踩坑提醒
- 不要在JSX中使用
class设置元素类名,需要替换为className,否则样式无法生效 - 不要通过全局id选择器查找DOM元素,复杂多组件页面中容易出现同id节点导致脚本绑定错误元素
- 所有在脚本中注册的定时循环、全局事件监听,都要在销毁函数中做对应清理,避免后台资源浪费
内容的提问来源于stack exchange,提问作者Agrel
相关产品推荐
相关产品推荐

