React Hooks 详解

129 阅读5分钟

学习内容:

  1. 状态 useState
  2. 副作用 useEffect useLayoutEffct
  3. 上下文 useContext
  4. Redux useReducer
  5. 记忆 useMemo 回调useCallback
  6. 引用 useRef useimperativeHandle
  7. 自定义Hook useDebugValue

一、useState

useState 改变状态

1. useState不可局部更新

const App =()=>{
    const [user,setN] = React.useState({name:'yuyuan',age:18})
    const onClick = ()=>{
        setN({...user,age:user.age+1})  //不继承user的话,name会被undefined
    }
}

完整代码

2. setState地址要变

const onClick =()=>{
    // setN({...user,age:user.age+1})
    user.age +=1 //这就是错误写法了
}

3. useState接受函数作为参数传入

const [user,setN] = React.useState(()=>({name:'yuyuan',age:9+9}))

完整代码

4. setState接受函数作为参数传入

const onClick = ()=>{
    setN((user)=>({...user,age:user.age+1}))
     setN((user)=>({...user,age:user.age+1}))
}

完整代码

二、useReducer

useReducer示例

image.png

import React from 'react'
import ReactDOM from 'react-dom'

const initial = {n:0}
const reducer = (state,action)=>{
    if(action.type==='add'){
        return {n:state.n+action.number}
    }else if(action.type==='divide'){
        return {n:state.n-action.number}
    }else{
        throw new Error('unknow type')
    }
}

const App =()=>{
    const [state,dispatch] = React.useReducer(reducer,initial)
    const {n} = state
    const onClick = ()=>{
        dispatch({type:'add',number:1})
    }
    const onClick2 =()=>{
        dispatch({type:'add',number:2})
    }
    return (
        <div>
            <p>{n}</p>
            <p><button onClick={onClick}>+1</button></p>
            <p><button onClick={onClick2}>+2</button></p>
        </div>
    )
}

ReactDOM.render(<App/>,document.getElementById('root'))

一个用useReducer的表单的例子

(真的难写,useReducer:复杂版的useState)

三、如何代替Redux

题外知识:关于析构赋值

字符串、数组中

//按顺序用[]赋值就好了
var x = [1, 2, 3, 4, 5];
var [y, z] = x;
console.log(y); // 1
console.log(z); // 2

对象中

//标明key,来对应赋值
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
// foo = 'aaa'
// bar = 'bbb'
 
let { baz : foo } = { baz : 'ddd' };
// foo = 'ddd'

用useReducer和Context代替Redux

代替Redux的完整代码

image.png

模块化Redux的替代码

  • 先把依赖的都搞出去,例如ajax和Context,在 index.js中引入;
  • 然后把组件抛出去,组件中和index.js中都要注意引入依赖;
  • 然后把reducer中的方法写出去,写出对象中的函数,然后继承过来,然后再匹配使用

四、useContext

useContext使用示例

useContext的三句表达式

1. 创建上下文

const C = React.createContext(null)

2.确定作用域和传入数据

<C.Provider value={{n:n,setN:setN}}>……</C.Provider>

3.作用域中使用上下文

const {n,setN} = React.useContext(C)
const {n} = React.useContext(C)

useContext的注意事项

不是响应式的 : 你在一个模块中改变C中的数据,另一个模块不会感受到这个变化,是重新渲染后逐级通知的,才知道。

五、useEffect

effect:副作用——改变环境

用途:弥补函数组件中缺失生命周期的设置

  • 作为componentDidMount()使用,[]作为第二个参数
  • 作为componentDidUpdate()使用,[]中可指定依赖,不写[]即为全部
  • 作为componentWillUnmount()使用,通过return

特点:如果同一生命周期类型的useEffect,会按照出现次序执行

import React, { useEffect } from "react";
import ReactDOM from "react-dom";

const App = () => {
const [n, setN] = React.useState(0);
const onClick = () => {
    setN((x) => x + 1);
};

useEffect(() => {
    console.log("first render");
}, []); //第一次渲染执行

useEffect(() => {
    console.log("render都执行");
}, []); //渲染即执行

useEffect(() => {
    if (n > 0) {
    console.log("变化后执行");
}}, [n]); //依赖n

useEffect(() => {
    const id = setTimeout(() => {
    console.log("我是延时函数");
    }, 2000);
    return window.clearTimeout(id);
    }, []); //走的时候带走垃圾

useEffect(() => { console.log(1);}, []);
useEffect(() => {console.log(2);}, []);
//肯定先打印1,后打印2的啊

return (
<div>
n:{n} <button onClick={onClick}>+1</button>
</div>
)};

ReactDOM.render(<App />, document.getElementById("root"));

六、useLayoutEffect

写一个useLayoutEffect的和useEffect对比就知道是干什么的了,就是执行顺序的差别。

useEffect是创建完虚拟DOM和真实的DOM,UI渲染完成后执行;useLayoutEffect是在真实DOM创建后立即执行。

所以,useLayoutEffect总是比useEffect先执行。实例

useLayoutEffect如果不改变Layout布局,就最好不要用。

Hook API 索引 – React (reactjs.org)

七、useMemo

1.memo初次使用

作用

react中有多余的render,如果props不变,就没必要再执行一个函数组件,于是使用React.memo对代码进行优化

useMemo对代码进行优化

const Child = (props)=>{
    console.log('内部代码')
    return (
        <div>{props.data}</div>
    )
}

//React.memo优化 这样props.data值不变,Child就不会重新执行
const Child = React.memo(props=>{
    console.log('内部代码')
    return (
        <div>{props.data}</div>
    )
})

useMemo与监听函数

React.memo中写监听函数,会导致失效,即这个被memo的函数组件没有变化时也会被渲染,看实例:memo失效案例

const App =()=>{
    ……//原组件写个数据变化和个监听函数就行了
    return (
        ……
        <Child2 m={m} onClickM={onClickM}>
    )
}

const Child = (props)=>{
    console.log('我是Child组件')
    return (
        <div>
            <p>m:{props.m}</p>
            <button onClick={props.onClickM}>m+1</button>
        </div>
    )
}

const Child2 = React.memo(Child)

为什么会触发组件更新呢?函数是个对象,虽然m和setM的值没变就没变,而函数是个对象,地址变了就是变了。而每次App组件重新渲染的时候,都会生成新的函数地址,也就是新的函数,从而导致Child也被莫名奇妙的更新。

而useMemo可以实现函数的重用

useMemo的使用

useMemo不是写在组件上,而是写在你希望缓存的函数上。

React.useMemo(()=>value,[m])
  • 第一个参数是一个函数,()=>value,如果value是一个函数,就要写成
const onClickM = React.useMemo(()=>(x)=>setM(x+1),[m])
  • 第二个参数是它的依赖[m,n],只有依赖变化,才会重新计算出新的value
  • 第一个参数中的()=>value,()=>非常多余,于是有了语法糖,useCallback
useCallback(x=>console.log(x),[n])
//等价于
useMemo(()=>x=>console.log(x),[n])

useMemo实现函数重用案例

useCallback实现函数重用案例

八、useRef

作用:

如果你希望有一个值在render的时候保持不变,可以使用useRef

  • 初始化数值
const count = React.useRef(0)
  • 读值和改值
count.current += 1
console.log(count.cuurent)

useRef使用案例

为什么读值和改值需要用current

因为 React.useRef(0)相当于: React.useRef({current:0})

如果不使用current会直接修改对象的地址,如果使用current就会修改对象的值,不会改变原对象的地址。

React中的数据处理函数

  • useState/useReducer 每次渲染App(),n都会发生改变
  • useMemo/useCallback [m]依赖对象变化的时候,才会发生改变
  • useRef 永远不变

所以,useRef不会去刷新UI,如果需要刷新UI,需要你手动去设置,例如使用us eState中的set

九、forwardRef

forwardRed相当于是useRef功能的扩展,当props传递参数的时候,是无法传递ref的,forwardRef就是来弥补useRef的这个功能缺陷的。

forwardRef解决ref传送问题的示例

const Button = React.forwardRef((props,ref)=>{
    console.log(props)
    console.log(ref)
    return (
        <div>
            <button ref = {ref}>{props.children}</button>
        </div>
    )
})

十、useImperativeHandle

这个名字太垃圾了,叫setRef就好多了。useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。可以自定义ref属性。

useImperativeHandle的语法:

useImperativeHandle(ref, createHandle, [deps])

不使用setRef案例

const App =()=>{
    const buttonRef = React.createRef(null)
    return (
    <div className = 'App'>
        <Button ref={buttonRef}>按钮</Button>
        <button className='close' onClick={()=>{
            console.log(buttonRef)
            buttonRef.current.remove()
        }}>x</button>
    </div>
    )
}

const Button = React.forwardRef((props,ref)=>{
    return (
        <div ref={ref}>
            {props.children}
        </div>
    )
})

使用setRef案例

const App =()=>{
    const buttonRef = React.createRef(null)
    return (
    <div className = 'App'>
        <Button ref={buttonRef}>按钮</Button>
        <button className='close' onClick={()=>{
            console.log(buttonRef)
            buttonRef.current.x()
        }}>x</button>
    </div>
    )
}

const Button = React.forwardRef((props,ref)=>{
    const realButton = React.creatRef(null)
    React.useImperativeHandle(ref,()=>{
        return {
            x : ()=>{
                realButton.current.remove()
            },
            realButton:realButton
        }
    })
    return (
        <div ref={realButton}>
            {props.children}
        </div>
    )
})

十一、自定义Hook

封装数据,通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

自定义Hook示例

image.png

十二、Stale Closure

Stale Closure 过时闭包 函数引用的变量是之前的,而不是即时的。