react hooks之useEffect

·  阅读 43
react hooks之useEffect

前言

reacthooksreact 16.8引入的一个新特性, 旨在让开发者可以在不使用class的情况下使用state和其他的react特性, 例如生命周期

今天想和大家聊一聊useEffect的具体使用方法, 一些需要预先了解的内容可以移步react hooks之useState查看, 这里就不再赘述了

概览

useEffect是一个结合了componentDidMount, componentDidUpdatecomponentWillUnmount这3个class组件生命周期的方法, 它给我们提供了在不写class时使用上述1个或几个生命周期的能力, 同样的事, 使用uesEffect只需更少的代码即可完成

语法

在正文开始之前, 我们依旧先来看看它的语法:

  useEffect(
    () => {

    },
    []
  );
复制代码

它接收2个参数:

  1. (必填)第一个参数是个函数, 每次useEffect执行的时候就会执行这个函数
  2. (可选)第二个参数是依赖, 或者说是依赖数组, uesEffect会根据这个数组的内容的不同而在不同时机去执行第一个参数传入的函数

同时需要注意的是:

  1. 如果不传第二个参数, 那么组件每次渲染, 必填参数(函数)都会执行, 我们通常是需要让必填参数(函数)有条件的执行, 因此第二个参数一般都是需要传递的
  1. 无论我们是否传递第二个参数, 传的什么, 必填参数(函数)都至少会执行一次, 而且都是在页面渲染完毕之后执行, 也就是说初始化的时候必填参数(函数)就会执行, 至于接下来是否再次执行, 什么时候执行就取决于第二个参数传递的是什么了, 具体情况将在后面详述

接下来让我们动手写一写, 来感受一下这几个生命周期的不同写法所带来的差异

componentDidMount

相信诸位reacter对这个生命周期一定不陌生, 我们会在这个生命周期中对服务端发起请求, 去获取数据, 或者访问DOM节点

class

class中, 我们会这样写:

import React, { PureComponent } from 'react';

class BruceLee extends PureComponent {

  state = {
    name: '李小龙',
    career: '武术家, 演员',
    birthday: ''
  }

  componentDidMount() {
    console.log('componentDidMount');
    const nameNode = document.getElementById('name');
    console.log(nameNode);
  }

  render() {
    const { name, career, birthday } = this.state;

    return (
      <div>
        <div id="name">{`我叫${name}`}</div>
        <div>{`我是一名${career}`}</div>
        {
          birthday &&
          (
            <div>{`我的生日是: ${birthday}`}</div>
          )
        }
        <button
          onClick={
            () => {
              this.setState({
                birthday: '1940年11月27日'
              });
            }
          }
        >设置生日</button>
      </div>
    );
  }
}

export default BruceLee;
复制代码

不使用class

接下来我们着重看看不写class, 也就是在函数式组件中使用useEffect的时候如何表示componentDidMount, 先说结论, 这样方便查看:

当useEffect的第二个参数传[]的时候表示我们将其用作componentDidMount

具体代码如下:

import React, { useState, useEffect } from 'react';

const BruceLeeHook = () => {
  const [ name, setName ] = useState('李小龙');
  const [ career, setCareer ] = useState('武术家, 演员');
  const [ birthday, setBirthday ] = useState('');

  useEffect(
    () => {
      console.log('componentDidMount');
      const nameNode = document.getElementById('name');
      console.log(nameNode);
    },
    []
  );

  return (
    <div>
      <div id="name">{`我叫${name}`}</div>
      <div>{`我是一名${career}`}</div>
      {
        birthday &&
        (
          <div>{`我的生日是: ${birthday}`}</div>
        )
      }
      <button
        onClick={
          () => {
            setBirthday('1940年11月27日');
          }
        }
      >设置生日</button>
    </div>
  );
};

export default BruceLeeHook;
复制代码

componentDidUpdate

这个生命周期相信大家也不会陌生, 它会在页面重新渲染之后执行

class

import React, { PureComponent } from 'react';

class BruceLee extends PureComponent {

  state = {
    name: '李小龙',
    career: '武术家, 演员',
    birthday: ''
  }

  componentDidMount() {
    console.log('componentDidMount');
    const nameNode = document.getElementById('name');
    console.log(nameNode);
  }

  componentDidUpdate() {
    console.log('componentDidUpdate');
    console.log(this.state.birthday);
  }
  
