博主经验尚浅,如有错误,欢迎指正

一:useState

1.使用方式

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

从上述钩子定义的源代码可以看出,useState钩子函数的参数传入 一个初始值(或者一个函数,调用后获得初始值),返回值返回 给外界对该状态的读写操作(以数组形式)。基本的使用示例如下:

const [state, setState] = useState(initialState);

2.使用案例

众所周知,类组件也叫做有状态组件,具备管理状态的能力。而函数组件在钩子函数没有出现之前,也叫做无状态组件,不具备管理状态的能力。直到钩子函数出现之后,函数组件才具备了管理状态的能力。下面列举了类组件和函数组件(带useState)对状态的管理实现方式的不同。关注这两种方式对状态的初始化以及对状态的读和写操作

类组件管理状态
import React from "react";

export default class ClassComp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times (classComp)</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
函数组件管理状态(useState)
import React, { useState } from "react";

export default function FuncComp() {
  const [count, setCount] = useState(0);

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

3.使用细节

(1)惰性初始 state【性能优化:通过fn计算得出initialState,如 useState(fn)】

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

  • 不可取的方式1,initialState 在每次render都会计算一次,浪费性能。
const initialState = someExpensiveComputation(props);
const [state, setState] = useState(initialState);
  • 可取的方式2:通过传入一个函数的形式,让计算该初始值的函数只调用一次。
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});
(2)函数式更新【编码优化:通过fn计算得出updateState,如 setXxx(fn)】

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState 的两种用法:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}
(3)跳过state更新【解决疑惑:同值更新是否被视为状态改变】

调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)

  • object.is比较算法如下,由下可初步得出结论,在我们使用时可以基本使用 址判断规则 来判断。
Object.is('foo', 'foo');     // true
Object.is(window, window);   // true

Object.is('foo', 'bar');     // false
Object.is([], []);           // false

var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo);         // true
Object.is(foo, bar);         // false

Object.is(null, null);       // true

// 特例
Object.is(0, -0);            // false
Object.is(0, +0);            // true
Object.is(-0, -0);           // true
Object.is(NaN, 0/0);         // true

二:useEffect

1.使用方式

 function useEffect(effect: EffectCallback, deps?: DependencyList): void;

从上述钩子定义的源代码可以看出,useEffect钩子函数的参数1传入一个副作用回调函数,参数2传入一个副作用的依赖数组(可选),没有返回值。基本的使用示例如下:

  // componentDidMount or componentDidUpdate && count state change
  useEffect(() => {
    console.log(count)
  },[count])

2.使用案例

类组件副作用
import React from "react";

export default class ClassComp extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    console.log('class', 'componentDidMount')
  }

  componentWillUpdate(nextProps, nextState) {
    console.log('class', 'componentWillUpdate')
    if(this.state.count !== nextState.count)console.log(this.state.count)
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('class', 'componentDidUpdate')
    if(this.state.count !== prevState.count)console.log(this.state.count)
  }

  componentWillUnmount() {
    console.log('class', 'componentWillUnmount')
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times (classComp)</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
      </button>
      </div>
    );
  }
}
函数组件副作用(useEffect)
import React, { useState, useEffect } from "react";

export default function FuncComp() {
  const [count, setCount] = useState(0)

  // 等同于:componentDidMount
  useEffect(() => {
    console.log('function', 'componentDidMount')
  }, [])

  // 等同于:componentDidMount or componentDidUpdate
  useEffect(() => {
    console.log('function', 'componentDidMount or componentDidUpdate')
  })

  // 等同于:(componentDidMount or componentDidUpdate) && count state change
  useEffect(() => {
    console.log('function', 'componentDidUpdate console newCount', count)
  }, [count])

  // 等同于:componentWillUpdate or componentWillUnmount
  useEffect(() => {
    return () => {
      console.log('function', 'componentWillUpdate or componentWillUnmount')
    }
  })

  // 等同于:(componentWillUpdate or componentWillUnmount ) && count state change
  useEffect(() => {
    return () => {
      console.log('function', 'componentWillUpdate or componentWillUnmount console preCount', count)
    }
  }, [count])

  // 等同于:componentWillUnmount
  // useEffect(() => {
  //   return () => {
  //     console.log('function', 'componentWillUnmount')
  //   }
  // },[])

  // 等同于:(componentDidMount or componentDidUpdate) and (componentWillUpdate or componentWillUnmount)
  // useEffect(() => {
  //   console.log('function', 'componentDidMount or componentDidUpdate')
  //   return () => {
  //     console.log('function', 'componentWillUpdate or componentWillUnmount')
  //   }
  // })
  return (
    <div>
      <p>You clicked {count} times (FuncComp)</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
运行结果
  • 挂载
    在这里插入图片描述
    由上运行结果可知,在组件挂载后:类组件只有componentDidMount生命周期方法被执行了,而函数组件所有的useEffect的副作用逻辑都执行了(只有三个console是因为最后两个useEffect只注册了更新前和卸载逻辑)。
  • 更新
    在这里插入图片描述
    由上运行结果可知,在组件更新后:类组件的componentWillUpdate和componentDidUpdate生命周期方法都被执行了,而函数组件所有的useEffect的副作用逻辑除了第一个注册了依赖数组为空之外的所有副作用逻辑都执行了(没有依赖数组的副作用也执行了)。
  • 卸载
    在这里插入图片描述
    由上运行结果可知,在组件卸载后:类组件的componentWillUnMount生命周期方法被执行了,而函数组件所有的useEffect的副作用逻辑所有的注册了卸载逻辑的都会执行。

3.使用细节

(1)useEffect副作用逻辑的触发条件

默认情况下,effect 将在每轮渲染结束后执行,但可以通过传入依赖数组的方式选择让它 在只有某些值改变的时候 才执行。

(2)useEffect副作用逻辑触发后的执行时间

与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。

(3)useEffect副作用逻辑的清除【防止内存泄漏】

通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源(如在上述示例的第六个useEffect中)。为防止内存泄漏,清除函数会在组件卸载前执行。
另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除(如在上述示例的第四 / 五个useEffect中)。

4.遗留疑惑

从上述示例中,我们可以发现使用useEffect这个hooks可以模拟类组件中willMount和unMount这两个生命周期单独的实现,同时也可以模拟实现willMount与DidUpdate的组合,unMount与willUpdate的组合,以及willMount、DidUpdate、unMount、willUpdate四者组合。但是这有一个问题,我如何模拟单独的DidUpdate或者WillUpdate生命周期方法呢?在实际开发中,我遇到了这样的需求,由于没找到一个合适的方案,所以饶了个湾子实现,通过判断某个只有在更新阶段才有的标记状态来进行对挂载状态的排除,使其副作用逻辑只在DidUpdate时执行而不在DidMount时执行。如果看官能有一个更好的方式,欢迎告知,谢谢!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