image.png

什么是Hooks?

React有两个方式编写组件,类组件与函数组件,因为函数组件相比类组件会更少的代码实现相同的功能,所以一般推荐使用函数组件。

React 16.8版本就新增了Hooks特性,在不编写类组件的情况下就可以使用state等其他React特性。

这两种写法并没有代替的意思,在原来基于class写法的React项目可以继续使用Hooks写法,可以相互并存,不需要修改原先的代码。只需写新代码的时候采用Hooks的方式来实现就可以了。

脸书为什么要推出 Hooks?

很久之前,在Class写法是主流的时候,React其实就已经支持函数式的写法,但缺点是缺少状态、生命周期等一些机制,功能一直受限,直到推出了Hooks。

Hooks更好的体现了React的思想:即从State => View的函数式映射。

Hooks的机制

  • 术语版:把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时 ,产生这个目标结果的代码会重新执行,产生更新的结果。(所以Hooks实际上是钩子的意思)
  • 俗语版:能够把外部的数据绑定到函数的执行。当这个数据产生变化时,函数就能够重新执行。同样,能影响到外面的UI展示数据,都可以绑定通过这个机制绑定到React的函数组件。

这个数据源/事件源可以是:

  • URL的参数
  • State状态
  • 浏览器的参数(窗口大小变化)
  • 等……

好处:

逻辑的复用:
可以说是最大的好处,因为Hooks本质还是普通函数,所以可以在别的函数调用。同时,Hooks中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个Hook执行的结果。
对比Class写法,逻辑复用,必须要借助于高阶组件等复杂的设计模式才能实现,而且这样的组件会产生冗余,使调试变得困难。正因如此,React团队就推出了Hooks写法;

有助于关注分离:如图所示,左为Class写法,右为Hooks写法
image.png

Hooks基础API

useState

声明常量:即在组件初始化的时候就会定义

  1. import React, {useState} from "react";
  2. function fun(){
  3. const [count, setCount] = useState(0)
  4. return (
  5. <div>
  6. <p>You clicked {count} times</p>
  7. <button onClick={() => setCount(count + 1)}>
  8. Click me
  9. </button>
  10. </div>
  11. )
  12. }

关于useState接受的函数

声明函数:只有在开始渲染的时候函数才会执行

  1. const [count, setCount] = useState(()=>{
  2. const initalCount = someComputation(props);
  3. return initialState;
  4. })

上面代码可以计算一个复杂的state的值,然后返回这个值。

在使用setState传一个函数代表什么?

  1. function App() {
  2. const [n, setN] = useState(0);
  3. const onClick = () => {
  4. setN(n + 1);
  5. setN(n + 1);
  6. };
  7. }

上面有两个setN(n + 1),会执行两次的setN(n + 1)吗?不会,执行一次

但如果改成两个setN(n => n + 1) ,就会执行两次,想执行多少次就写多少个。

注意:

stete是不可局部更新的,例如页面有很多数据,但要求只需要改其中的一小段数据,如果是Vue会直接修改这一小段的数据,但React不会这样做,不会像Vue自动合并没改的数据,改的数据会渲染到页面,而没改的数据,因为没自动合并,就不会渲染到页面,就会消失。尝试以下代码:

  1. mport React, {useState} from "react";
  2. import ReactDOM from "react-dom";
  3. function App() {
  4. const [user,setUser] = useState({name:'Jeff', age: 20})
  5. const onClick = ()=>{
  6. setUser({
  7. name: 'Jack'
  8. })
  9. }
  10. return (
  11. <div className="App">
  12. <h1>{user.name}</h1>
  13. <h2>{user.age}</h2>
  14. <button onClick={onClick}>Click</button>
  15. </div>
  16. );
  17. }
  18. const rootElement = document.getElementById("root");
  19. ReactDOM.render(<App />, rootElement);

还有一点,如果setState(obj)的内存地址不变,那么React就认为数据没有改变。

useContext

为了使React能够 全局状态管理 就需要使用useContext。能够在这个组件上的树上的所有组件,都能够访问和修改这个Context.

基本用法

  1. import React, { useContext } from 'react';
  2. //首先需要在组件外面创建一个Constxt(声明上下文)
  3. const AppContext = React.createContext({})
  4. //组件1号
  5. const Navbar = () => {
  6. //在组件内引用该上下文
  7. const {one} = useContext(AppContext)
  8. return (
  9. <div>
  10. //这样就可以显示获取到的共享状态
  11. <p>1号{one}</p>
  12. </div>
  13. )
  14. }
  15. //组件2号
  16. const Navbar2 = () => {
  17. //在组件内引用该上下文
  18. const {tow} = useContext(AppContext)
  19. return (
  20. <div>
  21. //这样就可以显示获取到的共享状态
  22. <p>2号{tow}</p>
  23. </div>
  24. )
  25. }
  26. const Index = () => {
  27. return (
  28. //把想要共享状态的组件放入该上下文中,格式必须是 <xxx.Provider value={共享的状态}></xxx.Provider>
  29. //xxx便是声明的上下文名
  30. <AppContext.Provider value={{tow:2, one: 1}}>
  31. <Navbar/>
  32. <Navbar2/>
  33. </AppContext.Provider>
  34. );
  35. };
  36. export default Index;

尽量不要

useEffect

基础用法

基本用法看我之前的博客:React 函数组件详细

什么是副作用?
虽然说是使用生命周期函数,官方觉得,这个会改变页面环境,就是副作用。因此官方还提供了可以清除副作用的特性。

useLayoutEffect

定义:与useEffect作用一样,但有一点不同,useEffect在浏览器渲染完成后执行,而useLayoutEffect是在浏览器渲染前执行。

因此,不管这两者哪个在前哪个在后,第一个执行的都是useLayoutEffect,其他的按代码顺序来。

那么这两种先使用哪种好?
如果需要改变页面布局之类的,就优先使用useLayoutEffect,而数据改变,就使用useEffect,因为用户需要看到数据的加载过程及变化,为了用户体验好些。

useReducer

复杂版的useState同时也是代替useState方案,这是用来践行 Flux/Redux 的思想。
步骤分为:

  1. 创建初始值 initialState
  2. 创建所有操作 reducer(state, action)
  3. 传给 useReducer,得到读和写API。
  4. 调用写({type: '操作类型'})

如下:

  1. import React, {useReducer} from "react";
  2. import ReactDOM from "react-dom";
  3. //创造初始值
  4. const initial = {
  5. n: 0
  6. }
  7. //创造操作
  8. const reducer = (state, action) => {
  9. if(action.type === "add"){
  10. return {n: state.n + action.number};
  11. } else if(action.type === "multi"){
  12. return { n: state.n * 2}
  13. } else {
  14. throw new Error("unknown type")
  15. }
  16. }
  17. function App(){
  18. //传入读写API
  19. const [state, dispatch] = useReducer(reducer, initial);
  20. const {n} = state;
  21. const onClick = () => {
  22. //调用操作
  23. dispatch({type: "add", number: 1})
  24. }
  25. const onClick2 = () => {
  26. dispatch({type: "add", number: 2})
  27. }
  28. return (
  29. <div className="App">
  30. <h1>n: {n}</h1>
  31. <button onClick={onClick}>+1</button>
  32. <button onClick={onClick2}>+2</button>
  33. </div>
  34. )
  35. }

memo、useMemo、useCallback

memo

在了解React.useMemo前,需要了解React.memo

React经常为了一些没有变化的组件也全渲染一遍。因为这样的问题存在,就出了React.memo,memo的作用是,如果props不变,组件就不会重新渲染。

如下例子:

  1. function App() {
  2. const [n, setN] = React.useState(0)
  3. const [m, setM] = React.useState(0)
  4. const onClick = () => {
  5. setN(n + 1)
  6. }
  7. return (
  8. <div className="App">
  9. <button onClick={onClick}>
  10. update n {n}
  11. </button>
  12. <Child data={m}/>
  13. </div>
  14. )
  15. }
  16. function Child(props){
  17. console.log("Child执行了")
  18. console.log("假设这里有大量代码")
  19. return <div>child: {pros.data}</div>
  20. }

加载页面的时候就会渲染一次,点击App组件里的子组件Child按钮,就会执行一次Child里面的代码。不过因为Child数据没有变化,按道理是不应该再渲染一次。所以就用到了React.memo,在代码下方添加一行const Child2 = React.memo(Child),第13行改成<Child2 />这样Child组件只要props变了才会重新渲染一次。或者可以简写成:

  1. const Child2 = React.memo((props)=>{
  2. console.log("Child执行了")
  3. console.log("假设这里有大量代码")
  4. return <div>child: {pros.data}</div>
  5. })

useMemo

但问题又来了,如果组件中有函数,即使是有memo,也还是会重新渲染。这时就可以使用useMemo如下:

  1. const onClick = useMemo(()=>{
  2. return () => {
  3. console.log('1')
  4. }
  5. })

没错,React.useMemo用法就是useMemo(()=> (x)=> console.log(x)),有两个箭头函数。

React.useMemo作用和React.memo作用是没什么区别,主要是组件里面如果用了函数,可以重用这个函数,而不会在props没变的时候再一次渲染组件。

即使有了memo,为什么函数会让组件重新渲染?上面还没用函数的例子,是使用了数字,数字值与数字值如果之间是相等的,就不会变,不会变用了memo就不会渲染。而对象就不行,例如两个空对象,地址能一样吗?因为App会重新渲染,导致App组件里的对象,即使是同个对象,这两次的对象地址就会不一样,间接相当于props变化了,所以就会子组件重新渲染。

所以,React.useMemo就可以完完全全的,只有props变化了,才会执行。

useCallback

useMemo的用法对于有些人来说有点奇怪,那么useCallback可以代替useMemo,作用完全一样,改写上面的例子如下:

  1. const onClick = useCallback(()=>{
  2. console.log('1')
  3. })

注意

问题:是不是所有回调函数都需要使用 useCallback 或者 useMemo 来封装呢?是不是简单函数就不需要封装?

答:是否应该使用useCallback 或者 useMemo 这与函数的复杂度没有关系。而是回调函数绑定了哪个组件有关系。这是为了避免因为属性频繁的变化而导致不必要的重新渲染。

补充:对于原生的DOM节点,比如 button、input等,是不需要担心重新渲染的。如果事件处理函数是传递给原生节点,就不需要写useCallback 或者 useMemo ,几乎没有影响。如果是自定义组件,或者UI框架的组件,就应该用 useCallback 或者 useMemo 进行封装。

useRef

可以看作是在函数组件之外创建一个容器空间。通过唯一的current属性 设置一个值,从而在函数组件的多次渲染之间共享这个值。同时 ,可以保存DOM节点的能力。

举个例子,实现一个计时器,只需要实现开始和暂停功能两个功能。假设需要用window.setInterval提供计时功能,需要暂停则需要保存 window.setInterval返回的计数器的引用,以此来点击暂停按钮时用 window.clearInterval停止时间。现在问题是,需要在哪里存储这个计数器的应用?

最适合保存的地方就是使用useRef。如下在多次渲染之间共享数据的例子

  1. import React, {useState, useCallback, useRef} from "react";
  2. const ShowTime = () => {
  3. //保存累计时间
  4. const [time, setTime] = useState(0);
  5. //保存变量
  6. const timer = useRef(null);
  7. //开始
  8. const handleStart = useCallback(() => {
  9. //使用current属性设置 ref 的值
  10. timer.current = window.setInterval(() => {
  11. setTime((time) => time + 1)
  12. }, 100)
  13. }, []);
  14. //暂停
  15. const handlePause = useCallback(() => {
  16. window.clearInterval(timer.current)
  17. timer.current = null
  18. })
  19. return (
  20. <div>
  21. {time / 10}
  22. <button onClick={handleStart}>开始</button>
  23. <button onClick={handlePause}>暂停</button>
  24. </div>
  25. )
  26. }
  27. export default ShowTime;

大多数时候,在React中,并不需要关注真实的DOM,在一些场景中,已经需要获取DOM节点的引用,而React的ref属性以及useRef,就可以保存真实的DOM节点,并对这个节点进行操作。

如下需要点击某按钮时,让某个输入框获得焦点:

  1. const inputButton = () => {
  2. const inputEl = useRef(null)
  3. // current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
  4. const onButtonClick = () => {
  5. inputEl.current.focus() //DOM的获取焦点
  6. }
  7. return (
  8. <>
  9. <input ref={inputEl} type="text" />
  10. <button onClick={onButtonClick}>点击</button>
  11. </>
  12. )
  13. }

以上代码,只要渲染到界面上,就可以通过 current 属性访问到真实的DOM节点的实例。

小问题:useRef与useState有什么区别?
useRef即使改变了值,该组件是不会重新渲染,而useState相反。