  render() {
    const { name, career, birthday } = this.state;

    return (
      <div>
        <div id="name">{`我叫${name}`}</div>
        <div>{`我是一名${career}`}</div>
        {
          birthday &&
          (
            <div>{`我的生日是: ${birthday}`}</div>
          )
        }
        <button
          onClick={
            () => {
              this.setState({
                birthday: '1940年11月27日'
              });
            }
          }
        >设置生日</button>
      </div>
    );
  }
}

export default BruceLee;
复制代码

点击按钮修改statebirthday的值, 页面重新渲染, render方法执行, 然后进入到了componentDidUpdate

不使用class

接着还是着重来看看它在函数式组件中的写法:

import React, { useState, useEffect } from 'react';

const BruceLeeHook = () => {
  const [ name, setName ] = useState('李小龙');
  const [ career, setCareer ] = useState('武术家, 演员');
  const [ birthday, setBirthday ] = useState('');

  useEffect(
    () => {
      console.log('componentDidMount');
      const nameNode = document.getElementById('name');
      console.log(nameNode);
    },
    []
  );

  useEffect(
    () => {
      console.log('componentDidUpdate');
      console.log(birthday);
    },
    [ birthday ]
  );

  return (
    <div>
      <div id="name">{`我叫${name}`}</div>
      <div>{`我是一名${career}`}</div>
      {
        birthday &&
        (
          <div>{`我的生日是: ${birthday}`}</div>
        )
      }
      <button
        onClick={
          () => {
            setBirthday('1940年11月27日');
          }
        }
      >设置生日</button>
    </div>
  );
};

export default BruceLeeHook;
复制代码

就如一开始提到的, 初始化的时候useEffect的第一个参数(函数)就会执行一次, 我们可以看到一开始就会执行:

() => {
  console.log('componentDidUpdate');
  console.log(birthday);
}
复制代码

这部分代码, 然后当我们修改生日的时候会再次执行, 同时这里需要留意, 第二个参数是个数组, 里面可以传多个值, 比如我这边再加一个state:

const [ wisdom, setWisdom ] = useState('');
复制代码

同时在render方法中再额外修改一个state:

<button
  onClick={
    () => {
      setCareer('以演员为职业的武术家');
    }
  }
>设置职业</button>
复制代码

然后useEffect的代码修改如下:

useEffect(
    () => {
      console.log('componentDidUpdate');
      console.log(birthday);
      console.log(wisdom);
    },
[ birthday, wisdom ]
);
复制代码

那么最终代码如下:

import React, { useState, useEffect } from 'react';

const BruceLeeHook = () => {
  const [ name, setName ] = useState('李小龙');
  const [ career, setCareer ] = useState('武术家, 演员');
  const [ birthday, setBirthday ] = useState('');
  const [ wisdom, setWisdom ] = useState('');

  useEffect(
    () => {
      console.log('componentDidMount');
      const nameNode = document.getElementById('name');
      console.log(nameNode);
    },
    []
  );

  useEffect(
    () => {
      console.log('componentDidUpdate');
      console.log(birthday);
      console.log(wisdom);
    },
    [ birthday, wisdom ]
  );

  return (
    <div>
      <div id="name">{`我叫${name}`}</div>
      <div>{`我是一名${career}`}</div>
      {
        birthday &&
        (
          <div>{`我的生日是: ${birthday}`}</div>
        )
      }
      {
        wisdom &&
        (
          <div>{`我的名言是: ${wisdom}`}</div>
        )
      }
      <button
        onClick={
          () => {
            setBirthday('1940年11月27日');
          }
        }
      >设置生日</button>
      <button
        onClick={
          () => {
            setWisdom('be water my friend');
          }
        }
      >设置名言</button>
      <button
        onClick={
          () => {
            setCareer('以演员为职业的武术家');
          }
        }
      >设置职业</button>
    </div>
  );
};

export default BruceLeeHook;
复制代码

此时我们点击按钮会发现: 依赖中的任意一个值更新了, 函数就会执行, 但依赖之外的值改变则不会触发函数的执行

接下来, 我们着重看看这部分代码:

const BruceLeeHook = () => {
  const [ birthday, setBirthday ] = useState('');
  const [ wisdom, setWisdom ] = useState('');

  useEffect(
    () => {
      console.log('componentDidUpdate');
      console.log(birthday);
      console.log(wisdom);
    },
    [ birthday, wisdom ]
  );

  //...
};
复制代码

函数式组件的函数体部分中useEffect的函数里使用了birthdaywisdom, 这两个变量在BruceLeeHook函数体顶部被定义, 不仅如此, 它们还被传递到了useEffect的第二个参数中去, 这是因为我们要在birthdaywisdom改变的时候触发useEffect的第一个参数执行, 但如果我们这么写呢:

