Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

你应该知道的Hooks知识 #1

Open
LuckyFBB opened this issue Jan 22, 2022 · 0 comments
Open

你应该知道的Hooks知识 #1

LuckyFBB opened this issue Jan 22, 2022 · 0 comments
Assignees

Comments

@LuckyFBB
Copy link
Owner

Hooks

Hook是React16.8的新增特性,能够在不写class的情况下使用state以及其他特性。

动机

  • 在组件之间复用状态逻辑很难
  • 复杂组件变得难以理解
  • 难以理解的 class

hook规则

  • 只有在最顶层使用hook,不要再循环/条件/嵌套函数中使用
  • 只有在React函数中调用Hook

函数组件和类组件的不同

函数组件能够捕获到当前渲染的所用的值。

点击查看示例

对于类组件来说,虽然props是一个不可变的数据,但是this是一个可变的数据,在我们渲染组件的时候this发生了改变,所以this.props发生了改变,因此在this.showMessage中会拿到最新的props值。

对于函数组件来说捕获了渲染所使用的值,当我们使用hooks时,这种特性也同样的试用于state上。

点击查看示例

const showMessage = () => {
  alert("写入:" + message);
};

const handleSendClick = () => {
  setTimeout(showMessage, 3000);
};

const handleMessageChange = (e) => {
  setMessage(e.target.value);
};

如果我们想跳出'函数组件捕获当前渲染的所用值‘这个特性,我们可以采用ref来追踪某些数据。通过ref.current可以获取到最新的值

const showMessage = () => {
  alert("写入:" + ref.current);
};

const handleSendClick = () => {
  setTimeout(showMessage, 3000);
};

const handleMessageChange = (e) => {
  setMessage(e.target.value);
  ref.current = e.target.value;
};

useEffect

useEffect能够在函数组件中执行副作用操作(数据获取/涉及订阅),其实可以把useEffect看作是componentDidMount/componentDidUpdate/componentWillUnMount的组合

第一个参数是一个callback,返回destory。destory作为下一个callback执行前调用,用于清除上一次callback产生的副作用

第二个参数是依赖项,一个数组,可以有多个依赖项。依赖项改变,执行上一个callback返回的destory,和执行新的effect第一个参数callback

对于useEffect的执行,React处理逻辑是采用异步调用的,对于每一个effect的callback会像setTimeout回调函数一样,放到任务队列里面,等到主线程执行完毕才会执行。所以effect的回调函数不会阻塞浏览器绘制视图

  1. 相关的生命周期替换方案

    • componentDidMount替代方案

      React.useEffect(()=>{
        //请求数据,事件监听,操纵DOM
      },[]) //dep=[],只有在初始化执行
      /* 
      因为useEffect会捕获props和state,
      所以即使是在回调函数中我们拿到的还是最初的props和state
      */
    • componentDidUnmount替代方案

      React.useEffect(()=>{
        /* 请求数据 , 事件监听 , 操纵dom , 增加定时器,延时器 */
        return function componentWillUnmount(){
          /* 解除事件监听器 ,清除定时器,延时器 */
        }
      },[])/* 切记 dep = [] */
      
      //useEffect第一个函数的返回值可以作为componentWillUnmount使用
    • componentWillReceiveProps替代方案

      其实两者的执行时机是完全不同的,一个在render阶段,一个在commit阶段

      useEffect会初始化执行一次,但是componentWillReceiveProps只会在props变化时执行更新

      React.useEffect(()=>{
        console.log('props变化:componentWillReceiveProps')
      },[ props ])
    • componentDidUpdate替代方案

      useEffect 和 componentDidUpdate 在执行时期虽然有点差别,useEffect 是异步执行,componentDidUpdate 是同步执行 ,但都是在 commit 阶段

      React.useEffect(()=>{
        console.log('组件更新完成:componentDidUpdate ')     
      }) //没有dep依赖项,没有第二个参数,那么每一次执行函数组件,都会执行该 effect。
  2. 在useEffect中[]需要处理什么

    React官网FAQ这样说

    只有当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略,使用eslint-plugin-react-hooks帮助我们的代码做一个校验

    点击查看详细示例

    function Counter() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const id = setInterval(() => {
          setCount(count + 1);
        }, 1000);
        return () => clearInterval(id);
      }, []);
    
      return <h1>{count}</h1>;
    }
    //只会做一次更新,然后定时器不再转动
  3. 是否应该把函数当做effect的依赖

    const loadResourceCatalog = async () => {
      if (!templateType) return
      const reqApi = templateType === TEMPLATE_TYPE.STANDARD ? 'listCatalog' : 'getCodeManageCatalog'
      const res: any = await API[reqApi]()
      if (!res.success) return
      setCatalog(res.data)
    }
    
    useEffect(() => {
      loadResourceCatalog();
    }, [])
    //在函数loadResourceCatalog中使用了templateType这样的一个state
    //在开发的过程中可能会忘记函数loadResourceCatalog依赖templateType值

    第一个简单的解法,对于某些只在useEffect中使用的函数,直接定义在effect中,以至于能够直接依赖某些state

    useEffect(() => {
      const loadResourceCatalog = async () => {
        if (!templateType) return
        const reqApi = templateType === TEMPLATE_TYPE.STANDARD ? 'listCatalog' : 'getCodeManageCatalog'
        const res: any = await API[reqApi]()
        if (!res.success) return
        setCatalog(res.data)
      }
      loadResourceCatalog();
    }, [templateType])

    假如我们需要在很多地方用到我们定义的函数,不能够把定义放到当前的effect中,并且将函数放到了第二个的依赖参数中,那这个代码将就进入死循环。因为函数在每一次渲染中都返回一个新的引用

    const Template = () => {
      const getStandardTemplateList = async () => {
        const res: any = await API.getStandardTemplateList()
        if (!res.success) return;
        const { data } = res;
        setCascaderOptions(data);
        getDefaultOption(data[0])
      }
      useEffect(() => {
        getStandardTemplateList()
      }, [getStandardTemplateList])
    }

    针对这种情况,如果当前函数没有引用任何组件内的任何值,可以将该函数提取到组件外面去定义,这样就不会组件每次render时不会再次改变函数引用。

    const getStandardTemplateList = async () => {
      const res: any = await API.getStandardTemplateList()
      if (!res.success) return;
      const { data } = res;
      setCascaderOptions(data);
      getDefaultOption(data[0])
    }
    
    const Template = () => {
      useEffect(() => {
        getStandardTemplateList()
      }, [])
    }

    如果说当前函数中引用了组件内的一些状态值,可以采用useCallBack对当前函数进行包裹

    const loadResourceCatalog = useCallback(async () => {
      if (!templateType) return
      const reqApi = templateType === TEMPLATE_TYPE.STANDARD ? 'listCatalog' : 'getCodeManageCatalog'
      const res: any = await API[reqApi]()
      if (!res.success) return
      setCatalog(res.data)
    }, [templateType])
    
    useEffect(() => {
      loadResourceCatalog();
    }, [loadResourceCatalog])
    //通过useCallback的包裹,如果templateType保持不变,那么loadResourceCatalog也会保持不变,所以useEffect也不会重新运行
    //如果templateType改变,那么loadResourceCatalog也会改变,所以useEffect也会重新运行

