React学习笔记——Hooks中useEffect的基础介绍和使用

在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI 组件我们可以使用函数,无状态组件来展示 UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给 UI 组件进行渲染。使用 React Hooks 相比于从前的类组件有以下几点好处:

  1. 代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护
  2. 组件树层级变浅,在原本的代码中,我们经常使用 HOC/render props 等方式来复用组件的状态,增强功能等,无疑增加了组件树层数及渲染,而在 React Hooks 中,这些功能都可以通过强大的自定义的 Hooks 来实现

Hook 是 React 16.8.0 版本增加的新特性,可以在函数组件中使用 state以及其他的 React 特性
Hooks只能在函数式组件中使用,既无状态组件(所有钩子在用时都要先引入)

1、Hook 使用规则

Hook 就是JavaScript 函数,但是使用它们会有两个额外的规则:
1、只能在函数最外层调用 Hook。不要在循环、条件判断或者嵌套函数(子函数)中调用。
2、只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
3、在多个useState()调用中,渲染之间的调用顺序必须相同

2、useEffect

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

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

默认情况下,effect 将在每轮渲染结束后执行;但你可以选择让它在只有某些值改变的时候才执行。

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

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

问:useEffect 做了什么?

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

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

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

(1)参数

useEffect可以传两个参数。
参数一:

  • 接收一个函数,可以用来做一些副作用比如异步请求,修改外部参数等行为。即接收一个包含命令式、且可能有副作用代码的函数。

