什么是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如何达到这两种效果。
- componentDidMount
useEffect(() => {
// mount时会调用
},[])
其中第二个参数可以传一个数组,如果数组为空,则只在componentDidMount的时候执行。如果传入一个state,当该state变化导致页面重新渲染时,useEffect也会执行,达到componentDidUpdate的效果。
- componentDidUpdate 和 componentDidMount
useEffect(() => {
// mount或者update都会调用
})
- 模拟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时执行的效果。
- 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吧!