«
前端进阶——React Hook基础

时间:2022-4   


前言

前置学习内容

一、Hook 介绍基础 Hook API

1.什么是Hook

Hook“钩子”不是React特有,是计算机程序设计术语

hook例子:

我们可以通过hook把需要的状态、副作用方法钩进来,放在函数内部使用。让原本呆板的react函数拥有状态和生命周期。

2.Hook出现的意义

解决Class问题:

React Hook:函数式编程;关注状态变化响应式编程;关注分类,逻辑费用。Hook不擅长组件实例、异步闭包引用、非幂调用等场景。因此在实际开发过程中,需要根据实际的场景来选择class和hook

3.基础Hook API

1.useState —— 让函数拥有内部状态

function functionState(){
  const [state,setState] = useState(initialState);
}
/*
 *接受一个参数作为初始化值(initialState)
 *返回一个数组,数组的第一个值为最新的状态 state,第二个值为一个函数用于修改状态 setState
 *setState 设置后需要触发重新渲染
 */

2.useEffect——让函数可以执行副作用

函数组件中没有生命周期,那么可以使用useEffect来替代。如果你熟悉React class的生命周期函数,你可以把useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

useEffect(didUpdate,dependencies);
//该hook接收一个包含命令式、且有可能有副作用代码的函数(didUpdate)
//dependencies为[]时,仅函数第一次执行时执行。
//不传,函数每次执行时都会重新执行。
//传入依赖[a,b,...]函数每次执行时,如果依赖有变化,则重新执行第一个回调方法。

//在依赖变化和销毁前可以执行清理函数。
useEffect(
  //do update works
  return function(){
    //do clear works
  }
);

函数副作用是指函数在正常工作任务之外对外部环境所施加的影响。具体地说,函数副作用是指函数被调用,完成了函数既定的计算任务,但同时因为访问了外部数据,尤其是因为对外部数据进行了写操作,从而一定程度地改变了系统环境。函数的副作用也有可能是发生在函数运行期间,由于对外部数据的改变,导致了同步运行的外部函数受到影响。

3.useContext——让函数可以接收content

如果想要组件之间共享状态,可以使用useContext。React 的 Context API 是一种在应用程序中深入传递数据的方法,而无需手动一个一个在多个父子孙之间传递 prop。

如果普通的父子组件之间传参,即父子组只有单纯的一层时,用props传参更省事,useContext反而不是最佳的选择,官方也是这样推荐的(建议用组合组件来完成传参)。

使用 Context ,首先顶层先声明 Provier 组件,并声明 value属性,接着在后代组件中声明 Consumer 组件,这个 Consumer子组件,只能是唯一的一个函数,函数参数即是 Context 的负载。

const myContext = React.createContext();

function TestUseState(){
    return (
        <myContext.Provider value={{name:"xxx",age:21}}>
            <SubComponent />
        </myContext.Provider>
    );
}

function SubComponent(){
    const value = useContext(myContext);
    return (
        <div>
            <myContext.Consumer>
                {value=> <div>myName:{value.name}</div>}
            </myContext.Consumer>
            <div>myAge:{value.age}</div>
        </div>
    );
}

/*
 *React.createContext创建一个context类
 *myContext.Provider提供value(生产者)
 *myContext.Consumer和useContext(myContext)使用value(消费者)
 */

4.Hook 规则

二、自定义 Hook 扩展 Hook API

1.自定义Hook

有时可能会希望在组件之间重用一些 state 的逻辑,一般之前的做法都是通过高阶组件或者是 render props 来解决。自定义 Hook 能够做到这种需求,而不需要向 tree 中增加更多组件。

使用自定义hook可以将某些组件逻辑提取到可重用的函数中。自定义hook是一个从use开始的调用其他hook的Javascript函数。请记住,组件和hook是函数,因此我们在这里实际上并没有创建任何新概念。我们只是将代码重构为另一个函数以使其可重用。

简单的来说,自定义Hook仅仅是把原函数的逻辑提取出来,放到另一个函数中去执行。

自定义Hook规则

自定义hook:const [ xxx , ... ] = useXXX(参数A,参数B...)

这我们需要清楚的知道传进去了什么以及我们需要传出来什么。并且在自定hook中常常需要加入限制避免不必要的性能开销。

2.扩展Hook API

useReducer —— 独立状态管理

useReducer是React提供的一个高级Hook,它不像useEffect、useState、useRef等必须hook一样,没有它我们也可以正常完成需求的开发,但useReducer可以使我们的代码具有更好的可读性、可维护性、可预测性。

const [state,dispatch] = useReducer(reducer,initialArg,init);
function reducer(state,action){
  switch(action.rype){
    case actionOne:
      funcOne();
      break;
    ...
  }
}
dispatch({type:action});
//state 状态,dispatch 触发状态更新,reducer 状态修改器

reducer是一个利用action提供的信息,将state从A转换到B的一个纯函数,具有一下几个特点:

