目录

前言

一、useState

1、基本用法

2、useState异步更新

3、Hooks书写位置

二、useEffect

1、认识useEffect

2、useEffect的运行机制

3、回调函数中常见副作用

三、useContext

四、性能优化的Hooks

1、React.memo

2、useMemo

3、useCallback

五、useReducer

六、函数组件的ref

1、useRef

2、forwardRef

3、useImperativeHandle

七、自定义Hooks

八、小案例

九、相关面试题

1、useEffect模拟函数组件生命周期

2、自定义hooks的好处

3、React常用的hooks

4、useCallback useMemo区别

5、浏览器的重排 重绘


前言

React Hooks是React16.8引入的新特性,它是基于函数组件开始设计的,只支持函数组件。有了Hooks,我们可以在函数组件中使用state和react的其他特性,让函数组件具备状态管理能力,它还可以让我们的代码更简洁,更好的复用逻辑,这篇文章就主要讲解我在项目中会用到的Hooks


一、useState

1、基本用法

useState是函数中最基础且最核心的hooks,有了useState函数组件就拥有了管理和添加状态的能力

import {useState} from "react"
function App(){
const [state,setState]=useState(initialState)
}

initialState:状态的初始值,这个初始值可以是任意的值(数字、字符串、对象、数组、函数等等)

useState的返回值是一个包含两个元素的数组,第一个元素state是当前的状态值,第二个元素setState是更新状态的函数

初始值为数字类型

import React, { useState } from "react";
function App (){
 const [num,setNum]=useState(0)
 const fn=()=>{
  setNum(num+1)
 }
 return<>
 <h1>我是函数组件{num}</h1>
 <button onClick={fn}>按钮</button>
 </>
}
export default App

初始值为字符串

import { useState } from "react";
function App (){
 const [str,setStr]=useState("Hello")
 const fn=()=>{
  setStr("小明")
 }
 return<>
 <h1>我是函数组件{str}</h1>
 <button onClick={fn}>按钮</button>
 </>
}
export default App

初始值为数组

import { useState } from "react";
function App (){
 const [arr,setArr]=useState([1,2,3])
 const fn=()=>{
  setArr([...arr,45,6])
 }
 return<>
 <h1>我是函数组件{arr}</h1>
 <button onClick={fn}>按钮</button>
 </>
}
export default App

初始值为对象

function App (){
 const [obj,setObj]=useState({name:"张三",age:18})
 const fn=()=>{
  setArr({...obj,name:"王五"})
 }
 return<>
 <h1>我是函数组件{obj.name}:{obj.age}</h1>
 <button onClick={fn}>按钮</button>
 </>
}
export default App

如果有多个数据,多次调用useState就可以了,调用一次useState只修改一个数据

 const [arr,setArr]=useState([1,2,3])
 const [obj,setObj]=useState({name:"张三",age:18})
 const [str,setStr]=useState("Hello")

2、useState异步更新

useState返回的更新对象的方法是异步的,要在下次重绘才能获取新值,不能立即获取更新后的状态

function App() {
  const [num, setNum] = useState(0)
  const fn = () => {
    setNum(100)
    console.log(num)
  }
  return <>
    <h1>{num}</h1>
    <button onClick={fn}>按钮</button>
  </>
}

控制台输出

useState会将多次更新合并为一次

function App() {
  const [num, setNum] = useState(0)
  const fn = () => {
    //将多次更新合并为一次
    setNum(num+1)
    setNum(num+1)
    setNum(num+1)
  }
  return <>
    <h1>{num}</h1>
    <button onClick={fn}>按钮</button>
  </>
}
export default App
function App() {
  const [num, setNum] = useState(0)
  const fn = () => {
    //将多次更新合并为一次
   setTimeout(()=>{
setNum(num+1)
},1000) 
    
  }
  return <>
    <h1>{num}</h1>
    <button onClick={fn}>按钮</button>
  </>
}
export default App

如果我们不想将useState多次更新合并为一次,我们可传入一个回调函数,也就是函数式更新

function App() {
  const [num, setNum] = useState(0)
  const fn = () => {
    setNum((state) => {
      return state + 1
    })
    setNum((state) => {
      return state + 1
    })
    setNum((state) => {
      return state + 1
    })
  }
  return <>
    <h1>{num}</h1>
    <button onClick={fn}>按钮</button>
  </>
}
export default App

注:fn()属于嵌套函数

新的state需要使用之前的state计算得出,将函数传给setState,函数将接收之前的state,返回更新后的值

3、Hooks书写位置

所有Hooks都不能放在条件判断,循环语句,嵌套函数,类组件中,因为我们每次更改的数据并没有像setState那样将数据存起来。react依赖于hooks调用的顺序来正确管理状态的。如果我们将Hooks放在了条件语句,循环,或函数嵌套中,可能会导致hooks调用的顺序在渲染之间发生变化,就会引起顺序错乱

所有Hooks都要在函数组件的顶层作用域中直接调用,还可以在自定义hook的顶层作用域中直接调用

二、useEffect

1、认识useEffect

useEffect是一个钩子,可以让我们在函数组件中执行副作用操作,它类似于类组件中的生命周期钩子,所以我们可以使用useEffect来模拟生命周期的挂载阶段(componentDidMount),更新阶段(componentDidUpdate),卸载阶段(componentWillUnmount)

  useEffect(() => {
    //可以在这里写副作用(组件挂载或更新时执行)
    return () => {
      //清理函数(组件卸载/依赖更新时执行)
    }
  },[])//依赖数组

useEffect有两个参数,一个是回调函数,另一个是依赖数组。

回调函数:回调函数是一个包含副作用逻辑的函数,可以发送请求,操作dom,订阅,也可以返回一个回调函数,用于组件卸载,或下一次依赖更新前用于取消订阅,清除定时器等清理操作

依赖数组:一个数组,包含了useEffect所依赖的值,依赖数组可以省略,可以是一个空数组

2、useEffect的运行机制

依赖数组的三种情况

没有依赖数组:会在每次组件渲染后都会执行回调函数。

function App() {
 const [num,setNum]=useState(0)
 useEffect(()=>{
   console.log("组件挂载了")
 })
 const fn=()=>{
  setNum(num+1)
 }
  return <>
    <p>我是函数组件{num}</p>
    <button onClick={fn}>按钮</button>
  </>
}

空依赖数组:只会在组件挂载时执行一次,如果有清理函数的话,会在卸载时执行清理函数。

function App() {
 const [num,setNum]=useState(0)
 useEffect(()=>{
   console.log("组件挂载了")
return ()=>{
console.log("我卸载了")
 },[])
 const fn=()=>{
  setNum(num+1)
 }
  return <>
    <p>我是函数组件{num}</p>
    <button onClick={fn}>按钮</button>
  </>
}

当依赖数组发生变化时:useEffect会在每次渲染后比较依赖项,如果依赖项有变化,就会执行回调函数,并在执行当前回调函数之前,如果有清理函数的话,会先执行上一次的清理函数

function App() {
 const [num,setNum]=useState(0)
 useEffect(()=>{
   console.log("组件挂载了")
 },[num])
 const fn=()=>{
  setNum(num+1)
 }
  return <>
    <p>我是函数组件{num}</p>
    <button onClick={fn}>按钮</button>
  </>
}

注:清理函数就是return后面的回调函数

3、回调函数中常见副作用

副作用:就是与组件渲染无关的操作,如数据获取、操作dom、订阅事件等,这些操作可能会影响其它组件

1、获取数据

const [num,setNum]=useState(0)
 useEffect(()=>{
  fetch('/api/data').then(res=>setNum(res))
 })

2、操作dom

//操作dom
const [num,setNum]=useState(0)
 useEffect(()=>{
  document.num=1
 })

3、事件监听或订阅

useEffect(()=>{
  window.addEventListener("click",fn)
  return ()=>window.removeEventListener("click",fn)
 })

4、定时器

useEffect(() => {
    let timer = setInterval(() => {}, 1000)
    return () => clearInterval(timer)
  }, [])

注:副作用通常需要清理,可以防止内存泄漏,过时闭包等问题

三、useContext

useContext是React提供的用于跨组件通信的Hooks,可以让组件直接使用useContext访问上下文的值,不用逐层传递props

useContext基本用法:是在子组件中使用,来消费父组件提供的数组

//context.js文件
import React from "react";
const myContext=React.createContext("")
export default myContext
//父组件
function App() {
  const [num, setNum] = useState(0)
  const fn = () => {
    setNum(num + 1)
  }
  return <MyContext.Provider value={num}>
    <h1>{num}</h1>
    <button onClick={fn}>按钮</button>
    <Child/>
  </MyContext.Provider>
}
//子组件
function Child(){
 const childContext=useContext(myContext)
 return <>
 <h1>{childContext}</h1>
 </>
}

四、性能优化的Hooks

1、React.memo

React.memo是一个高阶组件,主要用于函数组件的性能优化,避免因为父组件的重新渲染而导致不必要的子组件的渲染,它只检查组件的props是否发生变化,如果props没有改变,不会重新渲染子组件

//父组件
function App() {
  const [num, setNum] = useState(0)
  const [childData,setChildData]=useState(1)
  console.log("组件重新渲染了")
  const fn = () => {
    setNum(num + 1)
  }
  return <MyContext.Provider value={num}>
    <h1>{num}</h1>
    <button onClick={fn}>按钮</button>
    <Mymemo data={childData} />
  </MyContext.Provider>
}
const Mymemo=React.memo(Child)
//子组件
function Child(props){
 console.log("子组件渲染")
 return <>
 <h1>{props.data}</h1>
 </>
}

注:函数组件没有识别props的能力,每次父组件更新相当于给子组件传了一个新的props。有了React.memo,只有子组件用到的属性发生变化了,子组件才会重新渲染更新。React.memo是浅层比较props变化

2、useMemo

useMemo是React提供的用于性能优化的Hooks,用来缓存函数的结果或者组件,避免在每次渲染时都进行复杂的计算。useMemo是同步的,不能进行副操作,如获取数据,取消订阅等等

//useMemo基本语法
useMemo(()=>{return 值},[])

useMemo的第一个参数是回调函数,返回需要缓存的值,第二个参数是依赖项数组,只有当依赖项发生变化时才会重新计算

useMemo缓存计算结果

function App() {
 const [count,setCount]=useState(1)
 const [price,setPrice]=useState(100)
 const fn=()=>{
  return count*price
 }
 const myMemo=useMemo(fn,[count,price])
 return<>
 <p>{myMemo}</p>
 <button>按钮</button>
 </>
}

缓存组件

//子组件
function Child({count,price}){
 console.log("子组件渲染")
 return <>
 <h1>{count*price}</h1>
 </>
}
//父组件
function App() {
  const [count,setCount]=useState(10)
  const [price,setPrice]=useState(100)
  const MyChild=useMemo(()=>{
    return <Child count={count} price={price}/>
  },[count,price])
  return <>
  {MyChild}
  </>
}

3、useCallback

useCallback也是React提供的用于性能优化的钩子函数,用于缓存函数,避免在每次渲染时,都创建新的函数实例。当一个组件传递回调给子组件,尤其是在子组件进行了性能优化时,useCallback很有用

const returnFunction=useCallback(()=>{},[])
//父组件
function App() {
  const [count,setCount]=useState(0)
  const myUsecallback=useCallback(()=>{
    return 123
  },[count])
  return<>
  <MyChild fn={myUsecallback}/>
  <button onClick={()=>setCount(10)}>按钮</button>
  <p>我是子组件{count}</p>
  </>
}
const MyChild=React.memo(Child)
//子组件
function Child({ fn }) {
  console.log("我是子组件")
  return <>
    <p>我是子组件{fn()}</p>
  </>
}
export default Child

如果不使用useCallback缓存函数的话,即便子组件没有使用父组件的值,父组件每次重新渲染,都会重新创建函数,子组件也会重复渲染更新

useCallback要配合React.memo一起使用,原因也是函数组件没有识别props的能力,每次父组件更新,相当于给子组件传了一个新的props

注:useCallback,useMemo不要大量使用,因为缓存本身很耗性能,React底层会比较两次值变没变,比较的过程很耗性能,如果两次的值没有变化,就不会更新,用缓存的值,变化了才会进行更新

五、useReducer

useReducer是React提供的一个hooks,可以用于处理更复杂的组件状态逻辑,是useState的替代方案,它适合于状态逻辑复杂,涉及多个子值,或者是下一个状态要依赖于之前的状态,它还可以与context API相结合,实现简单的全局状态管理

基本用法

const [state,dispatch]=useReducer((state,action)=>newState,initialArg,init)

第一个参数是回调函数(reducer函数),这个函数返回一个新的state

第二个参数是state的初始值

第三个参数是用于计算初始值的函数,这个参数也可以不传

useReducer的工作原理

定义reducer函数:它是一个纯函数,接收当前state和action,返回新的state

初始化state:可以直接提供初始值,或通过初始化函数生成

获取state和dispatch:useReducer返回当前state和dispatch的值

触发更新:通过dispatch发送action来更新state

reducer函数写法一

//myReducer.js
function myReducer(state,action){
  if(action=="add"){
    return state+1
  }else if(action=="minus"){
    return state-1
  }else{
    return 0
  }
}
export {myReducer}
//App.jsx
import React, { useReducer } from "react";
import { myReducer } from "./myReducer";
function App() {
 const [state,dispatch]=useReducer(myReducer,0)
 return <>
 <p>{state}</p>
 <button onClick={()=>dispatch("add")}>+</button>
 <button onClick={()=>dispatch("minus")}>-</button>
 </>
}
export default App

reducer函数写法二

action是一个包含type属性的对象

//reducer.js
function myReducer(state, action) {
  switch (action.type) {
    case "add":
      return state + 1
    case 'minus':
      return state - 1
      default:0
  }
}
export { myReducer }
//App.jsx
import React, { useReducer } from "react";
import { myReducer } from "./myReducer";
function App() {
 const [state,dispatch]=useReducer(myReducer,0)
 return <>
 <p>{state}</p>
 <button onClick={()=>dispatch({type:'add'})}>+</button>
 <button onClick={()=>dispatch({type:'minus'})}>-</button>
 </>
}
export default App

注:如果我们在项目中使用useReducer,我们可以重新创建一个myReducer.js文件,专门用来存放reducer函数,可以让我们代码的可读性更高,便于我们进行维护

六、函数组件的ref

1、useRef

useRef是react提供的可以在函数组件中创建引用对象的hooks,它可以用来直接操作dom元素,如获取输入框的值,设置焦点,测量dom元素的尺寸等等。它还可以存储任意不需要引发组件重新渲染的变量

useRef访问dom元素

useRef(initialValue)返回一个包含current属性的对象,initialValue可以是任意类型,如number,string,object

function App(){
 const myRef=useRef()
 const getRef=()=>{
  console.log(myRef.current)
 }
 return<>
 <p ref={myRef}>888</p>
 <button onClick={getRef}>按钮</button>
 </>
}

useRef存储不需要引发重新渲染的变量

function App() {
  const myRef = useRef()
  useEffect(() => {
   //myRef.current存储的是定时器的id
    myRef.current = setInterval(() => {
      myRef.current++
      console.log("页面一个加载了" + myRef.current + "s")
    }, 1000)
    return () => {
      clearInterval(myRef.current)
    }
  }, [])
  return <>
    <p>666</p>
    <button>按钮</button>
  </>
}

useRef的特点:useRef返回的对象在组件的整个生命周期中都是持久的,不会在每次渲染都重新的进行创建,每次渲染返回的都是同一个对象。当useState中状态改变时,组件会重新渲染,但当useRef的current属性改变时,组件不会重新渲染

useRef与createRef的区别:createRef会在每次组件渲染的时候,ref都会被重新进行创建,而useRef不会,它只会在组件首次渲染的时候创建。

注:一个ref属性只能访问一个dom元素

2、forwardRef

forwardRef是react的一个高阶组件,可以让子组件的ref传递给父组件,父组件可以直接通过ref访问子组件内部的dom元素,或子组件实例

基本用法

子组件中使用forwardRef包裹子组件,让组件能接收父组件传递的ref,并将其绑定到内部元素上

子组件接收两个参数,第一个参数是props,第二个参数是父组件传递的ref(只有当子组件被forwardRef包裹时才会接收)

//子组件
import React,{forwardRef} from "react";
function Child(props,ref){
  return<>
  <input ref={ref}/>
  </>
}
export default forwardRef(Child)

父组件通过ref属性获取子组件内部绑定的dom元素

import React, {useRef } from "react";
function App() {
 const myRef=useRef()
//子组件input表单获得焦点
 const fn=()=>{
  myRef.current.focus()
 }
 return<>
 <Child ref={myRef}/>
 <button onClick={fn}>按钮</button>
 </>
}
export default App

注:forwardRef仅对函数组件和类组件有效,不能直接用于dom元素

3、useImperativeHandle

useImperativeHandle是React提供的一个高级hook,用于自定义通过ref暴露给父组件的实例值,它需要与forwardRef结合使用,允许子组件控制哪些属性或方法可以被父组件访问

基本用法

useImperativeHandle(ref,createHandle,[])//[]依赖数组

ref:从forwardRef中获得的ref

createHandle:是一个回调函数,返回一个对象,这个对象包含要暴露给父组件的属性或方法

依赖数组:当依赖数组发生变化时,createHandle会重新执行已更新暴露的对象,如果省略,每次渲染都会重新创建

子组件通过forwardRef接收父组件传递的ref

import React,{forwardRef,useImperativeHandle} from "react";
function Child(props,ref){
 return<>
 <input ref={ref}/>//父组件传递的ref
 </>
}
export default forwardRef(Child)

在子组件中,使用useImperativeHandle来定义暴露给父组件的值

//Child.jsx
import React,{forwardRef,useImperativeHandle,useRef,useState} from "react";
function Child(props,ref){
  const childRef=useRef()
  const [data,setData]=useState("我是子组件的数据")
  const changeNum=()=>{
    console.log("我改变了子组件的方法")
  }
  const changeInput=()=>{
    childRef.current.focus()
  }
  const changeData=()=>{
    setData("123456")
  }
//暴露给父组件的属性和方法
useImperativeHandle(ref,()=>({
  data,
  changeNum,
  changeInput,
  changeData
}))
 return<>
 <p>{data}</p>
 <input ref={childRef}/>
 </>
}
export default forwardRef(Child)

父组件通过ref可以访问子组件暴露的值

//App.jsx
import React, {useRef } from "react";
function App() {
 const myRef=useRef()
 const fn=()=>{
  myRef.current.changeInput()
  myRef.current.changeNum()
  myRef.current.changeData()
 }
 return<>
 <Child ref={myRef}/>
 <button onClick={fn}>按钮</button>
 </>
}
export default App

注:函数组件没有实例对象,我们可以使用useImperativeHandle只暴露必要的属性或方法给父组件,保持组件的封装性,不能过度使用,要优先考虑props和状态提升

七、自定义Hooks

自定义hooks是一个以use开头的js函数,它的内部可以调用其它的hooks,可以实现逻辑复用,让我们的代码更简洁,更易于维护

自定义hooks创建步骤

1、做项目时,我们要观察组件中哪些逻辑可以在多个地方复用或者它是足够独立、复杂的逻辑

2、我们在项目目录下创建一个以use打头的文件,将状态、副作用等可复用的逻辑代码提取到这个文件中定义的函数里,这个函数要以小驼峰的方式命名

3、提取完复用逻辑后,我们要考虑这个hook需要从外部接受什么参数,以及它需要向外部返回什么数据或方法

4、我们在要使用自定义hook的组件中,引入它,然后像使用内置hook一样使用自定义hook就可以了

自定义hook返回值问题:内置hook的返回值是不同的,有的hook返回数组,有的hook返回的是变量或函数,所以自定义hook的返回值,可以根据情况返回不同类型的数据

案例:封装表单

自定义hook返回数组

//useFormInput.js
//自定义hook
import { useState } from "react";
function useFormInput(initValue){
  const [value,setValue]=useState(initValue)
  const changeHandle=(e)=>{
    setValue(e.target.value)
  }
  return [value,changeHandle]
}
export default useFormInput
//在App.jsx组件中使用自定义hook
import useFormInput from "./useFormInput";
function App() {
 const [value,onChange]=useFormInput("")
 const [value1,onChange1]=useFormInput("")
 return<>
 <h1>{value}{value1}</h1>
 <input value={value} onChange={onChange}/>
 <input value={value1} onChange={onChange1}/>
 </>
}
export default App

自定义hook返回对象

//useFormInput.js
//自定义hook
import { useState } from "react";
function useFormInput(initValue){
  const [value,setValue]=useState(initValue)
  const changeInput=(e)=>{
      setValue(e.target.value)
  }
   return {value:value,onChange:changeInput}
}
export default useFormInput
//App.jsx
import useFormInput from "./useFormInput";
function App() {
const username=useFormInput("")
const passWord=useFormInput("")
return<>
<p>{username.value} {passWord.value}</p>
<input {...username}/>
<input {...passWord}/>
</>
}
export default App

八、小案例

渲染商品

function Trolley(){
   const [data, setData] = useState([
    { id: 1, name: "电脑", price: 8000, selected: false, count: 1 },
    { id: 2, name: "巧克力", price: 10, selected: false, count: 1 },
    { id: 3, name: "蛋糕", price: 80, selected: false, count: 1 },
    { id: 4, name: "辣条", price: 8, selected: false, count: 1 },
    { id: 5, name: "蛋挞", price: 6, selected: false, count: 1 },
  ])
  return <>
    {
      data.map((item, index) => <div>
        <img src={item.selected ? sel : unsel} alt=""  width="15" onClick={()=>selectedSome(index)}/>
        <span>{item.name}</span>
        <span>{item.price}</span>
        <button onClick={()=>changeCount("-",index)}>-</button>
        <span>{item.count}</span>
        <button onClick={()=>changeCount("+",index)}>+</button> &nbsp;
        <button onClick={()=>del(index)}>删除</button>
      </div>)
    }
   总价:<img src={selectedAll? sel:unsel} alt="" onClick={changeAll} width="15"/>{totalPrice}
  </>
}
 export default Trolley

点击加减按钮,商品的数量会发生变化,注意当商品数量为1时,不能再减少商品了

//点击加减按钮
const changeCount = (flag, index) => {
    const narr = [...data]
    if (flag == "+") {
      narr[index] = { ...narr[index], count: narr[index].count + 1 }
    }else if(flag=="-"&&narr[index].count>1){
      narr[index]={...narr[index],count:narr[index].count-1}
    }
    setData(narr)
  }

点击未选中的小圆圈,小圆圈会被选中,小圆圈我是用图片做的

//先引入图片
import unsel from "./assets/unsel.png"
import sel from "./assets/sel.png"
//渲染图片
 <img src={item.selected ? sel : unsel} alt=""  width="15" onClick={()=>selectedSome(index)}/>
//小圆圈点击事件
 const selectedSome=(index)=>{
   const narr=[...data]
   narr[index]={...narr[index],selected:!narr[index].selected}
   setData(narr)
  }

点击最下面的小圆圈,上面的小圆圈会全部选中,再点击一次,上面的小圆圈会全部取消

let selectedAll=useMemo(()=>{
    return data.filter(item=>item.selected).length==data.length
  },[data])
  const changeAll=()=>{
    let narr=selectedAll?{...data.map(item=>item.selected),selected:false}:{...data.map(item=>item.selected),selected:true}
    setData(narr)
  }

计算选中商品的总价

const totalPrice=useMemo(()=>{
  let selectedArr=data.filter(item=>item.selected)
  let total=0
  selectedArr.forEach(item =>total+=item.price*item.count);
  return total
},[data])

删除商品

const del=(index)=>{
  const narr=[...data]
  narr.splice(index,1)
  setData(narr)
}

注:react中修改数组或对象要先浅拷贝一份数据,整体替换,不能直接修改原数据

九、相关面试题

1、useEffect模拟函数组件生命周期

在面试中的回答:

useEffect本质是用来处理函数组件副作用的,它提供了一种声明式,关注副作用本身与状态依赖关系的方式。我们可以向useEffect中传入两个参数,第一个参数是回调函数,第二个参数是空依赖数组,来模拟函数组件的挂载阶段。我们可以通过省略useEffect的第二个参数或向依赖数组中传入数据,这样当useEffect在每次渲染时或依赖数组发生变化时,会触发更新阶段的生命周期。在useEffect的第一个参数的回调函数中,我们可以通过返回一个清理函数,来模拟函数组件卸载时的生命周期。这个清理函数不仅会在组件卸载时执行,在下一次副作用执行前,也会执行上一次的清理函数,避免内存泄漏。如果对于一些需要同步执行的,涉及到dom布局的副作用,使用useLayoutEffect来模拟函数组件生命周期的挂载阶段和更新阶段会更合适

2、自定义hooks的好处

自定义hook本质上是一个js函数,必须要以use开头,它的内部可以调用其它的内置hooks,它只能在react函数组件或其它自定义hooks中调用。在项目中,我通常会在表单处理,获取数据时使用自定义hooks,这样可以避免在多个组件中重复使用相同的逻辑,实现逻辑复用,让代码的可读性更高,更易于维护

3、React常用的hooks

面试中我们先回答常用的hooks,再回答几个不是很常用的的hooks就可以了

React Hooks时React16.8引入的新特性,它让函数组件拥有与类组件同样的能力。我在做项目时,会用useState来管理组件的状态,它返回一个包含两个元素的数组,它的状态更新是异步的,多次调用会被合并为一次,可以避免组件重复渲染,提高程序性能。我还会使用useEffect来处理副作用,如获取数据,手动修改dom,订阅等等,它还可以来模拟函数组件的挂载阶段(componentDidMount),更新阶段(componentDidUpdate)和卸载阶段(componentWillUnmount)的生命周期。这两个hooks我在做每个react项目时几乎都会用到。

我有时会使用useContext来消费父组件或其他兄弟组件传来的数据。当遇到状态逻辑复杂的数据或下一个状态需要依赖前一个状态时,我会使用useReducer替代useState来管理数据。当遇到需要进行大量的复杂运算的情况时,我会使用useMemo来缓存计算结果。当遇到父组件需要传递回调函数给子组件时,我会使用useCallback来缓存这个回调函数,避免子组件因为父组件每次重新渲染而进行不必要的渲染,使用useCallback时,我会配React.memo一起使用,因为函数组件没有识别props的能力,每次父组件更新,相当于给子组件传了一个新的props。当我需要直接访问dom元素或存储不需要引发组件重新渲染的变量时,我会使用useRef,它每次返回的都是同一个对象并且在组件的整个生命周期中都是持久的,不会在每次渲染时都重新的进行创建

面试中还可能会问:

React中父组件数据更新了,为什么子组件也会跟着更新呢

函数组件没有识别props的能力,每次父组件更新相当于给子组件传了一个新的props。我们可以使用React官方提供的高阶组件React.memo,来包裹子组件,只有子组件用到的属性发生变化了,子组件才会重新渲染更新

4、useCallback useMemo区别

useCallback和useMemo都是react提供的用于性能优化的hooks,它们都接收两个参数一个是回调函数,一个是依赖项,当依赖项没发生变化时,返回上一次缓存的结果,避免不必要的内存开销。useCallback返回一个函数,可以避免组件每次渲染时,都重新的创建函数实例。useMemo返回一个值,避免每次渲染时,都进行复杂的计算

5、浏览器的重排 重绘

重绘就是当元素的外观属性发生改变时,不影响其布局的情况下,浏览器重新绘制元素的过程,如修改元素的背景颜色,字体颜色

重排也叫回流,当元素的几何属性发生改变时,浏览器需要重新计算元素在页面中的布局位置,重新进行布局的过程,如修改元素的宽度、高度

重排的代价更高,因为它总触发重绘,在项目中,我们可以通过批量操作dom,使用css动画代替js动画来开启GPU加速,还可以通过使用虚拟滚动,防抖或节流,requestAnimationFrame来调度动画帧,当涉及元素显示和隐藏时,我们可以使用visibility:hidden来代替display:none等方案来减少重排

面试中还可能会问:

重排会引起浏览器的重新绘制吗

不会,浏览器有一个优化机制,脏位标记,在出现这个机制之前,发生重排操作会引起浏览器整个页面的重新绘制,这是非常消耗性能的过程,所以浏览器对此进行了一个优化,引入了脏位标记,浏览器会把几何属性发生改变的元素标记下来,标记完后,浏览器只渲染被标记了的元素,不会影响其他元素,可以优化浏览器渲染的效率和性能

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