声明Effect
来看一个简单的案例,我们希望实现一个播放视频的组件,通过页面上的按钮点击来控制播放暂停
video的DOM元素本身并没有这样的功能,只提供了pause和play的函数
因此我们会进行如下的初步的设计
1 2 3 4 5 6 7 8 9
| function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } return <video ref={ref} src={src} loop playsInline />; }
|
上述的组件中使用到了useRef
钩子,这里简单认为我们通过它来获取DOM元素的引用,对video DOM进行操作
但是上述的实现并不正确
- 首先在组件的元素渲染逻辑部分,不应当直接操作DOM
- 其次在初次渲染的时候,ref因为还没有走到生成
<video>
的部分,因此保留了null的默认值。React 在返回 JSX 之前不知道要创建什么样的 DOM,所以没有 DOM 节点可以调用 play()
或 pause()
方法
这个时候我们可以使用useEffect
钩子
1 2 3 4 5 6 7 8 9 10 11
| function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />; }
|
此时组件的渲染顺序为:
- React 会更新页面,确保
<video>
标签带着正确的 props 出现在 DOM 中
- React 将运行 Effect,Effect 将根据
isPlaying
的值调用 play()
或 pause()
Effect的触发时机
默认情况下,Effect会在组件被渲染后运行
而我们如果更新组件的state,会触发组件再次进行渲染
因此下列的代码是死循环
1 2 3 4 5 6
| const [count, setCount] = useState(0);
useEffect(() => { setCount(count + 1); });
|
设置Effect依赖项
上面提到了,上述声明的Effect默认是在每一次组件渲染后都会被执行
但是有的时候我们Effect并不希望在每一次渲染后就执行:
- 服务器连接,不能多建立,只希望组件第一次渲染的时候才建立
- 组件中其他修改state的行为不应该重新导致Effect执行
来看下面的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| function VideoPlayer({ src, isPlaying }) { const ref = useRef(null);
useEffect(() => { if (isPlaying) { console.log('调用 video.play()'); ref.current.play(); } else { console.log('调用 video.pause()'); ref.current.pause(); } });
return <video ref={ref} src={src} loop playsInline />; }
export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? '暂停' : '播放'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
|
我们多加了一个 const [text, setText] = useState('');
这就导致我们每次修改文本框的内容,都会触发一次setText,导致state更新,导致组件重新渲染,最终导致Effect被运行
这时候我们可以通过指定Effect的依赖项来解决这一问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| //只要组件渲染就都会调用 useEffect(() => { //do something });
//只有组件第一次渲染(也叫挂载)的时候才会调用 useEffect(() => { //do something }, []);
//只要a或者b发生了变化的时候就会调用 useEffect(() => { //do something }, [a,b]);
|
cleanup函数
上面我们提到了,建立连接的场景可能也会需要为Effect设置依赖项。假如我们正在编写一个 ChatRoom
组件,该组件在显示时需要连接到聊天服务器。现在为你提供了 createConnection()
API,该 API 返回一个包含 connect()
与 disconnection()
方法的对象。如何确保组件在显示时始终保持连接?
1 2 3 4
| useEffect(()=>{ const connection = createConnection(); connection.connect(); }, [])
|
这样虽然实现了只会在组件第一次渲染也就是挂载的时候建立连接,但是存在关键问题,如果切换页面,组件再次第一次渲染的时候,就会创建第二个连接
我们需要在 useEffect 中实现类似 finally
块的效果,在组件卸载(被移除)时最后一次调用,或者在每次 Effect 重新运行的时候调用
这个时候我们就需要使用 useEffect 提供的 cleanUp 函数,写法就是进行一个 return
1 2 3 4 5 6
| useEffect(()=>{ const connection = createConnection(); connection.connect(); return () => connection.disconnection(); }, [])
|
值得注意的是,在开发模式下,默认React会针对所有的Effect在第一次渲染后再调用一次渲染,目的就是为了及时发现在Effect主函数体中申请了资源但是没有cleanUp的bug