React新特性-Hooks之useEffect和useLayoutEffect(五)

Effect Hook 可以让你在函数组件中执行副作用操作,类似于class组件中的componentDidMount,componentDidUpdatecomponentWillUnmount这三种生命周期函数的组合

系列目录

什么是副作用

为了更好的理解副作用,首先我们先要知道什么是纯函数(Pure function)?

纯函数:一个函数的返回结果只依赖与它的参数,并且没有副作用,我们就把这个函数叫做纯函数。

  1. 函数的返回结果只依赖于它的参数
  2. 函数执行过程里面没有副作用
const a = 1
const foo = (b) => a + b
foo(2) // => 3

foo函数不是一个纯函数,因为它返回的结果依赖于外部变量 a,我们在不知道a的值的情况下,并不能保证foo(2)的返回值是 3。虽然foo函数的代码实现并没有变化,传入的参数也没有变化,但它的返回值却是不可预料的,现在 foo(2) 是 3,可能过了一会就是 4 了,因为 a 可能发生了变化变成了 2。

const a = 1
const foo = (x, b) => x + b
foo(1, 2) // => 3

现在 foo 的返回结果只依赖于它的参数 xbfoo(1, 2) 永远是 3。今天是 3,明天也是 3,在服务器跑是 3,在客户端跑也 3,不管你外部发生了什么变化,foo(1, 2) 永远是 3。只要 foo 代码不改变,你传入的参数是确定的,那么 foo(1, 2) 的值永远是可预料的。

看到上面纯函数的介绍,大概对副作用也有些理解了。

副作用(Side Effect):是指一个函数做了和本身运算返回值无关的事,比如:修改了全局变量、修改了传入的参数、甚至是 console.log(),所以 ajax 操作,修改 dom 都是算作副作用的;

如何使用useEffect

const TestInput= (props) => {
  const [inputValue,setInputValue] = useState('请输入')
  useEffect(()=>{
    document.title = `标题 ${inputValue}`;
  })
  return (
    <div>
      <input value={inputValue} onChange={(e)=>setInputValue(e.target.value)} /> {inputValue}
    </div>
  )
}
export default TestInput

声明了inputValue state变量,当DOM渲染完成后执行useEffect hook函数。然后使用document.title浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 inputValue 值,因为他在函数的作用域内。这个过程在每次渲染时都会发生(componentDidUpdate),包括首次渲染(componentDidMount)。每次重新渲染,都会生成新的 effect,替换掉之前的。

componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。

需要清除的 effect

上面写的例子中不需要清除副作用,还有一些副作用是需要清除的。例如订阅外部数据源、定时器等。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!相当于class组件中componentWillUnmount所要做的事情。

内存泄露:不再用到的内存,没有及时释放,就叫做内存泄漏

import React, { useState, useEffect } from 'react'
const OtherComponent = () => {
  const [count, setCount] = useState(0);
  //使用useEffect来执行副作用
  useEffect(() => {
    const timer = setInterval(() => {
      setCount((count) => count + 1)
    }, 1000)
    return () =>clearInterval(timer)
  })
  return (
    <div>
      数量:{count}
    </div>
  )
}
export default OtherComponent

useEffect可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数

React 何时清除 effect

React 会在组件卸载组件每次重新渲染时都会执行。所以React会在执行当前effect之前对上一个 effect 进行清除

 const TestInput= (props) => {
  const [inputValue,setInputValue] = useState('请输入')
  useEffect(()=>{
    console.log('进入useEffect')
    document.title = `标题 ${inputValue}`;
    return ()=>console.log('清除 effect')
  },[])
  return (
    <div>
      <input value={inputValue} onChange={(e)=>setInputValue(e.target.value)} /> {inputValue}
    </div>
  )
}
export default TestInput

上图显示了初次渲染组件之后,进入到useEffect函数中,接着每次输入setInputValue之后都会再次进入到useEffect函数中,但是在进入之前会先运行清除函数在进入到useEffect中。

使用多个 Effect 实现关注点分离

上一章中我们说过Hook的优势,其中就有关注点分离解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。

下面是class组件中的弊端。

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...

可以发现在设置document.title的逻辑被componentDidMountcomponentDidUpdate所分割,订阅逻辑又被componentDidMountcomponentWillUnmount分割。而且 componentDidMount 中同时包含了两个不同功能的代码。

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    //document.title 的effect
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    //订阅逻辑effect
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

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

Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

通过跳过 Effect 进行性能优化

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中通过在componentDidUpdate中添加对prevPropsprevState的比较逻辑解决。

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

如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

第二个参数是数组,如果数组中的其中一个值在重渲染之间发生变化,就会在次进入effect中。如果没有React 会跳过这个 effect,这就实现了性能的优化。

注意:这里的对比本次和上一次对比,也就是current count === prev count 。简单类型对比数值,复杂类型对比地址。

对于有清除操作的 effect 同样适用,同时也支持props传入的值进行对比,比如父组件频繁的渲染这时候就能利用到useEffect第二个参数来对比决定是否子组件要进入useEffect中。

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 发生变化时,重新订阅

如果你只需要组件首次渲染才进入到useEffect中,可以设置useEffect第二参数为一个空的数组即可。这样做除了首次渲染会进入后面的渲染都不会在进入。

useEffect(()=>{
  //首次会进入,以后每次渲染都不会进入,除非卸载后再次进入到该组件,才会再进入到useEffect中
},[])

useEffect和useLayoutEffect

useLayoutEffect函数与useEffect相同,但它会在所有的DOM变更之后同步调用effect。

当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.

看个例子吧:

import React, { useEffect, useLayoutEffect, useRef } from "react";
import TweenMax from "gsap/TweenMax";
import './index.less';

const Animate = () => {
    const REl = useRef(null);
    useEffect(() => {
        //下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置
        TweenMax.to(REl.current, 0, {x: 600})
    }, []);
    return (
        <div className='animate'>
            <div ref={REl} className="square">square</div>
        </div>
    );
};

export default Animate;

可以清楚的看到有一个一闪而过的方块.

改成useLayoutEffect试试

const Animate = () => {
    const REl = useRef(null);
    //变更为useLayoutEffect
    useLayoutEffect(() => {
        //下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置
        TweenMax.to(REl.current, 0, {x: 600})
    }, []);
    return (
        <div className='animate'>
            <div ref={REl} className="square">square</div>
        </div>
    );
};

可以看出,每次刷新,页面基本没变化


文章作者: jackie chen
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jackie chen !
评论
 上一篇
React新特性-Hooks之useContext(六) React新特性-Hooks之useContext(六)
useContext 跟class组件Context是一样的效果,Context能够允许数据跨域组件层级直接传递到任何的子组件身上。 详细请参考: React新特性-Context(一) 系列目录 React新特性-Context(一)
2019-07-22
下一篇 
React新特性-Hooks之useState(四) React新特性-Hooks之useState(四)
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。 系列目录 React新特性-Context(一) React新特性-Lazy和Suspense(二
2019-07-12
  目录