React官方文档:使用Effect Hook

151 阅读4分钟

数据获取,设置订阅以及手动更改React组件中的DOM都属于副作用。

componentDidMount:组件加载; componentDidUpdate:组件更新;

在React Class中,把副作用操作放到componentDidMount和componentDidUpdate中。

useEffect:componentDidMount、componentDidUpdate和componentWillUnmount三个函数的组合; 通过这个hook,可以告诉React组件需要在渲染后执行某些操作。 useEffect会在每次渲染后都执行吗?是的。默认情况下,它会在第一次渲染之后和每次更新之后都会执行。React保证每次运行effect的同时,DOM都已经更新完毕。

【与componentDidMount和componentDidUpdate不同,使用useEffect调度的effect不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect不需要同步地执行。在个别情况下(例如测量布局),有单独的useLayoutEffect Hook,其API与useEffect相同】

需要清除的effect,如订阅外部数据源。 这种情况下,清除工作是非常重要的,可以防止内存泄漏。

React class中,通常会在componentDidMount中设置订阅,并在componentWillUnmount中清除它。 `

componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); }

` 使用Hook的示例 如果你的effect返回一个函数(可选),React将会在执行清除操作时调用它:

useEffect(() => {
    function handleStatusChange(status) {
        setIsOnline(status.isOnline)
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect
    // 原本这个cleanup函数是放在componentWillUnmount中
    return function cleanup() {
        ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange);
    }
})

React何时清除effect?React会在组件卸载的时候执行清除操作。 并不是必须为 effect 中返回的函数命名。但其实也可以返回一个箭头函数或者给起一个别的名字。

解释: 为什么每次更新的时候都要运行 Effect

为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次? 忘记正确地处理componentDidUpdate是React中常见的bug来源,useEffect避免了在class组件中因为没有处理更新逻辑而导致常见的bug。 (为了处理React组件更新阶段可能导致的bug。。。

通过跳过Effect进行性能优化: 在某些情况下,每次渲染后都执行清理或者执行effect可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

。。。 对于有清除操作的effect同样适用:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。

如何处理函数以及数组频繁变化时的措施? 在依赖列表中省略函数是否安全?

// 不安全
function Example({ someProp }) {
    function doSomething() {
        console.log(someProp);
    }
    
    useEffect(() => {
        doSomething()
    }, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}

要记住effect外部的函数使用了哪些props和state很难,通常会在effect内部去声明它所需要的函数。

function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);    }

    doSomething();
  }, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)}

如果你指定了一个 依赖列表 作为 useEffectuseLayoutEffectuseMemouseCallback 或 useImperativeHandle 的最后一个参数,它必须包含回调中的所有值,并参与 React 数据流。这就包括 props、state,以及任何由它们衍生而来的东西。

这同时也允许你通过 effect 内部的局部变量来处理无序的响应:

useEffect(() => {
    let ignore = false;
    async function fetchProduct() {
        const response = await fetch('http://myapi/product/' + productId)
        const json = await response.json()
        if(!ignore) setProduct(json)
    }
    fetchProduct()
    return () => { ignore = true }
}, [productId])

如果出于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法: react.docschina.org/docs/hooks-…

  • 把函数加入 effect 的依赖但 把它的定义包裹 进 [useCallback]万不得已的情况下,你可以 把函数加入 effect 的依赖但 把它的定义包裹 进 useCallback Hook。
function ProductPage({ productId }) {
    // ✅ 用 useCallback 包裹以避免随渲染发生改变
    const fetchProduct = useCallback(() => {
        // ... Does something with productId ...
    }, [productId]); // ✅ useCallback 的所有依赖都被指定了
    
    return <ProductDetails fetchProduct={fetchProduct} />
}

function ProductDetails({ fetchProduct }) {
    useEffect(() => {
        fetchProduct();
    }, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
    // ...
}

注意在上面的案例中,我们 需要 让函数出现在依赖列表中。这确保了 ProductPage 的 productId prop 的变化会自动触发 ProductDetails 的重新获取。

如果我的 effect 的依赖频繁变化,我该怎么办?