react hooks篇

106 阅读9分钟

1. hooks的定义

“hooks”直译是“钩子”,它并不仅是React,甚至不仅是前端界的专业术语,而是整个行业所熟知的用语。通常指:

系统运行到某一时期时,会触发被注册到该十几的回调函数

比较常见的钩子有: windows系统的钩子能够监听到系统的各种事件,浏览器提供的onloadaddEventListener能注册在浏览器各种时机被调用的方法。

reaact@16.x 之前,当我们谈论hooks时,我们谈论的可能是“组件的生命周期”

现在以react为例, hooks是:

一系列以“use”作为开头的方法,他们提供了让你可以完全避免class式的写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。

2. 为什么我们需要hooks

2.1 Mixins 的弊端

mixin虽然解决了状态复用的能力, 但是弊端比较多

  1. 当mixins 嵌套的比较多,mixins 嵌套的东西比较多时, 难以追溯的方法与属性

    1. mixins 之间覆盖,同名。导致复用功能失败。

2.2 hooks 的优势

相比于 mixins,它们简直太棒了!

  1. 方法和属性好追溯吗?这可太好了,谁产生的,哪儿来的一目了然。

  2. 会有重名、覆盖问题吗?完全没有!内部的变量在闭包内,返回的变量支持定义别名。

  3. 多次使用,没开N度?你看上面的代码块内不就“梅开三度” 了吗?

  4. 相比较于class 组件, 没有this指向的烦恼。

    2.3 react 组件复用的手法

    react16.x之前

    渲染属性(Render Props)高阶组件(Higher-Order Components)

    react 16.x 之后 hooks

渲染属性指的是使用一个值为函数的prop来传递需要动态渲染的nodes或组件。如下面的代码可以看到我们的DataProvider组件包含了所有跟状态相关的代码,而Cat组件则可以是一个单纯的展示型组件,这样一来DataProvider就可以单独复用了。

import Cat from 'components/cat'
class DataProvider extends React.Component {
  constructor(props) {
    super(props);
    this.state = { target: 'Zac' };
  }
​
  render() {
    return (
      <div>
        {this.props.render(this.state)}
      </div>
    )
  }
}
​
<DataProvider render={data => (
  <Cat target={data.target} />
)}/>

高阶组件这个概念就更好理解了,说白了就是一个函数接受一个组件作为参数,经过一系列加工后,最后返回一个新的组件。看下面的代码示例,withUser函数就是一个高阶组件,它返回了一个新的组件,这个组件具有了它提供的获取用户信息的功能。

const withUser = WrappedComponent => {
  const user = sessionStorage.getItem("user");
  return props => <WrappedComponent user={user} {...props} />;
};
​
const UserPage = props => (
  <div class="user-container">
    <p>My name is {props.user}!</p>
  </div>
);
​
export default withUser(UserPage);

3.基本的hooks使用

1. useState()

let showFruit = true;
function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  
  if(showFruit) {
    const [fruit, setFruit] = useState('banana');
    showFruit = false;
  }
 
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
复制代码

这样一来,

  //第一次渲染
  useState(42);  //将age初始化为42
  useState('banana');  //将fruit初始化为banana
  useState([{ text: 'Learn Hooks' }]); //...//第二次渲染
  useState(42);  //读取状态变量age的值(这时候传的参数42直接被忽略)
  // useState('banana');  
  useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错

react是怎么保证多个useState的相互独立的?

react是根据useState出现的顺序来定的。 鉴于此,react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。

在同一个事件中并不会因为调用了两次setCount而让count增加两次,试想如果在同一个事件中每次调用setCount都生效,那么每调用一次setCount组件就会重新渲染一次,这无疑使非常影响性能的;实际上如果修改的state是同一个,最后一个setCount函数中的新state会覆盖前面的

2. useEffect

通过使用useEffect 解绑一些副作用(比如组件创建一个定时器,组件销毁是清空)

useEffect(() => {
    let ter = setInterval(() => {}, 3000)
    return () => {
        clearInterval(ter)
    }
})

3. useEffect && useLayoutEffect

  • 使用

这两个hook用法一致,第一个参数是回调函数,第二个参数是数组,数组的内容是依赖项deps,依赖项改变后执行回调函数;注意组件每次渲染会默认执行一次,如果不传第二个参数只要该组件有state改变就会触发回调函数,如果传一个空数组,只会在初始化执行一次。另外,如果用return返回了一个函数,组件每次重新渲染的时候都会先执行该函数再调用回调函数。

  • 区别

表面上看,这两个hook的区别是执行时机不同,useEffect的回调函数会在页面渲染后执行;useLayoutEffect会在页面渲染前执行。实际上是React对这两个hook的处理不同,useEffect是异步调用,而useLayoutEffect是同步调用。 那什么时候用useEffect,什么时候用useLayoutEffect呢? 我的理解是视情况而定 如果回调函数会修改state导致组件重新渲染,可以useLayoutEffect,因为这时候用useEffect可能会造成页面闪烁; 如果回调函数中去请求数据或者js执行时间过长,建议使用useEffect;因为这时候用useLayoutEffect堵塞浏览器渲染。

4. useMemo && useCallback

这两个hook可用于性能优化,减少组件的重复渲染;现在就来看看这两个神奇的hook怎么用。

  • uesMemo
function MemoDemo() {
    let [count, setCount] = useState(0);
    let [render,setRender] = useState(false)
    const handleAdd = () => {
        setCount(count + 1);
    };
    const Childone = () => {
        console.log("子组件一被重新渲染");
        return <p>子组件一</p>;
    };
    const Childtwo = (props) => {
        return (
            <div>
                <p>子组件二</p>
                <p>count的值为:{props.count}</p>
            </div>
        );
    };
    const handleRender = ()=>{
        setRender(true)
    }
    return (
        <div style={{display:"flex",justifyContent:'center',alignItems:'center',height:'100vh',flexDirection:'column'}}>
            {
                useMemo(() => {
                    return <Childone />
                }, [render])
            }
            <Childtwo count={count} />
            <button onClick={handleAdd}>增加</button>
            <br/>
            <button onClick={handleRender} >子组件一渲染</button>
        </div>
    );
}
复制代码

Childone组件只有render改变才会重新渲染

这里顺带讲下,React.memo,用React.memo包裹的组件每次渲染时会和props会和旧的props进行浅比较,如果没有变化则组件不渲染;示例如下

const Childone = React.memo((props) => {
    console.log("子组件一被重新渲染",props);
    return <p>子组件一{props.num}</p>;
})
function MemoDemo() {
    let [count, setCount] = useState(0);
    let [render,setRender] = useState(false)
    let [num,setNum] = useState(2)
    const handleAdd = () => {
        setCount(count + 1);
    };
   
    const Childtwo = (props) => {
        return (
            <div>
                <p>子组件二</p>
                <p>count的值为:{props.count}</p>
            </div>
        );
    };
    const handleRender = ()=>{
        setRender(true)
    }
    return (
        <div style={{display:"flex",justifyContent:'center',alignItems:'center',height:'100vh',flexDirection:'column'}}>
            {/* {
                useMemo(() => {
                    return <Childone />
                }, [render])
            } */}
            <Childone num={num}/>
            <Childtwo count={count} />
            <button onClick={handleAdd}>增加</button>
            <br/>
            <button onClick={handleRender} >子组件一渲染</button>
        </div>
    );
}
​
复制代码

这个例子是把上个例子中的Childone拆出来套上React.memo的结果,点击增加后组件不会该组件不会重复渲染,因为num没有变化

  • useCallback

还是上面那个例子,我们把handleRenderuseCallback包裹,也就是说这里num不变化每次都会传同一个函数,若是这里不用useCallback包裹,每次都会生成新的handleRender,导致React.memo函数中的props浅比较后发现生成了新的函数,触发渲染

const Childone = React.memo((props) => {
    console.log("子组件一被重新渲染",props);
    return <p>子组件一{props.num}</p>;
})
export default function MemoDemo() {
    let [count, setCount] = useState(0);
    let [render,setRender] = useState(false)
    let [num,setNum] = useState(2)
    const handleAdd = () => {
        setCount(count + 1);
    };
   
    const Childtwo = (props) => {
        return (
            <div>
                <p>子组件二</p>
                <p>count的值为:{props.count}</p>
            </div>
        );
    };
    const handleRender = useCallback(()=>{
        setRender(true)
    },[num])
    return (
        <div style={{display:"flex",justifyContent:'center',alignItems:'center',height:'100vh',flexDirection:'column'}}>
            {/* {
                useMemo(() => {
                    return <Childone />
                }, [render])
            } */}
            <Childone num={num} onClick={handleRender}/>
            <Childtwo count={count} />
            <button onClick={handleAdd}>增加</button>
            <br/>
            <button onClick={handleRender} >子组件一渲染</button>
        </div>
    );
}
复制代码

5. useRef

1. useRef 的基本使用
  • 返回一个可变的 ref 对象,该对象只有个 current 属性,初始值为传入的参数( initialValue )。
  • 返回的 ref 对象在组件的整个生命周期内保持不变
  • 当更新 current 值时并不会 re-render ,这是与 useState 不同的地方
  • 更新 useRef 是 side effect (副作用),所以一般写在 useEffect 或 event handler 里
  • useRef 类似于类组件的 this

这个hook通常用来获取组件实例,还可以用来缓存数据❗ 获取实例就不过多解释了,需要注意的是只有类组件才有实例; 重点看下useRef如何缓存数据的:

function UseRef() {
    let [data, setData] = useState(0)
    let initData = {
        name: 'lisa',
        age: '20'
    }
    let refData = useRef(initData)   //refData声明后组件再次渲染不会再重新赋初始值
    console.log(refData.current);
    refData.current = {       //修改refData后页面不会重新渲染
        name: 'liyang ',
        age: '18'
    }
    const reRender = () => {
        setData(data + 1)
    }
    return (
        <div>
            <button onClick={reRender}>点击重新渲染组件</button>
        </div>
    )
}
复制代码

组件重新渲染后,变量会被重新赋值,可以用useRef缓存数据,这个数据改变后是不会触发组件重新渲染的,如果用useState保存数据,数据改变后会导致组件重新渲染,所以我们想悄悄保存数据,useRef是不二选择👊

2. 为什么要使用useRef

跨渲染取值

import React, { useState } from "react";
const LikeButton: React.FC = () => {
    const [like, setLike] = useState(0)
    function handleAlertClick() {
        setTimeout(() => {
            alert(`you clicked on ${like}`) 
            //形成闭包,所以弹出来的是当时触发函数时的like值
        }, 3000)
    }
    return (
        <>
            <button onClick={() => setLike(like + 1)}>{like}赞</button>
            <button onClick={handleAlertClick}>Alert</button>
        </>
    )
}
export default LikeButton

现象: 在like为6的时候, 点击 alert , 再继续增加like到10, 弹出的值为 6, 而非 10. (形成了不同渲染结果不同步的情况)

使用useRef

import React, { useRef } from "react";
const LikeButton: React.FC = () => {
  // 定义一个实例变量
  let like = useRef(0);
  function handleAlertClick() {
    setTimeout(() => {
      alert(`you clicked on ${like.current}`);
    }, 3000);
  }
  return (
    <>
      <button
        onClick={() => {
          like.current = like.current + 1;
        }}
      >
        {like.current}赞
      </button>
      <button onClick={handleAlertClick}>Alert</button>
    </>
  );
};
export default LikeButton;

现象: 在like为6的时候, 点击 alert , 再继续增加like到10, 弹出的值为 10.

总结 采用useRef, 作为组件实例的变量, 保证获取到的数据肯定是最新的

3. createRef() 与 useRef()的不同

createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用

4. useImperativeHandle

使用useImperativeHandle用于定义暴露给父组件的ref方法

import React, {
    MutableRefObject,
    useState,
    useImperativeHandle,
    useRef,
    forwardRef,
    useCallback
} from 'react'
interface IProps {
    label: string
}
let ChildInput = forwardRef((props: IProps, ref: any) => {
    const { label } = props
    const [value, setValue] = useState('')
    // 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
    // 参数1: 父组件传递的ref属性
    // 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
    useImperativeHandle(ref, () => ({
        getValue
    }))
    const handleChange = (e: any) => {
        const value = e.target.value
        setValue(value)
    }
    const getValue = useCallback(() => {
        return value
    }, [value])
    return (
        <div>
            <span>{label}:</span>
            <input type="text" value={value} onChange={handleChange} />
        </div>
    )
})
const ParentCom: React.FC = (props: any) => {
    const childRef: MutableRefObject<any> = useRef({})
    const handleFocus = () => {
        const node = childRef.current
        alert(node.getValue())
    }
    return (
        <div>
            <ChildInput label={'名称'} ref={childRef} />
            <button onClick={handleFocus}>focus</button>
        </div>
    )
}
export default ParentCom

4. 自定义hook

自定义hook,也就是hook的封装,至于为什么要封装hook呢?react官网给出了答案

使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。 通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

先来看下这个例子:

export default function CustomHook() {
    let refone = useRef(null)
    let X, Y, isMove = false,left,top
    //基于鼠标事件实现拖拽
    useEffect(() => {
        const dom = refone.current
        dom.onmousedown = function (e) {
            isMove = true
            X = e.clientX - dom.offsetLeft;
            Y = e.clientY - dom.offsetTop;
        }
        dom.onmousemove = function (e) {
            if (isMove) {
                left = e.clientX - X
                top = e.clientY - Y
                dom.style.top = top + "px"
                dom.style.left = left + "px"
            }

        }
        dom.onmouseup = function (e) {
            isMove = false
        }
    }, [])
    return (
        <div style={{ display: "flex", justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
            <div ref={refone} style={{ width: '70px', height: '70px', backgroundColor: '#2C6CF9',position:'absolute' }}></div>
        </div>
    )
}
复制代码

我们利用鼠标事件简单的实现了一个拖拽方格的效果,那如果在其他页面也需要这个效果呢?😏于是,我们可以考虑把这段相同的逻辑封装起来,就像我们提取公共组件一般。来看下面这个例子:

import {useEffect, useRef } from "react";
function useDrop() {
    let refone = useRef(null)
    let X, Y, isMove = false,left,top
    //基于鼠标事件实现拖拽
    useEffect(() => {
        const dom = refone.current
        dom.onmousedown = function (e) {
            isMove = true
            X = e.clientX - dom.offsetLeft;
            Y = e.clientY - dom.offsetTop;
        }
        dom.onmousemove = function (e) {
            if (isMove) {
                left = e.clientX - X
                top = e.clientY - Y
                dom.style.top = top + "px"
                dom.style.left = left + "px"
            }

        }
        dom.onmouseup = function (e) {
            isMove = false
        }
    }, [])
    return refone
}
export default function CustomHook() {
    let refone = useDrop()
    let reftwo = useDrop()
    return (
        <div style={{ display: "flex", justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
            <div ref={refone} style={{ width: '70px', height: '70px', backgroundColor: '#2C6CF9',position:'absolute' }}></div>
            <div ref={reftwo} style={{ width: '70px', height: '70px', backgroundColor: 'red',position:'absolute' }}></div>
        </div>
    )
    }
复制代码

这里为来减少代码量只展示了在同一个页面使用封装过的hook,事实上封装这段hook我们只改了几行代码,却实现了逻辑的重用是不是很神奇!😆需要注意的是hook的封装函数必须要以use开头,因为使用hook本身是有规则的,比如不能在条件语句中使用hook,不能在函数外使用hook;如果不适用use开头封装hook,则react无法自动检查该函数内使用的hook是否符合规则。