useCallback

React官网定义

返回一个memoized回调函数,该回调函数仅在某个依赖项改变时才会更新

import React, { useCallback, useState } from "react";

const CallBackTest = () => {
  const [count, setCount] = useState(0);
  const [total, setTotal] = useState(0);
  const handleCount = () => setCount(count + 1);
  //const handleCount = useCallback(() => setCount(count + 1), [count]);
  const handleTotal = () => setTotal(total + 1);

  return (
    <div>
      <div>Count is {count}</div>
      <div>Total is {total}</div>
      <br />
      <div>
        <Child onClick={handleCount} label="Increment Count" />
        <Child onClick={handleTotal} label="Increment Total" />
      </div>
    </div>
  );
};

const Child = React.memo(({ onClick, label }) => {
  console.log(`${label} Child Render`);
  return <button onClick={onClick}>{label}</button>;
});

export default CallBackTest;

点击查看详细示例

React.memo是通过记忆组件渲染结果的方式来提高性能,memo是react16.6引入的新属性,通过浅比较(源码通过Object.is方法比较)当前依赖的props和下一个props是否相同来决定是否重新渲染;如果使用过类组件方式,就能知道memo其实就相当于class组件中的React.PureComponent,区别就在于memo用于函数组件。useCallback和React.memo一定要结合使用才能有效果。

使用场景

  • 作为props,传递给子组件,为避免子元素不必要的渲染,需要配合React.Memo使用,否则无意义
  • 作为useEffect的依赖项,需要进行比较的时候才需要加上useCallback

useMemo

React官网定义

返回一个 memoized 值

仅会在某个依赖项改变时才重新计算memoized值,这种优化有助于避免在每次渲染时都进行高开销的计算

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

对于实现上,基本上是和useCallback相似,只是略微有些不同

使用场景

  • 避免在每次渲染时都进行高开销的计算

两个 hooks 内置于 React 都有特别的原因:

  1. 引用相等

    当在React函数组件中定义一个对象时,它跟上次定义的相同对象,引用是不一样的(即使它具有所有相同值和相同属性)

    • 依赖列表
    • React.memo

    大多数时候,你不需要考虑去优化不必要的重新渲染,因为优化总会带来成本。

  2. 昂贵的计算

    计算成本很高的同步计算值的函数

总结

本文介绍了hooks产生动机、函数组件和类组件的区别以及useEffect/useCallback/useMemo等内容。重点介绍了useEffect的生命周期替换方案以及是否把函数作为useEffect的第二参数。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant