React学习之hooks

129 阅读10分钟

一、什么是hooks?

hooks是React16.8.0版本增加的新特性/新语法,可以让你在函数组件中使用state以及其他的React新特性。 hook是为了帮助函数组件解决局限性(生命周期,维护state状态),让函数组件能完成类组件的诸多功能的。 因为函数组件能真正地将数据和渲染绑定到了一起。函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式。

二、hooks的使用

2.1 React.useState()

首选先说一下什么是state。中文翻译就是状态,而状态就是组件对象中最重要的属性,值是对象 (可以包含多个key-value的组和)。组件又被称之为状态机,通过更新组件的state来更新对应页面的显示(重新渲染组件)。废话少说,咱们直接上代码。

import * as React from 'react'
export const MyComponent = () => {
  const [state, setSate] = React.useState('程序员')
  const handleChange = () => {
    setSate('李四')
  }
  return (
    <>
      <h1>{state}</h1>
      <button onClick={handleChange}>修改</button>
    </>
  )
}

运行截图: image.png

当我们点击修改按钮:

image.png

我们再来看一下语法

   //state 是状态 set就是修改当前状态的函数 由于这里是解构赋值可以以任意命名
   const [state, setSate] = React.useState('程序员')

此外如果SetState依赖的是上一个state的值,那么setState的参数可以传递一个函数。 修改handleChange如下:

    const handleChange = () => {
    setSate((preState)=>{
      return preState + '-李四'
    })
  }

而当我们多次点击修改之后:

image.png

注意

  • setState是直接替换掉当前state不像class组件中的this.setState去做状态的合并。
  • 当一个组件中有多个state的时候我们可以去声明多个useState

2.2 React.useEffect()

接下来我们看一下useEffect这个函数。effect翻译过来为效应 影响 作用。那么什么东西在React中又被称之为作用呢?

是指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。我们把这些称之为作用或者称之为副作用

在使用class组件的时候我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount,componentDidUpdate和componentWillUnmount。而现在的useEffect就相当与这些声明周期函数钩子的集合体。它以一抵三。废话少说,上代码。

export class ClassMyComponent extends React.Component {
  constructor(props:any) {
    super(props);
    this.state = {
      count: 0
    }
  }
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
      //产生副作用的代码
    document.title = `You clicked ${this.state.count} times`;
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

image.png

当我们改用useEffect时:

export const MyFunComponent = () => {
  const [count, setCount] = React.useState(0)
  React.useEffect(()=>{
     //产生副作用的代码
    document.title = `You clicked ${count} times`;
  })
  return (
    <>
      <button onClick={()=> setCount((preCount) => preCount + 1)}>Click me</button>
    </>
  )
}

是不是瞬间感觉清爽了许多。那么我们应该如何去解绑副作用呢? 在class组件中我们可以在componentDidMount执行副作用,再在componentWillUnmount清除或者解绑副作用。在useEffect中我们可以在useEffect中返回一个函数,在这个函数中去解绑对应的副作用。现在需要实现这样的一个需求,每隔一秒需要让document.title中的点击次数+1。废话少说,上代码。

  export const MyFunComponent = () => {
  const [count, setCount] = React.useState(0)
  React.useEffect(():any=>{
    //产生副作用的代码
    document.title = `You clicked ${count} times`;
    //开起一个定时器
     setInterval(()=>{
      setCount(number=>number+1);
  },1000);    
  },[])
  return (
    <>
      <button onClick={()=> setCount((preCount) => preCount + 1)}>Click me</button>
    </>
  )
}

大概五秒之后浏览器就会卡死。原因就在于我们没有去及时清除定时器。

image.png

修改代码如下

 React.useEffect((): any => {
    //产生副作用的代码
    document.title = `You clicked ${count} times`;
    //开起一个定时器
    let timer = setInterval(() => {
      setCount(number => number + 1);
    }, 1000);
    return () => {
      //清除定时器
      clearInterval(timer)
    }
  })

通过返回一个函数,在组件卸载和更新时调用,在执行副作用函数之前,会先调用上一次返回的函数。

优化 另外useEffect还能接收第二个参数,为数组类型,

  • 我们可以将某些依赖添加到数组当中,只有这些依赖发生了变化才去执行useEffect
  • 如果只想在组件挂载和卸载的时候执行useEffect,可以只传递一个空数组作为第二个参数。
export const MyFunComponent = () => {
  const [count, setCount] = React.useState(0)
  const [other,setOtherCount] = React.useState(0)
  React.useEffect((): any => {
    //产生副作用的代码
    document.title = `You clicked ${count} times`;
    //开起一个定时器
    let timer = setInterval(() => {
      setCount(number => number + 1);
    }, 1000);
    return () => {
      //清除定时器
      clearInterval(timer)
    }
  })//注意 这里没有添加依赖项
  return (
    <>
      <div>count: {count}</div>
      <div>other count: {other}</div>
      <br></br>
      <button onClick={() => setCount((preCount) => preCount + 1)}>Click me</button>
      //如果我们此时频繁点击other(间隔 < 1s) 将导致页面频繁更新,count来不及更新
      <button onClick={()=> setOtherCount((preOtherCount) => preOtherCount + 1 )}>Click other count</button>
    </>
  )
}

所以我们修改如下

React.useEffect((): any => {
    //产生副作用的代码
    document.title = `You clicked ${count} times`;
    //开起一个定时器
    let timer = setInterval(() => {
      setCount(number => number + 1);
    }, 1000);
    return () => {
      //清除定时器
      clearInterval(timer)
    }
  },[count])  //这里添加依赖,那么只有count变化的时候才会去执行React.useEffect

2.3 React.memo && React.useMemo && React.useCallback

memo有翻译过来有备忘录的意思,那么大概率就是和缓存有关了,而useCallback也能做到缓存的目的,之所以会把这两个 hooks 放到一起说,是因为他们的主要作用都是性能优化,且使用useMemo可以实现useCallback

2.3.1 React.memo

React.memo是用于判断一个函数组件是否重新渲染的高阶组件,是否渲染的比较方式依然是浅比较,如果有复杂对象时则无法比较。废话少说,上代码。

index.tsx

import * as React from 'react'
import Child from './Child';
import ChildMemo from './ChildMemo';
export const MyMemo = () => {
  const [step, setStep] = React.useState(0);
  const [count, setCount] = React.useState(0);
  const [number, setNumber] = React.useState(0);
  const handleSetStep = () => {
    setStep(step + 1);
  }
  const handleSetCount = () => {
    setCount(count + 1);
  }
  const handleCalNumber = () => {
    setNumber(count + step);
  }
  return (
    <div>
      <button onClick={handleSetStep}>step is : {step} </button>
      <button onClick={handleSetCount}>count is : {count} </button>
      <button onClick={handleCalNumber}>numberis : {number} </button>
      <hr />
      <Child step={step} count={count} number={number} /> <hr />
      <ChildMemo step={step} count={count} number={number} />
      <br></br>
    </div>
  );
}

Child.tsx

import React from 'react';
type meomProps =  {
  step :number
  count:number
  number:number
}
export default (props:meomProps) => {
    console.log(`--- re-render ---`);
    return (
        <div>
            <p>step is : {props.step}</p>
            <p>count is : {props.count}</p>
            <p>number is : {props.number}</p>
        </div>
    );
};

ChildMemo.tsx

import React, { memo, } from 'react';
type meomProps =  {
  step :number
  count:number
  number:number
}
const isEqual = (prevProps:meomProps, nextProps:meomProps) => {
    if (prevProps.number !== nextProps.number) {
        return false;
    }
    return true;
}

export default React.memo((props:meomProps) => {
    console.log(`--- memo re-render ---`);
    return (
        <div>
            <p>step is : {props.step}</p>
            <p>count is : {props.count}</p>
            <p>number is : {props.number}</p>
        </div>
    );
},isEqual);

image.png 通过以上代码我们发现,ChildMemo组件只有在props.number的值发生变化的时候才去更新当前组件。
使用方式

function MyComponent(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  /*
  return true if passing nextProps to render would return
  the same result as passing prevProps to render,
  otherwise return false
  */
}
export default React.memo(MyComponent, areEqual);

React.memo的使用方式很简单。在函数体之外,在声明一个 areEqual 方法来判断两次 props 有什么不同,如果第二个参数不传递,则默认只会进行 props 的浅比较

2.3.2 React.useMemo

再来说一下React.useMemo。useMemo()返回的是一个memoized的值,只有依赖项发生变才会重新计算这个memoied的值。memoied的值不变的情况下是不会重新触发渲染逻辑的。注意useMemmo()是在渲染期间执行的,所以不能进行一些副作用操作。废话少说,上代码。

import * as React from 'react'
import Child from './Child';
import ChildMemo from './ChildMemo';
export const MyMemo = () => {
  const [step, setStep] = React.useState(0);
  const [count, setCount] = React.useState(0);
  const [number, setNumber] = React.useState(0);
  const handleSetStep = () => {
    setStep(step + 1);
  }
  const handleSetCount = () => {
    setCount(count + 1);
  }

  const Calnumber = React.useMemo(()=>{
    return step + count
  },[step])
  return (
    <div>
      <button onClick={handleSetStep}>step is : {step} </button>
      <button onClick={handleSetCount}>count is : {count} </button>
      <h4>numberis : {Calnumber} </h4>
    </div>
  );
}

通过上面的代码我们发现,只有step变化时才会让Calnumber去更新,在点击handleSetCount是不会去触发渲染逻辑的。

2.3.3 React.useCallback

useCallback功能:与 useMemo类似,不同点是返回值是可缓存的函数,避免每次渲染做不必要的函数创建。
源码实现

function useCallback(callback, args) {
    return useMemo(() => callback, args);
}

当我们把函数传递给子组件的时候记得要将当前函数使用React.useCallback函数进行包裹。 要注意的是函数式组件要避免re-render,还需要结合React.memo来使用。 父组件每一次re-render的时候,在父组件中定义的函数都是新的引用,新的引用对于子组件来说这个props中的函数就不等于之前的那个,此时无论子组件是pureComponent还是用React.memo进行包裹都会触发子组件的re-render。这里想要强调的是props中的函数需要加上useCallback,结合起来使用。
注意 useCallback存在隐式依赖问题,等我们说完useRef在来一起讨论这个问题

2.3 React.useRef()

首先我们先来了解一下Ref对象:Ref对象是我们通过React.createRef或利用React Hooks——useRef生成的一个对象,由于本篇讲述的是React Hooks所以我们只探讨useRef。一个标准的Ref对象往往是以下形式

{
    current:null
}
  • 可以获取DOM元素以及DOM元素的属性
  • 获取组件实例以及组件的方法
  • useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个我们可以利用它创建一个通用容器去保存变量 首先我们来看一下如何获取DOM元素
import * as React from 'react'
export const MyRef: React.FC = React.memo(() => {
  const myRef = React.useRef(null) //使用useRef创建ref对象
  const handleClick = () => {
    console.log(myRef.current);
  }
  return (
    <>   
      <input ref={myRef} defaultValue={'hello'}></input>
      <button onClick={handleClick}>点击</button>
    </>
  )
})

image.png

接下来我们看一下如何获取组件实例
通常为了在组件上添加ref你可能会这么写

<Child ref={textInput} />

紧接着在控制台就会看到这样的错误提示

image.png

根据提示我们将Child组件尝试用forwardRef进行包裹

  const Child = forwardRef((props, ref) => { 
      ref={ref}/>;//** 看我挂到对应的dom上 ** 
  });

创建一个通用容器去保存变量

import * as React from 'react'
import {Child} from './Child'
export const MyRef:React.FC = React.memo(()=>{
  const [timer,setTimer] = React.useState(0)
  const ref1 = React.useRef() as {current:number}
  React.useEffect(()=>{
    ref1.current = setInterval(()=>{
      setTimer(pre => pre + 1)
      console.log(ref1.current);
    },1000)
    return()=>{
      clearInterval(ref1.current)
    }
  },[])
  return(
    <>
      <p>倒计时:{timer}</p>
      <button onClick={()=>{
        clearInterval(ref1.current)
      }}>
        清空倒计时
      </button>
    </>
  )
})

接下来我们把目光转移到useCallback中,在上一节我们提到的隐式依赖到底是什么呢?

import * as React from 'react'
import { useMemoizedFn } from 'ahooks'
const c = React.memo(({ handleClick }:{handleClick:()=> void}) => {
  React.useEffect(()=>{
    console.log('re-render');
  })
  return (
    <button onClick={handleClick}>Click!</button>
  )
})
export  const MyComponent = () => {
  const [clickCount, increaseCount] = React.useState(0);
  // 使用 useCallback 将 handleClick 缓存起来
  const handleClick = React.useCallback(() => {
    increaseCount(count => count + 1);
  }, [])//这里我们没有添加依赖
  return (
    <div>
      <p>{clickCount}</p>
      <Button handleClick={handleClick} />
    </div>
  )
}

通过使用useCallback防止子组件Button重复渲染,但是如果我们在useCallback中添加了动态的依赖可能会导致useCallback失去作用。

const handleClick = React.useCallback(() => {
    increaseCount(count => count + 1);
  }, [count])//添加依赖

image.png 我们会发现随着点击 Button组件在重复渲染,那么应该怎么解决呢?
这就使用到了上面提到的ref

export  const MyComponent = () => {
  const [clickCount, increaseCount] = React.useState(0);
  const countRef = React.useRef<any>()
  // 使用 useCallback 将 handleClick 缓存起来
  const handleClick = React.useCallback(() => {
    // 在取值的地方直接获取 countRef.current 以致于可以获取到最新值
    increaseCount( countRef.current + 1);
  }, [])
  //每次更新时都将最新的 clickCount 的值赋给 countRef.current
  React.useEffect(()=>{
    countRef.current = clickCount
  })
  return (
    <div>
      <p>{clickCount}</p>
      <Button handleClick={handleClick} />
    </div>
  )
}

或者我们直接使用 useMemoizedFn

import * as React from 'react'
import { useMemoizedFn } from 'ahooks'

const Button = React.memo(({ handleClick }:{handleClick:()=> void}) => {
  React.useEffect(()=>{
    console.log('re-render');
  })
  return (
    <button onClick={handleClick}>Click!</button>
  )
})

export  const MyComponent = () => {
  const [clickCount, increaseCount] = React.useState(0);
  const countRef = React.useRef<any>()
  // 使用 useCallback 将 handleClick 缓存起来
  const handleClick =useMemoizedFn(() => {
    // 在取值的地方直接获取 countRef.current 以致于可以获取到最新值
    increaseCount( countRef.current + 1);
  })
  //每次更新时都将最新的 clickCount 的值赋给 countRef.current
  React.useEffect(()=>{
    countRef.current = clickCount
  })
  return (
    <div>
      <p>{clickCount}</p>
      <Button handleClick={handleClick} />
    </div>
  )
}

那我们应该何时去使用refs呢?官网给了我们以下几点建议

  • 管理焦点 文本选择或媒体播放
  • 触发强制动画
  • 集成第三方DOM库

2.4 React.useReducer()

如果你熟悉 Redux 的话,就已经知道它如何工作了。如果不知道的话没关系,咱们慢慢来。reducer语法如下:

const [state, dispatch] = useReducer(reducer, initialState, init);
  • state:状态,类似于我们使用useState()创建好的状态
  • dispatch:直接译为分发,用于触发action中的定义的行为
  • reducer:用于初始化和加工状态。加工时根据旧的stateaction,产生新的state的纯函数
  • initialArg:初始 state
  • init:可选参数 是一个函数。用于将初化state的逻辑提取出来。例如我们想重置state就可以直接调用init函数

代码如下

import * as React from 'react'
const initCount = 0;
function reducer(state, action) {
 switch (action.type) {
   case 'increment':
     return {count: state.count + 1};
   case 'decrement':
     return {count: state.count - 1};
   case 'reset':
     return init(initCount);
   default:
     throw new Error();
 }
}
function init(initCount) {
 return {
   count:initCount 
 }
}
export const MyComponent =  () => {
 const [state, dispatch] = React.useReducer(reducer, initCount,init);
 return (
   <>
     Count: {state.count}
     <button onClick={() => dispatch({type: 'decrement'})}>-</button>
     <button onClick={() => dispatch({type: 'increment'})}>+</button>
     <button onClick={() => dispatch({type: 'reset'})}>reset</button>
   </>
 );
}

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等

2.5 React.useContext()

context意思为山下文,该hooks可用于父子组件以及多级父子组件中的通信。

import * as React from 'react'
//创建上下文对象
const CounterContext = React.createContext('葫芦娃');
//创建子组件
const SubComponent = () => {
 const name = React.useContext(CounterContext);
 return (
   <div>
     子组件:{name}
   </div
 )
}
export const MyComponent = () => {
 const [name, setName] = React.useState('李四')
 return (
   <div>
     <h4>父组件{name}</h4>
     <CounterContext.Provider value={name}>
       <SubComponent></SubComponent>
     </CounterContext.Provider>
     <button onClick={() => setName('王二')}>修改名字</button>
   </div>
 )
}

当我们点击修改按钮时,父组件和子组件的中的name值都会修改。 注意 useContext接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> value prop 决定。

2.6 React.useLayoutEffect()

看这个hooks的名字大概率和布局有关,名字中带有effect是不是useEffect有关系呢?
其实这个hook和useEffect语法上相同,只是而二者的执行时机不同。

  • useEffect是在DOM更新后的渲染到了浏览器之才去执行的,并且是异步,保证了不会阻塞浏览器的渲染过程。
  • useLayoutEffect 是在DOM更新后的渲染到了浏览器之去执行的,并且是同步的,能够阻塞后面的流程 官网上提及到绝大部分场景只用到 useEffect 就可以,只有当它出问题的时候再尝试使用 useLayoutEffect。 可以看出useLayoutEffect使用情况很少。 当我们使用useEffect时快速点击按钮,带有蓝字的按钮,发现会有那么一瞬间,颜色变为蓝色伴有抖动,截图如下。
import * as React from 'react'
export function LayoutEffect() {
 const [color, setColor] = React.useState('yellow');
 React.useEffect(() => {
     setColor('pink')
 })
 return (
   <>
     <div id="myDiv" style={{ background: color }}>颜色</div>
     <button onClick={() => setColor('red')}>红</button>
     <button onClick={() => setColor('blue')}>蓝</button>
   </>
 )
}

image.png

当我们使用useLayoutEffect就不会有抖动的现象

 import * as React from 'react'
 export function LayoutEffect() {
 const [color, setColor] = React.useState('yellow');
 React.useLayoutEffect(()=>{
     setColor('pink')
 })
 return (
   <>
     <div id="myDiv" style={{ background: color }}>颜色</div>
     <button onClick={() => setColor('red')}>红</button>
     <button onClick={() => setColor('blue')}>蓝</button>
   </>
 )
}

三、总结

本文只是简单的介绍了常见hooks的简单用法,没有涉及到原理部分,后续会进行补充。