参数二:

  1. 第二个参数称之为dependencies,是一个数组,如果数组中的值(依赖)变化才会触发。useEffect的第二个参数为一个```空数组,初始化调用一次之后不再执行,相当于componentDidMount。
  2. 还有另外一个情况,就是不传递第二个参数,也就是useEffect只接收了第一个函数参数,代表不监听任何参数变化。每次渲染DOM之后,都会执行useEffect中的函数。
(2)清除effect——return

组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。

useEffect(
  () => {
    const subscription = props.source.subscribe();
    //return里面就是清除函数
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);
(3)实现componentDidMount 的功能

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

function Demo () {
  useEffect(() => {
    console.log('hello world')
  }, [])
  return (
    <div>
      hello world
    </div>
  )
}
// 等价于
class Demo extends Component {
  componentDidMount() {
    console.log('hello world')
  }
  render() {
    return (
      <div>
        hello world
      </div>
    );
  }
}
import React, { useState ,useEffect} from 'react';

const App3 = () => {
    const [data,setData]=useState(0)
    useEffect(() => {
        console.log('useEffect')
    }, [])
    const handleClick=()=>{
        console.log('点击我')
        setData(data=>data+1)
    }
    return (
        console.log('render'),
        <div>
            {data}
            <button onClick={handleClick}>点击我</button>
        </div>
    )
};

export default App3;

效果图
React学习笔记——Hooks中useEffect的基础介绍和使用_第1张图片

(4)实现 componentDidMount和componentDidUpdate 的组合功能

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

import React, { Component } from 'react';

class App3 extends Component {
    state = {
        data : 0
    }
    componentDidMount(){
        console.log('useEffect')
    }
    componentDidUpdate(){
        console.log('useEffect')
    }
    handleClick = () => {
        console.log('点击我')
        this.setState({
            data:this.state.data+1
        })
    }
    render() {
        return (
                console.log('render'),
            <div>
                {this.state.data}
                <button onClick={this.handleClick}>点击我</button>
            </div>
        );
    }
}

export default App3;

//等价于

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

const App3 = () => {
    const [data,setData]=useState(0)
    useEffect(() => {
        console.log('useEffect')
    })
    const handleClick=()=>{
        console.log('点击我')
        setData(data=>data+1)
    }
    return (
        console.log('render'),
        <div>
            {data}
            <button onClick={handleClick}>点击我</button>
        </div>
    )
};

export default App3;

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

const App3 = () => {
    const [data,setData]=useState(0)
    useEffect(() => {
        console.log('useEffect')
    })
    const handleClick=()=>{
        console.log('点击我')
        setData(data=>data+1)
    }
    return (
        console.log('render'),
        <div>
            {data}
            <button onClick={handleClick}>点击我</button>
        </div>
    )
};

export default App3;

效果图
React学习笔记——Hooks中useEffect的基础介绍和使用_第2张图片

(5)实现 componentDidMount componentWillUnmount 的组合功能

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

class Example extends Component {
  constructor (props) {
    super(props);
    this.state = {
      count: 0
    }
  }
  componentDidMount() {
    this.id = setInterval(() => {
      this.setState({count: this.state.count + 1})
    }, 1000);
  }
  componentWillUnmount() {
    clearInterval(this.id)
  }
  render() { 
    return <h1>{this.state.count}</h1>;
  }
}

// 等价于

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>hello world</h1>
}

3、effect的执行时机

与 componentDidMount、componentDidUpdate 不同的是,传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新

然而,并非所有 effect 都可以被延迟执行。例如,一个对用户可见的 DOM 变更就必须在浏览器执行下一次绘制前被同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。在开始新的更新前,React 总会先清除上一轮渲染的 effect。

(1)useEffect 和 useLayoutEffect的区别

React学习笔记——Hooks中useEffect的基础介绍和使用_第3张图片

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

实例一:useLayoutEffect里面进行alert拦截

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

const App3 = () => {
    const [color, setColor] = useState('red');
    useLayoutEffect(() => {
        alert(color);
        console.log('useLayoutEffect_color',color)
    });
    
    useEffect(() => {
        // alert(color);
        console.log('useEffect_color', color);
    });
    return (
        <>
            <div id="myDiv" style={{ background: color }}>颜色</div>
            <button onClick={() => setColor('red')}></button>
            <button onClick={() => setColor('yellow')}></button>
            <button onClick={() => setColor('blue')}></button>
        </>
    );
};

export default App3;

效果图:

可以看出,在页面绘制之前,useLayoutEffect里面的alert进行了拦截,此时页面DOM元素还没有在页面上绘制
React学习笔记——Hooks中useEffect的基础介绍和使用_第4张图片
点击“确定”之后,发现DOM元素在页面上呈现,在控制台中,先打印“useLayoutEffect_color”,随后打印“useEffect_color”
React学习笔记——Hooks中useEffect的基础介绍和使用_第5张图片
由此说明useLayoutEffect HOOKuseEffect HOOK触发的时机

实例二:useEffect里面进行alert拦截

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

const App3 = () => {
    const [color, setColor] = useState('red');
    useLayoutEffect(() => {
        // alert(color);
        console.log('useLayoutEffect_color',color)
    });
    
    useEffect(() => {
        alert(color);
        console.log('useEffect_color', color);
    });
    return (
        <>
            <div id="myDiv" style={{ background: color }}>颜色</div>
            <button onClick={() => setColor('red')}></button>
            <button onClick={() => setColor('yellow')}></button>
            <button onClick={() => setColor('blue')}></button>
        </>
    );
};

export default App3;

效果图:

我们发现,在alert拦截时,页面上面已经进行了绘制,并且在控制台中打印“useLayoutEffect_color”,但是没有打印“useEffect_color”
React学习笔记——Hooks中useEffect的基础介绍和使用_第6张图片
点击“确定”之后,控制台中才打印“useEffect_color”
React学习笔记——Hooks中useEffect的基础介绍和使用_第7张图片
由此说明useLayoutEffect HOOKuseEffect HOOK触发的时机

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

4、依赖如何处理函数(官网)

(1)传统方式:

一般来说,在依赖列表中省略函数是否安全?答案是:不安全。

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

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

要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数
这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:

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

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

如果这样之后我们依然没用到组件作用域中的任何值,就可以安全地把它指定为 [ ]:

useEffect(() => {
  function doSomething() {
    console.log('hello');
  }

  doSomething();
}, []); // ✅ 在这个例子中是安全的,因为我们没有用到组件作用域中的 *任何* 值

只有当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。
下面这个案例有一个 Bug:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  async function fetchProduct() {
    const response = await fetch('http://myapi/product/' + productId); // 使用了 productId prop
    const json = await response.json();
    setProduct(json);
  }

  useEffect(() => {
    fetchProduct();
  }, []); //  这样是无效的,因为 `fetchProduct` 使用了 `productId`
  // ...
}

推荐的修复方案是把那个函数移动到你的 effect 内部。
这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    // 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId);
      const json = await response.json();
      setProduct(json);
    }

    fetchProduct();
  }, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId
  // ...
}

这同时也允许你通过 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]);
(2)特殊情况:

如果出于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法:

  • 你可以尝试把那个函数移动到你的组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。
  • 如果你所调用的方法是一个纯计算,并且可以在渲染时调用,你可以 转而在 effect 之外调用它, 并让 effect 依赖于它的返回值
  • 万不得已的情况下,你可以 把函数加入 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 的所有依赖都被指定了
  // ...
}

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

5、依赖变化频繁(官网)

有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // 这个 effect 依赖于 `count` state
    }, 1000);
    return () => clearInterval(id);
  }, []); //  Bug: `count` 没有被指定为依赖

  return <h1>{count}</h1>;
}
  • 传入空的依赖数组 [ ],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1

效果图:
React学习笔记——Hooks中useEffect的基础介绍和使用_第8张图片
如图所示,每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1

指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。

事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
    }, 1000);
    return () => clearInterval(id);
  }, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量

  return <h1>{count}</h1>;
}

(setCount 函数的身份是被确保稳定的,所以可以放心的省略掉)

此时,setInterval 的回调依旧每秒调用一次,但每次 setCount 内部的回调取到的 count 是最新值,(在回调中变量命名为 c)。

你可能感兴趣的:(React,react.js)