const BruceLeeHook = () => {
  const [ birthday, setBirthday ] = useState('');
  const [ wisdom, setWisdom ] = useState('');
  const [ career, setCareer ] = useState('武术家, 演员');

  useEffect(
    () => {
      console.log('componentDidUpdate');
      console.log(birthday);
      console.log(wisdom);
      console.log(career);
    },
    [ birthday, wisdom ]
  );

  //...
};
复制代码

我们使用了一个在依赖数组中不存在的变量career, 此时我们点击按钮修改职业, 函数不会执行, 但可以看到界面上的职业确实被修改了, 再修改birthdaywisdom中的任意一个值, 此时函数执行了, 同时新的职业被打印出来了, 但我们平时在将useEffect用作componentDidUpdate的时候不这么写, 而是这么写:

//...
  useEffect(
    () => {
      console.log('componentDidUpdate');
      console.log(birthday);
      console.log(wisdom);
      console.log(career);
    },
    [ birthday, wisdom, career ]
  );
//...
复制代码

所有在函数中需要使用的值都需要写到依赖数组中去, 这是官方文档useEffect的一个推荐写法:

If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Learn more about how to deal with functions and what to do when the array values change too often.

如果你采用某个值改变再触发函数的写法, 请确保依赖数组包含组件作用域的所有值(例如props和state), 这些值会随着时间的推移而改变, 并且被useEffect所使用, 否则, 你的代码将引用以前渲染中的陈旧值, 想要了解更多关于如何正确使用useEffect的函数和如何处理依赖数组值频繁变更问题, 可点击这两个链接查看: 在依赖数组中省略useEffect函数中所使用的值是否安全, 如何处理依赖频繁变更的问题

componentWillUnMount

这个生命周期一般用来清理事件绑定或者clearTimeoutclearInterval

class

index.js:

import React, { PureComponent } from 'react';
import BruceLee from './BruceLee';

class Index extends PureComponent {

  state = {
    switchComp: false
  }
  
  render() {
    const { switchComp } = this.state;

    return (
      <div>
        {
          !switchComp
            ? <BruceLee />
            : <div>abc</div>
        }
        <button
          onClick={
            () => {
              this.setState({
                switchComp: !switchComp
              });
            }
          }
        >切换组件</button>
      </div>
    );
  }
}

export default Index;
复制代码

BruceLee.js:

import React, { PureComponent } from 'react';

class BruceLee extends PureComponent {

  state = {
    name: '李小龙',
    career: '武术家, 演员',
    birthday: ''
  }

  componentWillUnmount() {
    console.log('componentWillUnmount');
  }
  
  render() {
    const { name, career, birthday } = this.state;

    return (
      <div>
        <div id="name">{`我叫${name}`}</div>
        <div>{`我是一名${career}`}</div>
        {
          birthday &&
          (
            <div>{`我的生日是: ${birthday}`}</div>
          )
        }
        <button
          onClick={
            () => {
              this.setState({
                birthday: '1940年11月27日'
              });
            }
          }
        >设置生日</button>
      </div>
    );
  }
}

export default BruceLee;
复制代码

切换组件, 导致BruceLee卸载的时候会触发componentWillUnmount生命周期

不使用class

依赖数组传空数组

接下来还是着重来看看使用useEffect的写法, index.js中的代码保持不变, 修改BruceLee.js:

import React, { useState, useEffect } from 'react';

const BruceLeeHook = () => {
  const [ name, setName ] = useState('李小龙');
  const [ career, setCareer ] = useState('武术家, 演员');
  const [ birthday, setBirthday ] = useState('');

  useEffect(
    () => {
      console.log('componentDidMount');
      const nameNode = document.getElementById('name');
      console.log(nameNode);

      return () => {
        console.log('componentWillUnmount');
      };
    },
    []
  );

  useEffect(
    () => {
      console.log('componentDidUpdate');
      console.log(birthday);
    },
    [ birthday ]
  );

  return (
    <div>
      <div id="name">{`我叫${name}`}</div>
      <div>{`我是一名${career}`}</div>
      {
        birthday &&
        (
          <div>{`我的生日是: ${birthday}`}</div>
        )
      }
      <button
        onClick={
          () => {
            setBirthday('1940年11月27日');
          }
        }
      >设置生日</button>
    </div>
  );
};

export default BruceLeeHook;
复制代码

当点击切换组件的时候, 组件BruceLee卸载的时候componentWillUnmount字符串会被打印, 但这个写法相对独特, 接下来我们着重来看一下:

useEffect(
  () => {
    console.log('componentDidMount');
    const nameNode = document.getElementById('name');
    console.log(nameNode);
    return () => {
      console.log('componentWillUnmount');
    };
  },
  []
);
复制代码

我们可以看到, 在useEffect的第一个参数中return了一个函数, 这个函数就是我们的componentWillUnmount生命周期

同时这里的依赖数组[], 那么就意味着这个useEffect将在组件上树之后执行, 后续就不会再执行, 此时试想一下: 如果我们不传空数组呢?

依赖数组为非空数组

index.js依旧不变, 修改BruceLee.js:

import React, { useState, useEffect } from 'react';

const BruceLeeHook = () => {
  const [ name, setName ] = useState('李小龙');
  const [ career, setCareer ] = useState('武术家, 演员');
  const [ birthday, setBirthday ] = useState('');
  const [ wisdom, setWisdom ] = useState('');

  useEffect(
    () => {
      console.log('componentDidMount');
      const nameNode = document.getElementById('name');
      console.log(nameNode);
    },
    []
  );

  useEffect(
    () => {
      console.log('componentDidUpdate');
      
      return () => {
        console.log('componentWillUnMount');
      };
    },
    [ career, birthday, wisdom ]
  );

  return (
    <div>
      <div id="name">{`我叫${name}`}</div>
      <div>{`我是一名${career}`}</div>
      {
        birthday &&
        (
          <div>{`我的生日是: ${birthday}`}</div>
        )
      }
      {
        wisdom &&
        (
          <div>{`我的名言是: ${wisdom}`}</div>
        )
      }
      <button
        onClick={
          () => {
            setBirthday('1940年11月27日');
          }
        }
      >设置生日</button>
      <button
        onClick={
          () => {
            setWisdom('be water my friend');
          }
        }
      >设置名言</button>
      <button
        onClick={
          () => {
            setCareer('以演员为职业的武术家');
          }
        }
      >设置职业</button>
    </div>
  );
};

export default BruceLeeHook;
复制代码

关键代码如下:

useEffect(
  () => {
    console.log('componentDidUpdate');
    
    return () => {
      console.log('componentWillUnMount');
    };
  },
  [ career, birthday, wisdom ]
);
复制代码

根据上面的知识我们知道, 这个函数将在[ career, birthday, wisdom ]三个值中任意一个改变的时候执行, 然后我们修改这3个值, 每次修改, 上面的关键代码都会执行, 但此时有个现象, 就是都会先打印componentWillUnMount然后再打印componentDidUpdate, 这是为什么呢?

查看文档Cleaning up an effect之后我们发现, 这是因为:

previous effect is cleaned up before executing the next effect

上一个effect将会在下一个effect执行之前被清理

也就是说:

  1. 我们第一次, 初始化的时候console.log('componentDidUpdate');执行了
  2. 而接下来更新依赖数组的值, 在第二次执行console.log('componentDidUpdate');之前, 第一次return的函数会先执行, 也就是会执行console.log('componentWillUnMount');
  3. 然后才会执行第二次的console.log('componentDidUpdate');, 以此类推

就像官方文档里提到的clean up, 这个return的方法将在下一次effect执行之前先被执行, 这在实际的编码过程中会很有用, 写点伪代码, 比如:

let pivot = 0;

const BruceLeeHook = () => {

  const handlePivote = () => {
    //因为某些原因修改了pivot的值
    pivot = 1;
  };

  useEffect(
    () => {
      if(pivot === 0) {
        //做一些什么
      }
      
      return () => {
        pivot = 0;
      };
    },
    []
  );

};

export default BruceLeeHook;
复制代码

大意就是:

  1. 一开始我们使用某个值做一些事, 期间修改了这个值, 那么条件不符, 就不再做这件事
  2. 而当组件再次上树的时候又要使用初始值再来做一开始做的事(比如单页面应用路由的切换)
  3. 但此时这个值已经在第1步被修改过了, 那么此时就需要在componentWillUnMount这个生命周期中做一个重置或者说清理的工作, 使之被重置成初始值
  4. 这样组件再次上树之后才能符合预期地走第1步的逻辑, 如此往复

好的, 那么这就是这篇文章的全部内容了, 如果你觉得这篇文章写得还不错, 别忘了给我点个赞, 如果你觉得对你有帮助, 可以点个收藏, 以备不时之需

参考文献:

useEffect

分类:
前端
标签:
分类:
前端
标签: