useEffect

1、什么是useEffect

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

这么一看也许会有点不明白…看下面这个例子:

  1. import React, { useState, useEffect } from 'react';
  2. function Example() {
  3. const [count, setCount] = useState(0);
  4. useEffect(() => {
  5. document.title = `You clicked ${count} times`;
  6. });
  7. return (
  8. <div>
  9. <p>You clicked {count} times</p>
  10. <button onClick={() => setCount(count + 1)}>
  11. Click me
  12. </button>
  13. </div>
  14. );
  15. }

useEffect 做了什么?

通过使用这个 Hook,开发者可以告诉 React 组件需要在渲染后执行某些操作。React 会保存传递的函数(将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,设置了 documenttitle 属性,不过也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffect

将 useEffect 放在组件内部可以在 effect 中直接访问 count state 变量(或其他 props)。不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗?

是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(稍后会谈到如何控制它。)可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕 如果熟悉 React class 的生命周期函数可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount这三个函数的组合。

2、如何使用useEffect

2.1 实现componentDidMount 的功能

useEffect的第二个参数为一个空数组,初始化调用一次之后不再执行,相当于componentDidMount

  1. function Demo () {
  2. useEffect(() => {
  3. console.log('hello world')
  4. }, [])
  5. return (
  6. <div>
  7. hello world
  8. </div>
  9. )
  10. }
  11. // 等价于
  12. class Demo extends Component {
  13. componentDidMount() {
  14. console.log('hello world')
  15. }
  16. render() {
  17. return (
  18. <div>
  19. hello world
  20. </div>
  21. );
  22. }
  23. }

2.2 实现组合 componentDidMount componentDidUpdate 的功能

useEffect没有第二个参数时,组件的初始化和更新都会执行。

  1. class Example extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. count: 0
  6. };
  7. }
  8. componentDidMount() {
  9. document.title = `You clicked ${this.state.count} times`;
  10. }
  11. componentDidUpdate() {
  12. document.title = `You clicked ${this.state.count} times`;
  13. }
  14. render() {
  15. return (
  16. <div>
  17. <p>You clicked {this.state.count} times</p>
  18. <button onClick={() => this.setState({ count: this.state.count + 1 })}>
  19. Click me
  20. </button>
  21. </div>
  22. );
  23. }
  24. }
  25. // 等价于
  26. import React, { useState, useEffect } from 'react';
  27. function Example() {
  28. const [count, setCount] = useState(0);
  29. useEffect(() => {
  30. document.title = `You clicked ${count} times`;
  31. });
  32. return (
  33. <div>
  34. <p>You clicked {count} times</p>
  35. <button onClick={() => setCount(count + 1)}>
  36. Click me
  37. </button>
  38. </div>
  39. );
  40. }

2.3 实现组合 componentDidMount componentWillUnmount 的功能

useEffect返回一个函数,这个函数会在组件卸载时执行。

  1. class Example extends Component {
  2. constructor (props) {
  3. super(props);
  4. this.state = {
  5. count: 0
  6. }
  7. }
  8. componentDidMount() {
  9. this.id = setInterval(() => {
  10. this.setState({count: this.state.count + 1})
  11. }, 1000);
  12. }
  13. componentWillUnmount() {
  14. clearInterval(this.id)
  15. }
  16. render() {
  17. return <h1>{this.state.count}</h1>;
  18. }
  19. }
  20. // 等价于
  21. function Example() {
  22. const [count, setCount] = useState(0);
  23. useEffect(() => {
  24. const id = setInterval(() => {
  25. setCount(c => c + 1);
  26. }, 1000);
  27. return () => clearInterval(id);
  28. }, []);
  29. return <h1>hello world</h1>
  30. }

3、useEffect使用的坑

3.1 无限循环

useEffect的第二个参数传数组传一个依赖项,当依赖项的值发生变化,都会触发useEffect执行。请看下面的例子:
App组件显示了一个项目列表,状态和状态更新函数来自与useState这个hooks,通过调用useState,来创建App组件的内部状态。初始状态是一个object,其中的hits为一个空数组,目前还没有请求后端的接口。

  1. import React, { useState } from 'react';
  2. function App() {
  3. const [data, setData] = useState({ hits: [] });
  4. return (
  5. <ul>
  6. {data.hits.map(item => (
  7. <li key={item.objectID}>
  8. <a href={item.url}>{item.title}</a>
  9. </li>
  10. ))}
  11. </ul>
  12. );
  13. }
  14. export default App;

为了获取后端提供的数据,接下来将使用axios来发起请求,同样也可以使用fetch,这里会使用useEffect来隔离副作用。

  1. import React, { useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. useEffect(async () => {
  6. const result = await axios(
  7. 'http://localhost/api/v1/search?query=redux',
  8. );
  9. setData(result.data);
  10. });
  11. return (
  12. <ul>
  13. {data.hits.map(item => (
  14. <li key={item.objectID}>
  15. <a href={item.url}>{item.title}</a>
  16. </li>
  17. ))}
  18. </ul>
  19. );
  20. }
  21. export default App;

useEffect中,不仅会请求后端的数据,还会通过调用setData来更新本地的状态,这样会触发view的更新。
但是,运行这个程序的时候,会出现无限循环的情况。useEffect在组件mount时执行,但也会在组件更新时执行。因为在每次请求数据之后都会设置本地的状态,所以组件会更新,因此useEffect会再次执行,因此出现了无限循环的情况。只想在组件mount时请求数据。可以传递一个空数组作为useEffect的第二个参数,这样就能避免在组件更新执行useEffect,只会在组件mount时执行。

  1. import React, { useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. useEffect(async () => {
  6. const result = await axios(
  7. 'http://localhost/api/v1/search?query=redux',
  8. );
  9. setData(result.data);
  10. }, []);
  11. return (
  12. <ul>
  13. {data.hits.map(item => (
  14. <li key={item.objectID}>
  15. <a href={item.url}>{item.title}</a>
  16. </li>
  17. ))}
  18. </ul>
  19. );
  20. }
  21. export default App;

useEffect的第二个参数可用于定义其依赖的所有变量。如果其中一个变量发生变化,则useEffect会再次运行。如果包含变量的数组为空,则在更新组件时useEffect不会再执行,因为它不会监听任何变量的变更。
再看这个例子:业务场景:需要在页面一开始时得到一个接口的返回值,取调用另一个接口。思路是,先设置这个接口的返回值为data=[], 等到数据是再去请求另一个接口,即data作为useEffect的第二个参数传入。但是不知道为什么会造成死循环,拿不到想要的结果。直到在官网看到这个例子:
image.png
useEffect会比较前一次渲染和后一次渲染的值,如果所设置的data=[],那么即使后一次渲染的data也为[],那么[]===[]false,所以才会造成useEffect会一直不停的渲染,所以把data的初始值改为undefined,试了一下果然可以。 :::success 结论:useEffect的不作为componentDidUnmount的话,传入第二个参数时一定注意:第二个参数不能为引用类型,引用类型比较不出来数据的变化,会造成死循环 :::

3.2 使用async await 时的报错

在代码中,使用async / await从第三方API获取数据。如果对async/await熟悉的话,每个async函数都会默认返回一个隐式的promise。但是,useEffect不应该返回任何内容。这就是为什么会在控制台日志中看到以下警告:

Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect

这就是为什么不能直接在useEffect中使用async函数,因此,可以不直接调用async函数,而是像下面这样:

  1. function App() {
  2. const [data, setData] = useState({ hits: [] });
  3. useEffect(() => {
  4. const fetchData = async () => {
  5. const result = await axios(
  6. 'http://localhost/api/v1/search?query=redux',
  7. );
  8. setData(result.data);
  9. };
  10. fetchData();
  11. }, []);
  12. return (
  13. <ul>
  14. {data.hits.map(item => (
  15. <li key={item.objectID}>
  16. <a href={item.url}>{item.title}</a>
  17. </li>
  18. ))}
  19. </ul>
  20. );
  21. }

4、useEffect在实战中的应用

4.1 响应更新

很多情况下,需要响应用户的输入,然后再请求。这个时候会引入一个input框,监听query值的变化:

  1. import axios from 'axios';
  2. function App() {
  3. const [data, setData] = useState({ hits: [] });
  4. const [query, setQuery] = useState('redux');
  5. useEffect(() => {
  6. const fetchData = async () => {
  7. const result = await axios(
  8. 'http://localhost/api/v1/search?query=redux',
  9. );
  10. setData(result.data);
  11. };
  12. fetchData();
  13. }, []);
  14. return (
  15. <Fragment>
  16. <input
  17. type="text"
  18. value={query}
  19. onChange={event => setQuery(event.target.value)}
  20. />
  21. <ul>
  22. {data.hits.map(item => (
  23. <li key={item.objectID}>
  24. <a href={item.url}>{item.title}</a>
  25. </li>
  26. ))}
  27. </ul>
  28. </Fragment>
  29. );
  30. }

有个query值,已经更新query的逻辑,还需要将这个query值传递给后台,这个操作会在useEffect中进行前面说了,目前的useEffect只会在组件mount时执行,并且useEffect的第二个参数是依赖的变量,一旦这个依赖的变量变动,useEffect就会重新执行,所以需要添加query为useEffect的依赖:

  1. function App() {
  2. const [data, setData] = useState({ hits: [] });
  3. const [query, setQuery] = useState('redux');
  4. useEffect(() => {
  5. const fetchData = async () => {
  6. const result = await axios(
  7. `http://localhost/api/v1/search?query=${query}`,
  8. );
  9. setData(result.data);
  10. };
  11. fetchData();
  12. }, [query]);
  13. return (
  14. ...
  15. );
  16. }

一旦更改了query值,就可以重新获取数据。但这会带来另一个问题:query的任何一次变动都会请求后端,这样会带来比较大的访问压力。这个时候需要引入一个按钮,点击这个按钮再发起请求。

  1. function App() {
  2. const [data, setData] = useState({ hits: [] });
  3. const [query, setQuery] = useState('redux');
  4. const [search, setSearch] = useState('');
  5. useEffect(() => {
  6. const fetchData = async () => {
  7. const result = await axios(
  8. `http://localhost/api/v1/search?query=${query}`,
  9. );
  10. setData(result.data);
  11. };
  12. fetchData();
  13. }, [query]);
  14. return (
  15. <Fragment>
  16. <input
  17. type="text"
  18. value={query}
  19. onChange={event => setQuery(event.target.value)}
  20. />
  21. <button type="button" onClick={() => setSearch(query)}>
  22. Search
  23. </button>
  24. <ul>
  25. {data.hits.map(item => (
  26. <li key={item.objectID}>
  27. <a href={item.url}>{item.title}</a>
  28. </li>
  29. ))}
  30. </ul>
  31. </Fragment>
  32. );
  33. }

可以看到上面添加了一个新的按钮,然后创建新的组件state: search。每次点击按钮时,会把search的值设置为query,这个时候需要修改useEffect中的依赖项为search,这样每次点击按钮,search值变更,useEffect就会重新执行,避免不必要的变更:

  1. function App() {
  2. const [data, setData] = useState({ hits: [] });
  3. const [query, setQuery] = useState('redux');
  4. const [search, setSearch] = useState('redux');
  5. useEffect(() => {
  6. const fetchData = async () => {
  7. const result = await axios(
  8. `http://localhost/api/v1/search?query=${search}`,
  9. );
  10. setData(result.data);
  11. };
  12. fetchData();
  13. }, [search]);
  14. return (
  15. ...
  16. );
  17. }
  18. export default App;

此外,search state的初始状态设置为与query state 相同的状态,因为组件首先会在mount时获取数据。所以简单点,直接将的要请求的后端URL设置为search state的初始值。

  1. function App() {
  2. const [data, setData] = useState({ hits: [] });
  3. const [query, setQuery] = useState('redux');
  4. const [url, setUrl] = useState(
  5. 'http://localhost/api/v1/search?query=redux',
  6. );
  7. useEffect(() => {
  8. const fetchData = async () => {
  9. const result = await axios(url);
  10. setData(result.data);
  11. };
  12. fetchData();
  13. }, [url]);
  14. return (
  15. <Fragment>
  16. <input
  17. type="text"
  18. value={query}
  19. onChange={event => setQuery(event.target.value)}
  20. />
  21. <button
  22. type="button"
  23. onClick={() =>
  24. setUrl(`http://localhost/api/v1/search?query=${query}`)
  25. }
  26. >
  27. Search
  28. </button>
  29. <ul>
  30. {data.hits.map(item => (
  31. <li key={item.objectID}>
  32. <a href={item.url}>{item.title}</a>
  33. </li>
  34. ))}
  35. </ul>
  36. </Fragment>
  37. );
  38. }

4.2 如何处理Loading和Error

良好的用户体验是需要在请求后端数据,数据还没有返回时展现loading的状态,因此,还需要添加一个loading的state

  1. import React, { Fragment, useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. const [query, setQuery] = useState('redux');
  6. const [url, setUrl] = useState(
  7. 'http://hn.algolia.com/api/v1/search?query=redux',
  8. );
  9. const [isLoading, setIsLoading] = useState(false);
  10. useEffect(() => {
  11. const fetchData = async () => {
  12. setIsLoading(true);
  13. const result = await axios(url);
  14. setData(result.data);
  15. setIsLoading(false);
  16. };
  17. fetchData();
  18. }, [url]);
  19. return (
  20. <Fragment>
  21. <input
  22. type="text"
  23. value={query}
  24. onChange={event => setQuery(event.target.value)}
  25. />
  26. <button
  27. type="button"
  28. onClick={() =>
  29. setUrl(`http://localhost/api/v1/search?query=${query}`)
  30. }
  31. >
  32. Search
  33. </button>
  34. {isLoading ? (
  35. <div>Loading ...</div>
  36. ) : (
  37. <ul>
  38. {data.hits.map(item => (
  39. <li key={item.objectID}>
  40. <a href={item.url}>{item.title}</a>
  41. </li>
  42. ))}
  43. </ul>
  44. )}
  45. </Fragment>
  46. );
  47. }

useEffect中,请求数据前将loading置为true,在请求完成后,将loading置为false。可以看到useEffect的依赖数据中并没有添加loading,这是因为,不需要再loading变更时重新调用useEffect。请记住:只有某个变量更新后,需要重新执行useEffect的情况,才需要将该变量添加到useEffect的依赖数组中。
loading处理完成后,还需要处理错误,这里的逻辑是一样的,使用useState来创建一个新的state,然后在useEffect中特定的位置来更新这个state。由于使用了async/await,可以使用一个大大的try-catch:

  1. import React, { Fragment, useState, useEffect } from 'react';
  2. import axios from 'axios';
  3. function App() {
  4. const [data, setData] = useState({ hits: [] });
  5. const [query, setQuery] = useState('redux');
  6. const [url, setUrl] = useState(
  7. 'http://localhost/api/v1/search?query=redux',
  8. );
  9. const [isLoading, setIsLoading] = useState(false);
  10. const [isError, setIsError] = useState(false);
  11. useEffect(() => {
  12. const fetchData = async () => {
  13. setIsError(false);
  14. setIsLoading(true);
  15. try {
  16. const result = await axios(url);
  17. setData(result.data);
  18. } catch (error) {
  19. setIsError(true);
  20. }
  21. setIsLoading(false);
  22. };
  23. fetchData();
  24. }, [url]);
  25. return (......)

每次useEffect执行时,将会重置error;在出现错误的时候,将error置为true;在正常请求完成后,将error置为false。

4.3 处理表单

通常不仅会用到上面的输入框和按钮,更多的时候是一张表单,所以也可以在表单中使用useEffect来处理数据请求,逻辑是相同的:

  1. function App() {
  2. ...
  3. return (
  4. <Fragment>
  5. <form
  6. onSubmit={() =>
  7. setUrl(`http://localhost/api/v1/search?query=${query}`)
  8. }
  9. >
  10. <input
  11. type="text"
  12. value={query}
  13. onChange={event => setQuery(event.target.value)}
  14. />
  15. <button type="submit">Search</button>
  16. </form>
  17. {isError && <div>Something went wrong ...</div>}
  18. ...
  19. </Fragment>
  20. );
  21. }

上面的例子中,提交表单的时候,会触发页面刷新;就像通常的做法那样,还需要阻止默认事件,来阻止页面的刷新。

  1. function App() {
  2. ...
  3. const doFetch = () => {
  4. setUrl(`http://localhost/api/v1/search?query=${query}`);
  5. };
  6. return (
  7. <Fragment>
  8. <form onSubmit={event => {
  9. doFetch();
  10. event.preventDefault();
  11. }}>
  12. <input
  13. type="text"
  14. value={query}
  15. onChange={event => setQuery(event.target.value)}
  16. />
  17. <button type="submit">Search</button>
  18. </form>
  19. {isError && <div>Something went wrong ...</div>}
  20. ...
  21. </Fragment>
  22. );
  23. }

4.4 自定义hooks

可以看到上面的组件,添加了一系列hooks和逻辑之后,已经变得非常的庞大。那这时候怎么处理呢?hooks的一个非常的优势,就是能够很方便的提取自定义的hooks。这个时候,就能把上面的一大堆逻辑抽取到一个单独的hooks中,方便复用和解耦。

  1. function useHackerNewsApi = () => {
  2. const [data, setData] = useState({ hits: [] });
  3. const [url, setUrl] = useState(
  4. 'http://localhost/api/v1/search?query=redux',
  5. );
  6. const [isLoading, setIsLoading] = useState(false);
  7. const [isError, setIsError] = useState(false);
  8. useEffect(() => {
  9. const fetchData = async () => {
  10. setIsError(false);
  11. setIsLoading(true);
  12. try {
  13. const result = await axios(url);
  14. setData(result.data);
  15. } catch (error) {
  16. setIsError(true);
  17. }
  18. setIsLoading(false);
  19. };
  20. fetchData();
  21. }, [url]);
  22. const doFetch = () => {
  23. setUrl(`http://localhost/api/v1/search?query=${query}`);
  24. };
  25. return { data, isLoading, isError, doFetch };
  26. }

在自定义的hooks抽离完成后,引入到组件中。

  1. function App() {
  2. const [query, setQuery] = useState('redux');
  3. const { data, isLoading, isError, doFetch } = useHackerNewsApi();
  4. return (
  5. <Fragment>
  6. ...
  7. </Fragment>
  8. );
  9. }

然后需要在form组件中设定初始的后端URL

  1. const useHackerNewsApi = () => {
  2. ...
  3. useEffect(
  4. ...
  5. );
  6. const doFetch = url => {
  7. setUrl(url);
  8. };
  9. return { data, isLoading, isError, doFetch };
  10. };
  11. function App() {
  12. const [query, setQuery] = useState('redux');
  13. const { data, isLoading, isError, doFetch } = useHackerNewsApi();
  14. return (
  15. <Fragment>
  16. <form
  17. onSubmit={event => {
  18. doFetch(
  19. `http://localhost/api/v1/search?query=${query}`,
  20. );
  21. event.preventDefault();
  22. }}
  23. >
  24. <input
  25. type="text"
  26. value={query}
  27. onChange={event => setQuery(event.target.value)}
  28. />
  29. <button type="submit">Search</button>
  30. </form>
  31. ...
  32. </Fragment>
  33. );
  34. }

4.5 使用useReducer整合逻辑

到目前为止,已经使用了各种state hooks来管理数据,包括loading、error、data等状态。可以看到,这三个有关联的状态确是分散的,它们通过分离的useState来创建,为了有关联的状态整合到一起,需要用到useReducer
如果写过redux,那么将会对useReducer非常的熟悉,可以把它理解为一个轻量的reduxuseReducer返回一个状态对象和一个可以改变状态对象的dispatch函数。跟redux类似的,dispatch函数接受action作为参数,action包含typepayload属性。看一个简单的例子:

  1. import React, {
  2. Fragment,
  3. useState,
  4. useEffect,
  5. useReducer,
  6. } from 'react';
  7. import axios from 'axios';
  8. const dataFetchReducer = (state, action) => {
  9. ...
  10. };
  11. const useDataApi = (initialUrl, initialData) => {
  12. const [url, setUrl] = useState(initialUrl);
  13. const [state, dispatch] = useReducer(dataFetchReducer, {
  14. isLoading: false,
  15. isError: false,
  16. data: initialData,
  17. });
  18. ...
  19. };

useReducerreducer函数和初始状态对象作为参数。在例子中,data,loading和error状态的初始值与useState创建时一致,但它们已经整合到一个由useReducer创建对象,而不是多个useState创建的状态。

  1. const dataFetchReducer = (state, action) => {
  2. ...
  3. };
  4. const useDataApi = (initialUrl, initialData) => {
  5. const [url, setUrl] = useState(initialUrl);
  6. const [state, dispatch] = useReducer(dataFetchReducer, {
  7. isLoading: false,
  8. isError: false,
  9. data: initialData,
  10. });
  11. useEffect(() => {
  12. const fetchData = async () => {
  13. dispatch({ type: 'FETCH_INIT' });
  14. try {
  15. const result = await axios(url);
  16. dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
  17. } catch (error) {
  18. dispatch({ type: 'FETCH_FAILURE' });
  19. }
  20. };
  21. fetchData();
  22. }, [url]);
  23. ...
  24. };

在获取数据时,可以调用dispatch函数,将信息发送给reducer。使用dispatch函数发送的参数为object,具有type属性和可选payload的属性。type属性告诉reducer需要应用哪个状态转换,并且reducer可以使用payload来创建新的状态。在这里,只有三个状态转换:发起请求,请求成功,请求失败。
在自定义hooks的末尾,state像以前一样返回,但是因为拿到的是一个状态对象,而不是以前那种分离的状态,所以需要将状态对象解构之后再返回。这样,调用useDataApi自定义hooks的人仍然可以访问data,isLoading 和 isError这三个状态。

  1. const useDataApi = (initialUrl, initialData) => {
  2. const [url, setUrl] = useState(initialUrl);
  3. const [state, dispatch] = useReducer(dataFetchReducer, {
  4. isLoading: false,
  5. isError: false,
  6. data: initialData,
  7. });
  8. ...
  9. const doFetch = url => {
  10. setUrl(url);
  11. };
  12. return { ...state, doFetch };
  13. };

接下来添加reducer函数的实现。它需要三种不同的状态转换FETCH_INITFETCH_SUCCESSFETCH_FAILURE。每个状态转换都需要返回一个新的状态对象。看看如何使用switch case语句实现它:

  1. switch (action.type) {
  2. case 'FETCH_INIT':
  3. return {
  4. ...state,
  5. isLoading: true,
  6. isError: false
  7. };
  8. case 'FETCH_SUCCESS':
  9. return {
  10. ...state,
  11. isLoading: false,
  12. isError: false,
  13. data: action.payload,
  14. };
  15. case 'FETCH_FAILURE':
  16. return {
  17. ...state,
  18. isLoading: false,
  19. isError: true,
  20. };
  21. default:
  22. throw new Error();
  23. }
  24. };

4.6 取消数据请求

React中的一种很常见的问题是:如果在组件中发送一个请求,在请求还没有返回的时候卸载了组件,这个时候还会尝试设置这个状态,会报错。需要在hooks中处理这种情况,可以看下是怎样处理的:

  1. const useDataApi = (initialUrl, initialData) => {
  2. const [url, setUrl] = useState(initialUrl);
  3. const [state, dispatch] = useReducer(dataFetchReducer, {
  4. isLoading: false,
  5. isError: false,
  6. data: initialData,
  7. });
  8. useEffect(() => {
  9. let didCancel = false;
  10. const fetchData = async () => {
  11. dispatch({ type: 'FETCH_INIT' });
  12. try {
  13. const result = await axios(url);
  14. if (!didCancel) {
  15. dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
  16. }
  17. } catch (error) {
  18. if (!didCancel) {
  19. dispatch({ type: 'FETCH_FAILURE' });
  20. }
  21. }
  22. };
  23. fetchData();
  24. return () => {
  25. didCancel = true;
  26. };
  27. }, [url]);
  28. const doFetch = url => {
  29. setUrl(url);
  30. };
  31. return { ...state, doFetch };
  32. };

可以看到这里新增了一个didCancel变量,如果这个变量为true,不会再发送dispatch,也不会再执行设置状态这个动作。这里在useEffe的返回函数中将didCancel置为true,在卸载组件时会自动调用这段逻辑。也就避免了再卸载的组件上设置状态。

5、useEffectuseLayoutEffect

2021-05-03-21-31-48-042215.png

  • useEffect 在全部渲染完毕后才会执行
  • useLayoutEffect 会在 浏览器 layout 之后,painting 之前执行
  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  • 可以使用它来读取 DOM 布局并同步触发重渲染
  • 在浏览器执行绘制之前 useLayoutEffect 内部的更新计划将被同步刷新
  • 尽可能使用标准的 useEffect 以避免阻塞视图更新

    1. function LayoutEffect() {
    2. const [color, setColor] = useState('red');
    3. useLayoutEffect(() => {
    4. alert(color);
    5. });
    6. useEffect(() => {
    7. console.log('color', color);
    8. });
    9. return (
    10. <>
    11. <div id="myDiv" style={{ background: color }}>颜色</div>
    12. <button onClick={() => setColor('red')}>红</button>
    13. <button onClick={() => setColor('yellow')}>黄</button>
    14. <button onClick={() => setColor('blue')}>蓝</button>
    15. </>
    16. );
    17. }

    useEffect优势

    useEffect 在渲染结束时执行,所以不会阻塞浏览器渲染进程,所以使用 Function Component 写的项目一般都有用更好的性能。自然符合 React Fiber 的理念,因为 Fiber 会根据情况暂停或插队执行不同组件的 Render,如果代码遵循了 Capture Value 的特性,在 Fiber 环境下会保证值的安全访问,同时弱化生命周期也能解决中断执行时带来的问题。useEffect 不会在服务端渲染时执行。由于在 DOM 执行完毕后才执行,所以能保证拿到状态生效后的 DOM 属性。

    6、useEffect源码解析

    首先要牢记 effect hook 的一些属性:

  • 它们在渲染时被创建,但是在浏览器绘制后运行。

  • 如果给出了销毁指令,它们将在下一次绘制前被销毁。
  • 它们会按照定义的顺序被运行。

于是就应该有另一个队列来保存这些 effect hook,并且还要能够在绘制后被定位到。通常来说,应该是 fiber 保存包含了 effect 节点的队列。每个 effect 节点都是一个不同的类型,并能在适当的状态下被定位到:
在修改之前调用 getSnapshotBeforeUpdate() 实例。
运行所有插入、更新、删除和 ref 的卸载。
运行所有生命周期函数和 ref 回调函数。生命周期函数会在一个独立的通道中运行,所以整个组件树中所有的替换、更新、删除都会被调用。这个过程还会触发任何特定于渲染器的初始 effect hook。
useEffect() hook 调度的 effect —— 也被称为“被动 effect”,它基于这部分代码。
hook effect 将会被保存在 fiber 一个称为 updateQueue 的属性上,每个 effect 节点都有如下的结构:

  • tag —— 一个二进制数字,它控制了 effect 节点的行为
  • create —— 绘制之后运行的回调函数
  • destroy —— 它是 create() 返回的回调函数,将会在初始渲染前运行
  • inputs —— 一个集合,该集合中的值将会决定一个 effect 节点是否应该被销毁或者重新创建
  • next —— 它指向下一个定义在函数组件中的 effect 节点

除了 tag 属性,其他的属性都很简明易懂。如果对 hook 很了解,应该知道,React 提供了一些特殊的 effect hook:比如 useMutationEffect()useLayoutEffect()。这两个 effect hook 内部都使用了 useEffect(),实际上这就意味着它们创建了 effect hook,但是却使用了不同的 tag 属性值。这个 tag 属性值是由二进制的值组合而成(详见源码):

  1. const NoEffect = /* */ 0b00000000;
  2. const UnmountSnapshot = /* */ 0b00000010;
  3. const UnmountMutation = /* */ 0b00000100;
  4. const MountMutation = /* */ 0b00001000;
  5. const UnmountLayout = /* */ 0b00010000;
  6. const MountLayout = /* */ 0b00100000;
  7. const MountPassive = /* */ 0b01000000;
  8. const UnmountPassive = /* */ 0b10000000;

复制代码React 支持的 hook effect 类型 这些二进制值中最常用的情景是使用管道符号(|)连接,将比特相加到单个某值上。然后就可以使用符号(&)检查某个 tag 属性是否能触发一个特定的行为。如果结果是非零的,就表示可以。

  1. const effectTag = MountPassive | UnmountPassive
  2. assert(effectTag, 0b11000000)
  3. assert(effectTag & MountPassive, 0b10000000)

复制代码如何使用 React 的二进制设计模式的示例 这里是 React 支持的 hook effect,以及它们的 tag 属性(详见源码):

  • Default effect —— UnmountPassive | MountPassive.
  • Mutation effect —— UnmountSnapshot | MountMutation.
  • Layout effect —— UnmountMutation | MountLayout.

以及这里是 React 如何检查行为触发的(详见源码):

  1. if ((effect.tag & unmountTag) !== NoHookEffect) {
  2. // Unmount
  3. }
  4. if ((effect.tag & mountTag) !== NoHookEffect) {
  5. // Mount
  6. }

源码节选 所以,基于刚才学习的关于 effect hook 的知识,可以实际操作,从外部向 fiber 插入一些 effect:

  1. function injectEffect(fiber) {
  2. const lastEffect = fiber.updateQueue.lastEffect
  3. const destroyEffect = () => {
  4. console.log('on destroy')
  5. }
  6. const createEffect = () => {
  7. console.log('on create')
  8. return destroy
  9. }
  10. const injectedEffect = {
  11. tag: 0b11000000,
  12. next: lastEffect.next,
  13. create: createEffect,
  14. destroy: destroyEffect,
  15. inputs: [createEffect],
  16. }
  17. lastEffect.next = injectedEffect
  18. }
  19. const ParentComponent = (
  20. <ChildComponent ref={injectEffect} />
  21. )