xiaochengzi6 / Blog

个人博客
GNU Lesser General Public License v2.1
0 stars 0 forks source link

React Hook #13

Open xiaochengzi6 opened 2 years ago

xiaochengzi6 commented 2 years ago

约束条件:

1、只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用 2、只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用

一、useState

useState出现,使得react无状态组件能够像有状态组件一样,可以拥有自己state,useState的参数可以是一个具体的值,也可以是一个函数用于判断复杂的逻辑,函数返回作为初始值,usestate 返回一个数组,数组第一项用于读取此时的state值 ,第二项为派发数据更新,组件渲染的函数,函数的参数即是需要更新的值。

const App = () => {
    const [count, setCount] = useState(0)
    return (
        <div> {count} </div>
    )
}

useState派发更新函数的执行,就会让整个function组件从头到尾执行一次

class 组件通过一个实例化对象后往后的每一次更新只用调用 render 函数就行。而函数组件是每一次都会重新执行。

二、useEffect

如果不给useEffect执行加入限定条件,函数组件每一次更新都会触发effect ,那么也就说明每一次state更新,或是props的更新都会触发useEffect执行,

合理的用于useEffect就要给effect加入限定执行的条件,也就是useEffect的第二个参数,这里说是限定条件,也可以说是上一次useeffect更新收集的某些记录数据变化的记忆,在新的一轮更新,useeffect会拿出之前的记忆值和当前值做对比,如果发生了变化就执行新的一轮useEffect的副作用函数,useEffect第二个参数是一个数组,用来收集多个限制条件 。

执行时机:给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。

const App = () => {

    useEffect(()=>{
        console.log('1')
    },[])
    return (
        <div> {count} </div>
    )
}

useLayoutEffect

功能相似但是它和useEffect 的不同之处在于执行顺序

useEffect 执行顺序 组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执行useEffect回调 。

useLayoutEffect 执行顺序 组件更新挂载完成 -> 执行useLayoutEffect回调-> 浏览器dom 绘制完成

前者可能会造成页面的闪动 后者可能会造成页面的卡顿

三、(一)useRef

useRef 每次渲染返回同一个 Ref 的对象 并且它发生变化时 useRef 也不会做出什么反应 解决方法:回调Ref

在这里我了解到的用处有 1. 访问DOM元素 2.缓存数据。

先来了解一下 ref 在类组件中可以通过创建 ref 和在 dom 节点获取的方式

import React from 'react'
class App extends React.Componrnt {
    constructor(props) {
        super(props)
        // 创建 ref
        this.divRef = React.createRef();
    }
    render() {
        return(
            // 指定 ref
            <div ref={this.divRef}></div>
        )
    }
}
// 回调 Ref 的用法
class App extends React.Componrnt {
    constructor(props) {
        super(props)
        this.textInput = null;
        this.setTextInputRef = element => {
            this.textInput = element;
        };
    }

    render() {
        return(
            // 指定 ref
            <div ref={this.setTextInputRef}></div>
        )
    }
}

在默认的情况下函数组件内部不能使用 ref 属性 因为他们没有任何实例。你可以采用 forwardRef 的来找到 ref

const Func  = React.forwardRef((props,ref) => (
    <button ref={ref} >{props}</button>
))

1.当然在函数组件中使用 useRef hook 来获取函数组件中的 dom 节点或者 class 组件。

import React, {useRef} from 'react';
const FuncButton = () => {
    const buttonRef = useRef(null)
    return(
        <button ref={buttonRef}></button>
    )
}
  1. 高阶用法 缓存数据

当然useRef还有一个很重要的作用就是缓存数据,我们知道usestate ,useReducer 是可以保存当前的数据源的,但是如果它们更新数据源的函数执行必定会带来整个组件从新执行到渲染,如果在函数组件内部声明变量,则下一次更新也会重置,如果我们想要悄悄的保存数据,而又不想触发函数的更新,那么useRef是一个很棒的选择。

const func = (props) => {
    // 这里举得例子不好,但要大致明白 在组件中要使用 setState保存数据会使组件更新,使用 ref 来保存就不会出现这个问题。
    const Ref = useRef(props)
    ...
    return(

    )
}

(二)、useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 [forwardRef] 一起使用:

import React, { useRef, forwardRef, useImperativeHandle, useEffect } from "react";

const App = () => {
  // 这里开始创建 inputRef 接收实例
  const inputRef = useRef();

  return (
    <div>
      {/*让实例传到 inputRef */} 
      {/*父组件收到的不是 input 这个dom节点而是通过 useImperativeHandle 暴漏的实例 */} 
      <FancyInput ref={inputRef} />
      <button onClick={()=>{inputRef.current.focus()}} >q</button>
    </div>
  );
};
// inputRef ===> ref
function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    //这里就是 暴露给父组件的实例值 一个函数通过这个闭包函数访问到当前 inputRef 调用其focus。
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef}  />;
}
FancyInput = forwardRef(FancyInput);
export default App;

四、useCallback

在第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行。

import React , {useState,useRef,memo,useCallback,useEffect} from 'react';

const App = () => {
    const [text, updateText] = useState('');
    const textRef = useRef(text);

    // useCallback又依赖了textRef的变化,因此可以获取到最新的数据
    const handleSubmit = useCallback(() => {
        console.log('当前输入框的值:', textRef.current);
    }, [textRef])

    // 当text的值变化的时候就会给textRef的current重新赋值
    useEffect(() => {
      console.log(textRef)
        textRef.current = text;
    }, [text]);

    return(
        console.log('父组件----render'),
        <div>
            我是父组件
            <input type="text" value={text} onChange={(e) => updateText(e.target.value)}/>
            <Child onAdd={handleSubmit}/>
        </div>
    )

};
const Child = memo((props) => {
  return (
      console.log('子组件----render'),
      <button onClick={props.onAdd}>点击按钮获取值</button>
  )
})
export default App;

这里看以清楚的看出来 useCallback 返回的函数 且这个函数依赖与 textRef 这个ref 它绑定的使 input 中的文本节点

{
    current: ''
}

text的变化就是 current 的变化 无论current 的值怎么变化 ref 没有任何变动 ( {current: value} 这个对象还是那个对象)所以子组件不会更新。

五、useMemo

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

1.案例一:如果组件中父组件更新那么会势必带动其内的子组件的更新。(组件件没有传入任何值)

使用React.memo()后 如果 props 没有发生改变,则不会重新渲染此组件。

2.案例二:如果父组件往子组件传入值 且子组件使用 React.memo() 包裹子组件并没有去使用父组件传入的值 当父组件的更新也会带动子组件的更新

useMemo 缓存的是属性。当缓存依赖没改变,去获取曾经的缓存。 将 remo 和 useMemo 结合使用来去避免案例二的问题出现。

注意:useMemo 和 useCallback 很相似都能配合 React.memo() 来做到优化 前者缓存属性后者缓存函数。但前者能做到更多。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

简单来说: useCallback 就是处理父组件重复无用的调用子组件的。 useMemo 就是处理重复无用的调用属性的。

import React, {useState, memo} from 'react';
const App = () => {
    const [count1, setCount1] = useState(0);
    const [count, setCount] = useState(0);
    const userInfrom = {
        name: 'perter'
    }
    const handleClick = () => {
        setCount(count+1);
    } 
    return (
        <div>
            <button onClick={handleClick} >点击</button>
            <span> {count} </span>
            <Imafe userInfrom={userInfrom} />
        </div>

    )
}
const Image = memo() => {
    return (
        <div>2</div>
    )
}

这个时候就算Image组件不使用 userInfrom 父组件一旦更新子组件也会更新。 因为父组件点击后调用了一次 setCount 就会使状态更新从而父组件重新运行一次后导致 子组件的 userInfrom 的重新创建 就算子组件被 React.memo() 包裹也不行

解决办法就是使用 useMemo hook 来缓存属性。

const userInfo = useMemo(()=>{
    return {
        name: 'perter'
    }
},[count1])
// 这里的第二个参数使表示依赖性 当他改变的时候数据就会变动。

此时子组件就不会随便更新了 但要注意 useMemo 要和React.memo 搭配使用、

六、useContext

我们可以使用useContext ,来获取父级组件传递过来的context值,这个当前值就是最近的父级组件 Provider 设置的value值,useContext参数一般是由 createContext 方式引入 ,也可以父级上下文context传递 ( 参数为context )。useContext 可以代替 context.Consumer 来获取Provider中保存的value值

/* 用useContext方式 */
const DemoContext = ()=> {
    const value = useContext(Context)
    /* my name is alien */
    return <div> my name is { value.name }</div>
}

/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
    return <Context.Consumer>
         {/*  my name is alien  */}
        { (value)=> <div> my name is { value.name }</div> }
    </Context.Consumer>
}

export default ()=>{
    return <div>
        <Context.Provider value={{ name:'alien' , age:18 }} >
            <DemoContext />
            <DemoContext1 />
        </Context.Provider>
    </div>
}

七、useReducer

它的第一个参数可以看作是一个 reducer 其中接收 state,action 作为参数。第二个参数为初始值。 useReducer 返回一个数组。数组第一个值为数据 初始值在第二个参数给出。第二个值为dispatch函数用来传递action 从而来改变数据。

import React, { useReducer } from "react";

const App = () => {
  const [number, dispatchNumber] = useReducer((state, action) => {
    const { type } = action;
    switch (type) {
      case "a":
        return state + 1;
      case "b":
        return state + 2;
      case "c":
        return state + 3;
      case "d":
        return state + 5;
      case "e":
        return (state = 0);
    }
    return state;
  }, 0);

  return (
    <div>
      <button onClick={() => dispatchNumber({ type: "a" })}>a{number}</button>
      <button onClick={() => dispatchNumber({ type: "b" })}>b{number}</button>
      <button onClick={() => dispatchNumber({ type: "c" })}>c{number}</button>
      <button onClick={() => dispatchNumber({ type: "d" })}>d{number}</button>
      <button onClick={() => dispatchNumber({ type: "e" })}>d{number}</button>
    </div>
  );
};

八、自定义hook

自定义 hook 名字必须以 use 开头函数内部调用了其他的 hook 它并不服用 state 本身 它的存在是复用其状态的逻辑。每次调用 hook 都是一次全新的 state 。不会共享状态。

xiaochengzi6 commented 2 years ago

项目优化: 1、父组件嵌套子组件 当父组件更新时便会重新渲染整个组件这个过程是连带着子组件进行的 解决方法:[ React.mome ] 使用 React.memo 使其在组件准备更新时进行 props 的对比工作(浅层比较)

2、父组件嵌套子组件,父组件往子组件传值 但子组件没有使用 当父组件更新时就会带着子组件跟新 解决方法:[ React.memo + useMemo ] 使用 useMemo 用来包裹父组件往子组件传入的属性 并且子组件使用 React.memo 包裹

当然可能有更复杂的情况需要使用 React.memo() 的第二参数来进行深度对比从而减少子组件的渲染

合理的渲染方式以及代码结构才能带来整体的性能优化 随意使用 hook 更是一种灾难

xiaochengzi6 commented 1 year ago

考虑到闭包的影响

useEffect(() => {
    // 回调函数只运行一次,这里的 count 只记住初次渲染的那个值
    // 所以导致每一次的 setInterval 中用到的永远都不会变!
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
}, []);

由于 useEffect的依赖参数为空数组所以创建了一个匿名函数却不会修改它,这个时候就出现了闭包陷进,该函数始终记录整个 count 变量的值为 0 所以 count 只会到 1 就不会再发生变化

或者参考 [Dan] (https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/)的示例

const callbackRef = useRef(null)

const callback = () => setCount(count + 1)

useEffect(() => {
  callbackRef.current = callback
})

useEffect(() => {
  function tick() {
    savedCallback.current();
  }

  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
}, []);
xiaochengzi6 commented 1 year ago

React.memo(componet, arePropsEqual) 会返回一个新组件,该组件和原来组件相同, 被 react.memo 包裹并不会再父组件跟新的时候更新,它通过第二个参数进行对比,通常并不需要提供该函数,一般默认就有,使用 Object.is相同的算法