React hooks的使用小结

763 阅读8分钟

什么是hooks

我们都知道,在React中一切都是组件,而组件的创建方式有多种。如果你是久经战场的React开发者,那你肯定知道createclass创建组件的方式,但是这种方式已经过时了。目前常用的两种创建组件的方式是使用es6的class来创建组件,我们称为类组件,以及使用普通的函数来创建组件,我们称之为函数组件。在React V16.8之前,类组件和函数组件有着不同的功能,因此也适用于不同的适用场景。但是因为功能比较接近而又有着差异,因此让很多人难以抉择使用哪种方式来创建组件。 例如,我们在创建纯展示组件的时候,我们使用类组件创建方式如下:

class Hello extends React.Component {
  render() {
    return <div>Hello</div>;
  }
}

使用函数组件创建方式如下:

const Hello = () => {
  return <div>Hello</div>;
};

显而易见,使用class的方式创建该组件显得大材小用,因此在这种情况下,我们更推荐使用函数组件来创建。

那么函数组件有哪些优点呢?

  • 简单易懂。从上面可以看出来,函数式组件相比类组件简单易懂,而且编译出来的代码相比类组件更简单,因为类组件在在编译过程中需要将es6的class转为es5,同时需要继承React.Component。而函数组件只需将箭头函数转为普通的函数。
  • 更符合React UI=f(state)的哲学。函数组件更符合UI=f(state)的哲学,因为React本身就是一个画UI的库,f就是我们编写的组件,state就是我们的数据,我们只关心数据传到我们的组件中得到我们想要的界面。而类组件的方式并不符合这种哲学。
  • 更符合函数式编程的思想。函数式编程的思想是近年逐渐火起来的一种编程范式,React也一种在推崇函数式编程的思想,但无论如何都无法完全做到。函数式编程中纯函数、不可变数据、函数组合、声明式的特性也给代码减少了bug,提供了更好的代码稳定性。而函数组件更符合这种思想。

既然函数组件有那么多的优点,那么为什么我们还需要类组件呢?

  • 组件需要state状态
  • 组件需要例如shouldComponentUpdate等生命周期
  • 组件需要在生命周期中进行副作用

而以上功能,在函数式组件中是不存在的,因此我们必须使用类组件。知道React v16.8,React给我们带来了hooks,hooks就是用来提供类组件中有而函数组件中没有的功能。

React提供的hooks有很多,但是以下几种基本涵盖了我们业务中百分之八九十的场景:

  • useState
  • useEffect
  • custome hooks

我们从命名中可以看出,hooks采用约定的方式以use开头,这让我想起了HOC,HOC往往都是以with开头,就像withRouter一样,我们的组件赋予更强的能力。

这就是hooks的由来,这就是hooks,那么hooks怎么用呢?

hooks的简单使用

useState的使用

const Counter = () => { 
  const [count, setCount] = React.useState(0); 

  const add = () => { setCount(count + 1); }; 

  return ( 
    <React.Fragment> 
       <h1>{count}</h1> 
       <button onClick={add}>add</button> 
    </React.Fragment> 
  ); 
};

上面代码中,让我们感到很陌生的就是第二行代码:

  const [count, setCount] = React.useState(0); 

那么我们来简单分析一下useState。

在使用useState的时候,我们需要先初始化,即给useState传入一个初始值0,useState会给我们返回一个元组。元组的存在就是让我们方便解构,元组的第一个数据是一个数值,即我们定义的state值,第二个数据是修改该state值的方法。这两个值一一对应,且与外界没有任何关系。

这样我们在add方法中执行修改state的方法时,便可设置state的数据。

这就是useState的简单使用。

在我们的业务场景中,我们往往需要定义多个state,那么我们可以在函数的开头依次定义。

const Counter = () => { 
  const [count, setCount] = React.useState(0);
  const [count1, setCount1] = React.useState(10); 

  const add = () => { setCount(count + 1); };
  const minus = () => { setCount(count1 - 1); };

  return ( 
    <React.Fragment> 
       <h1>{count}</h1> 
       <button onClick={add}>add</button> 
       <h1>{count1}</h1> 
       <button onClick={minus}>minus</button> 
    </React.Fragment> 
  ); 
};

写到这,肯定有人会问,一个方法怎么保存状态?一个函数组件怎么保存状态?那么我们来分析下useState的原理。 我们都知道,每一个组件都对应一个Fiber对象,每个Fiber对象中都会有一个memorizeState的属性来存储组件内的状态。例如一个类组件中的所有状态都存储在Fiber对象的memorizeState中。而对于一个函数组件而言,当第一调用useState的时候,React会给这第一个state生成一个hooks对象,这个hooks对象就指向了Fiber对象的memorizeState属性,因此Fiber中的memorizeState属性也可以用来存储函数组件的状态。那么我们来看看hooks对象:

{ 
  baseState,
  next,
  baseUpdate,
  queue,
  memoizedState 
}

我们可以看到,hooks对象中也包含一个memorizedState属性,这个属性就是用来存储当前的state的值。同时包含一个next属性,这个next属性就是用来指向下一次执行useState时生成的hooks对象,以此类推,按照函数组件第一次执行时useState的初始化顺序生成一个hooks对象的调用链,以后按照这个顺序依次取值。说到这里,useState中的调用顺序至关重要,加入我们的代码是这样的

const Counter = () => { 
   const [count, setCount] = React.useState(0);
 
   if (count  === 0) { 
     const [count1, setCount1] = React.useState(1); 
   } 

   const [count2, setCount2] = React.useState(2);

当函数组件第一次执行的时候,默认会初始化三个useState。顺序如下:

useState1 => memoizedState count = useState1.memoizedState

useState1.next => useState2 count2 = useState2.memoizedState

useState2.next => useState3 count3 = useState3.memoizedState

当我们进行一系列操作之后,count的值由0变成其它数值了并且导致函数组件重新更新了,那么此时useState再次执行,执行顺序如下:

useState1 => memoizedState count === useState1.memoizedState

useState1.next => useState2 count2 === useState2.memoizedState

此时会把state1的值取出来赋值给count2,导致bug的出现。

因此,在使用useState时,至关重要的就是无论执行多少次,useState的执行顺序和执行数量都保持一致。

useEffect的使用

useEffect可以说是componentDidmount和compoenntDidUpdate的结合体,我们可以简单列下useEffect如何达到这两种效果。

  1. componentDidMount
useEffect(() => {
  // mount时会调用
},[])

其中第二个参数可以传一个数组,如果数组为空,则只在componentDidMount的时候执行。如果传入一个state,当该state变化导致页面重新渲染时,useEffect也会执行,达到componentDidUpdate的效果。

  1. componentDidUpdate 和 componentDidMount
useEffect(() => {
  // mount或者update都会调用
})
  1. 模拟componentDidUpdate
const mounted = React.useRef(); 

React.useEffect(() => { 
  if (!mounted.current) { 
    mounted.current = true; 
  } else { 
    // update操作 
  } 
});

我们可以使用React提供的另外一种hooks:useRef来实现componentDidUpdate的效果。useRef会返回一个对象,该对象包含一个current属性,useRef主要用来存储整个生命周期中自己需要缓存的数据。当组件mount的时候,current是undefined,此时我们给current赋值为true,这样在以后update的时候,current值永远为true,达到了只在componentDidUpdate时执行的效果。

  1. componentDidUnMount
useEffect(() => {
  // mount时会调用
  return () => {
    // unmount时会调用
  }
},[])

useEffect的第一个参数可以return一个方法,这个方法就用于在componentDidUnMount时执行。

custome hooks的使用

const useAdd = initValue => {
  const [count, setCount] = React.useState(initValue);
  const addCount = () => {
   setCount(count + 1);
  };
  return [count, addCount];
};

const Counter = () => {
  const [count, setCount] = useAdd(0);


const Counter = () => {
  const [count, setCount] = useAdd(0);

以上例子,我们可以将操作加这个行为的逻辑和数据封装成一个useAdd hook,然后在各组件中灵活复用,提高了代码的利用率,减少了冗余代码。

React hooks给我们带来了哪些好处?

  • 更好的代码复用性。我们都知道,react的思想就是一个页面可以拆成n个组件,然后用但想数据流的方式将所有内容串联起来。如果我们的react项目比较大,我们会发现我们的类组件往往很庞大也很难复用。在hooks之前,react官方推荐了两种方式来复用组件,一种是使用renderProps的方式,renderProps就是传递一个值为函数的prop来动态渲染组件,例如:
  <Wrapper render={({clickEvent}) => ( 
    <button onClick={clickEvent} /> 
  )}/>

另外一种方式是HOC,例如withRouter将react-router 的 history、location、match 三个对象传入prop。而hooks的custome hooks可以为我们更好的复用组件。

  • 完全使用函数组件,减少生命周期的误用,更方便的管理状态
  • 之前我们往往犹豫我们当前组件是否使用函数组件,因为可能考虑后续该组件中会存在状态,那么在拥有hooks之后,完全可以跳过这方面的考虑了。

总结

React v16.8带来的hooks一定程度上给我们提供了巨大的便利,提供了我们的工作效率,减少了冗余的代码。hooks也是一种趋势,业界也在一直拥抱hooks,因此如果你是react的忠实用户,勇敢使用hooks吧!