二、Hooks处理逻辑监听状态以及自定义hook

393 阅读5分钟

1.useState

1.做一个简单的计数器来了解useState()
const [count, setCount] = useState(0); // useState 可以触发组件更新 条件就是通过setCount来触发
const num = 1;                         // 普通js变量无法触发组件更新
const add = () => {
    setCount(count + 1)
};
const remove = () => {
    setCount(count - 1);
};
function App() {
    return (
        <div className="App">
              <h1>{count}</h1>
              <h1>{num}</h1>
              <button onClick={add}>计数+</button>
              <br />
              <button onClick={remove}>计数-</button>
        </div>
    );
}
如果一个变量不用于JSX 中显示,那就不要用setState来管理它,使用useRef()
2.state --不可变数据
  • props 父组件传递过来的数据
  • state 组件内部的状态信息,不对外
  • state变化, 触发组件的更新,重新渲染rerender页面
import React, { FC, useState } from 'react'
const UseStateDemo: FC = () => {
  const [userInfo, setUserInfo] = useState({ name: 'A类', age: 20 })
  const [arr, setArr] = useState(['a', 'b', 'c', 'd', 'e'])
  const changeUserInfo = () => {
    // 不可变数据
    setUserInfo({
      ...userInfo, // 解构赋值
      age: 18,
    })
  }
  const changeArrAdd = () => {
    setArr(arr.concat('eqw')) // concat返回一个新数组
    setArr(arr.push('eqw')) // push 返回长度
    setArr([...arr, 'eqw'])
  }
  return (
    <div>
      <h1>State 不可变数据</h1>
      <span>不可变数据 不去修改state的值,而是要传入一个新的值 </span>
      <h3>对象</h3>
      <h2>{userInfo.name}</h2>
      <h2>{userInfo.age}</h2>
      <button onClick={changeUserInfo}>改变用户信息</button>&nbsp;&nbsp;
      <br />
      <h2>{arr.map(item => item + '   ')}</h2>
      <button onClick={changeArrAdd}>改变数组</button>
    </div>
  )
}
export default UseStateDemo
1.异步更新
const [count, setCount] = useState(0);
const add = () => {
     // (5)每一次传入的值都是一样的值 并且会被合并
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    // 使用函数则不会被合并
    setCount(count => count + 1)
    setCount(count => count + 1)
    setCount(count => count + 1)
    setCount(count => count + 1)
    setCount(count => count + 1)
    console.log(count, 'count')  // 异步更新 无法直接拿到最新的state值
};

2.useEffect

(1)副作用

函数组件:执行函数,返回 JSX
    初次渲染时
    state 更新时
    
但有些场景需要如下功能
    当渲染完成时,做某些事情
    当某个 state 变化时,做某些事情
    如 ajax 加载数据(state 变化重新加载)

所以需要 useEffect

(2) 组件渲染完成时

// 组件是一个函数  执行返回 JSX片段, 组件初次渲染执行这个函数
// 任何Sate 更新 都会触发组件的更新(重新执行函数)
useEffect(() => {
    console.log('加载http请求')
  }, [])

(3)state更新时

// 数组里边是依赖项  根据依赖项的改变而触发, 没有依赖 组件第一次触发时更新
useEffect(() => {
    console.log('list被改变')
  }, [list]) 

(4)组件销毁时

  // 回调函数中 return一个回调函数  会执行组件的销毁
  useEffect(() => {
    console.log('加载http请求')
    return () => {
      console.log('销毁')
    }
  }, [])

(5)为什么初次加载会执行俩次

// 模拟组件挂载、销毁、重新挂载的完整流程,及早发现后续的问题。如果只挂载一次,有可能卸载组件时有问题。
// 实际项目中某些组件真的有可能会被挂载很多次(如重置 state),要及早模拟这种情况,
// 避免出现重复挂载的问题(如弹窗重复、bindEvent 重复)
// 生产环境下不会再执行俩次
  useEffect(() => {
    console.log('加载http请求')
  }, [])

3.useRef

  • 一般用于操作DOM
  • 也可以传入普通的JS变量,但是不会触发render
  • 必须要和Vue3的ref区分开,二者还是有很大的区别
  • vue3中ref用于监听响应式数据
  • react中ref用于获取DOM操作
1.操作DOM
export const UseRefDemo: FC = () => {
  // HTMLInputElement  ts 泛型
  const inputRef = useRef<HTMLInputElement>(null)
  const selectInput = () => {
    // current 当前指向哪个元素
    console.log(inputRef.current, '----ref---')
    const inputElem = inputRef.current
    if (inputElem) inputElem.select() // DOM节点,select DOM操作的API
  }
  return (
    <div>
      <input ref={inputRef} defaultValue="我是useRef" />
      <button onClick={selectInput}>选中input</button>
    </div>
  )
}

2.定义变量
export const UseRefDemo: FC = () => {
  const nameRef = useRef('我是useRef') // 普通的JS变量
  const changeName = () => {
    nameRef.current = '2421412' // 修改ref值,不会触发render
    console.log(nameRef.current, '----nameRef.current----') // 这里值已经被改变但是并没有render
  }
  return (
    <div>
      <p>name: {nameRef.current}</p>
      <button onClick={changeName}>改变</button>
    </div>
  )
}

4.useMemo

  • 函数组件,每次state更新都会重新执行函数并且render
  • useMemo 可以缓存数据, 不用每次执行函数都重新生成
  • 可用于计算量较大的场景,缓存提高性能 === 空间换时间
  • react官网中有一句话叫 你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证 zh-hans.reactjs.org/docs/hooks-…
export const UseMemoDemo: FC = () => {
  console.log('render')
  const [num1, setNum1] = useState(10)
  const [num2, setNum2] = useState(20)
  const [num3, setNum3] = useState(30)
  const [str, setStr] = useState('我是useMemo')
  const sum = useMemo(() => {
    console.log('sum------') // 缓存
    return num1 + num2 + num3
  }, [num1, num2, num3])
  
  return (
    <div>
      <h1>UseMemoDemo</h1>
      <p>{sum}</p>
      <p>
        {num1} &nbsp;&nbsp;&nbsp;<button onClick={() => setNum1(num1 + 1)}>Add Num1</button>
      </p>
      <p>
        {num2} &nbsp;&nbsp;&nbsp;<button onClick={() => setNum2(num2 + 1)}>Add Num2</button>
      </p>
      <p>
        {num3} &nbsp;&nbsp;&nbsp;<button onClick={() => setNum3(num3 + 1)}>Add Num3</button>
      </p>
      <p>
         // 改变change的时候并没有触发 sum 
        <input value={str} onChange={e => setStr(e.target.value)} />  
      </p>
    </div>
  )
}

4.useCallback

  • useCallback 就是 useMemo 的语法糖,和 useMemo 一样
export const UseCallbackDemo: FC = () => {
  const [text, setText] = useState('useCallback')
  const fn1 = () => console.log('fn1 useCallback', text)
  // useCallback 就是 useMemo 的语法糖,和 useMemo 一样
  const fn2 = useCallback(() => {
    console.log('fn2 useCallback', text)
  }, [text])
  return (
    <div>
      <h1>UseCallbackDemo</h1>
      <button onClick={fn1}>FN1</button>
      <button onClick={fn2}>FN2</button>
      <input value={text} onChange={e => setText(e.target.value)} />
    </div>
  )
}

5.自定义Hook

  • 内置Hooks 保证了基础功能
  • 内置Hooks 灵活配合,实现业务功能
  • 抽离公共部分, 自定义Hooks 或者第三方Hooks---复用代码,提高效率
  • 之前是Class 组件,现在是函数组件
  • class组件: Mixin HOC render-props 来复用公共逻辑
  • 函数组件: 使用Hooks --- 当前最完美的解决方案
1.修改网页标题Hook
// 这个比较简单
const useTitle = (title: string) => {
  document.title = title
}
使用 useTitle('标题')

2.获取鼠标位置
export const useMouse = () => {
  const [x, setX] = useState(0)
  const [y, setY] = useState(0)
  const mouseMoveHandler = useCallback((event: MouseEvent) => {
    setX(event.clientX)
    setY(event.clientY)
  }, [])

  useEffect(() => {
    // mousemove  监听鼠标事件
    window.addEventListener('mousemove', mouseMoveHandler)

    // 组件销毁时,解绑DOM事件  可能会出现组件内存泄露问题
    return () => {
      window.removeEventListener('mousemove', mouseMoveHandler)
    }
  }, [])
  return {
    x,
    y,
  }
}
使用: const { x, y } = useMouse()

3. 模拟异步加载数据
// 异步获取信息
function getInfo(): Promise<string> {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(Date.now().toString())
    }, 1500)
  })
}
export const useGetInfo = () => {
  const [loading, setLoading] = useState(true)
  const [info, setInfo] = useState('')
  useEffect(() => {
    getInfo().then(info => {
      setLoading(false)
      setInfo(info)
    })
  }, [])
  return {
    loading,
    info,
  }
}
使用:  const { loading, info } = useGetInfo()   
       <span>{loading ? '加载中' : info}</span>

5.第三方hook

  • ahooks 国内流行 https://ahooks.js.org/zh-CN/hooks/use-title/
  • 功能全面
  • 使用简单
  • 文档 demo 清晰易懂
  • React-use 国外流行 https://github.com/streamich/react-use

6.Hooks 使用规则

  • 必须使用useXX格式命名
  • 只能在两个地方调用hook,(组件内,其他hook内)
  • 不能把Hook放在if for 内部, 必须保证每次调用的顺序一致

7.Hooks 闭包陷阱

  • 当异步函数获取state时,可能不是当前最新的state
  • 可使用useRef来解决
  • 提前了解JS闭包
export const ClosureTrap: FC = () => {
  const [count, setCount] = useState(0)
  const countRef = useRef(0)

  // useEffect(() => {
  //   countRef.current = count
  // }, [count])

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

  // 闭包
  const alertFn = () => {
    setTimeout(() => {
      alert(count) // 值类型
      // alert(countRef.current) // 引用类型
    }, 3000)
  }

  return (
    <div>
      <h1>闭包陷阱</h1>
      <div>{count}</div>
      <button onClick={add}>增加</button>
      <button onClick={alertFn}>Alert</button>
    </div>
  )
}

// 复现闭包的场景  
    1.先点击增加按钮  
    2.然后点击alert---再快速点击增加按钮
    3.出现弹窗,弹窗内的值还是之前的值,setTimeout异步拿的不是点击增肌之后最新的值
    4.然后可以使用useRef解决这个陷阱, 可以打开屏蔽代码,注释alert()值类型 体验一下