useCallback —— 缓存函数

const memoizedCallback = useCallback(()=>{
  doSomething(a,b);
},[a,b]);

避免在component render时候声明匿名方法,因为这些匿名方法会被反复重新声明而无法被多次利用,然后容易造成component反复不必要的渲染。使用useCallback hook还可以避免bind操作。执行后返回一个被缓存的(memoized)回调函数,常用于性能优化。简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。

应用场景:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

useMemo —— 缓存计算结果

const memoizedValue = useMemo(
  ()=>{
    computeExpensiveValue(a,b)
  }
,[a,b]);

/*
 *第一个函数参数会在渲染期间执行
 *第二个参数依赖列表是执行条件,和useEffect一致
 */

简单来说就是传递一个创建函数和依赖项,创建函数会需要返回一个值,只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值。

有时我们修改组件中的某个数值是,但其他数值没有改变,当我们render执行还是会生成有新的内存地址的对象,那么就算带着memo的Child组件,也会跟着重新render, 尽管最后其实Child使用到的值没有改变。这样就多余render了,感觉性能浪费了。于是useMemo作为一个有着暂存能力的:

const Hook =()=>{
    const [count, setCount] = useState(0)
    const [name, setName] = useState('rose')

   const data = useMemo(()=>{
        return {
            name
        }
    },[name])

   /*return {
    *  name
    *}
    *即使name值没有改变,但是count值更新render,name都会重新创建
    */

    return(
        <div>
            <div>
                {count}
            </div>
            <button onClick={()=>setCount(count+1)}>update count </button>
            <Child data={data}/>
        </div>
    )
}

const Child = memo(({data}) =>{
    return (
        <div>
            <div>child</div>
            <div>{data.name}</div>
        </div>
    );
})

React.memo

React.memo(MyComponent, areEqual);

React.memo()是一个高阶函数,它与React.PureComponent类似,但是一个函数组件而非一个类。React.memo()可接受2个参数,第一个参数为纯函数的组件,第二个参数用于对比props控制是否刷新,与shouldComponentUpdate()功能类似。当我们对父组件进行重新渲染时有时我们并不需要子组件进行重新渲染,这是就可以使用memo来判断子组件是否需要渲染。

缓存比较

memo>useCallback>useMemo

  1. 优先做纯函数组件优化
  2. 再缓存传入的方法
  3. 最后做精细化的渲染缓存控制

useRef —— 函数存储器

ref的作用

1.引用某个dom节点或者某个类组件

React.createRef()//创建一个ref
<input ref="inputRef" /> //this.refs['inputRef']来访问
<CustomInput ref="comRef" /> //this.refs['comRef']来访问
  1. 回调函数

回调函数就是在dom节点或组件上挂载函数,函数的入参是dom节点或组件实例,达到的效果与字符串形式是一样的,都是获取其引用。回调函数的触发时机:

  1. 组件渲染后,即componentDidMount后
  2. 组件卸载后,即componentWillMount后,此时,入参为null
  3. ref改变后
<input ref={(input) => {this.textInput = input;}} type="text" />//1.dom节点上使用回调函数
<CustomInput ref={(input) => {this.textInput = input;}} />//2.类组件上使用

useRef:

const refContainer = useRef(initialValue)
/*
 *useRef调用后返回一个有current属性的"盒子"
 *给组件的ref传递后,current被设置为组件实例
 *current可以存储任何对象
 */

使用useRef可以创建refContainer对象,我们可以将这个对象赋值给某个组件。这样,我们就可以通过访问refContainer.current就可以访问对应的DOM对象。

组件中有什么东西可以跨渲染周期,也就是在组件被多次渲染之后依旧不变的属性?第一个想到的应该是state。没错,一个组件的state可以在多次渲染之后依旧不变。但是,state的问题在于一旦修改了它就会造成组件的重新渲染。那么这个时候就可以使用useRef来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染。

useImperativeHandle——实例化的"曲线救国"

useImperativeHandle介绍

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。

React.forwardRef:子组件通过React.forwardRef来创建,可以将ref传递到内部的节点或组件,进而实现跨层级的引用。

const FancyButton = React.forwardRef((props, ref) => (
  <div>
    <input ref={ref} type="text" />
    <button>{props.children}</button>
  </div>
));

// 父组件中使用子组件的 ref
function App() {
  const ref = useRef();
  const handleClick = useCallback(() => ref.current.focus(), [ ref ]);

  return (
    <div>
      <FancyButton ref={ref}>Click Me</FancyButton>
      <button onClick={handleClick}>获取焦点</button>
    </div>
  )
}

上面例子中创建了一个 FancyButton 组件,内部渲染了一个 button 元素,我们希望在父元素 App 中渲染 FancyButton,并通过传递给 FancyButton 的 ref 直接操作内部的 button。

附录

进阶学习内容

参考文献: