6.React Hook 概述(开发中遇到的问题与解决)

hooks 函数是做什么用的? 让静态组件动态化

React Hook 概述

什么是 Hook:

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook

解决的问题:

在组件之间复用状态逻辑很难,可能要用到 render、props 和高阶组件,React 需要为共享状态逻辑提供更好的原生途径,Hook 使你在无需修改组件结构的情况下复用状态逻辑
复杂组件变得难以理解,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
难以理解的 class,包括难以捉摸的this

注意事项

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

Hook 知识点:

useState √ | useEffect √ | useContext | useReducer | useCallback √ | useMemo √ | useRef √ | useHistory √

(打√的为我用过的,也是比较常用的,其它有机会再用)

useState √

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

const [state, setState] = useState(initialState);

  • useState:主要是为了给函数添加状态的。最好(必须)在函数的最外层申明使用。
  • initialState:是 state 的初始值,不限制数据类型,不写默认 undefined。
  • setState:是改变 state 的方法,类似于原来的 setState({state:false }),区别是没有钩子函数,也就是不能这样 --> this.setState({ state: false }, () => { console.log(this.state) }) 操作。 setState 函数用于更新 state,它接收一个新的 state 值并将组件的一次重新渲染加入队列。

setState 如何更新状态

setState(newState) // newState 可以是除了函数之外的任何值
console.log(state) // 有可能是异步执行的,,不能直接查看更新后的值

如何拿到 setState 更新后的最新值?

  1. 可以在 useEffect 里面添加对应的依赖,拿到更新后的值,useEffect(() => { console.log(state) }, [state]);
  2. 用 useRef 代替 useState 来定义一个state,当人这个 state 不能用在 DOM 中,因为 useRef.current 修改后也不会触发组件的更新。
  3. 用一个函数,包裹 setState,参数为最新的状态,需要改变状态的时候,调用函数就行。const setStateFn = (state) => { setState(state); console.log(state) }

使用 setState 如何拿到更新前的值

setState((n) => {
  console.log("当前的值:", n);
  // return 返回的值是本次需要更新的值
  return 3
})

知道这个有什么用?

在函数式组件中,使用定时器,因为产生了闭包的原因,无法拿到更新前的值,就可以使用这种方式。

  useEffect(() => {
    // 为何 hooks 组件中 定时器 里面没有拿到更新后的值,这里是因为闭包的原因,只能拿到 初始 值
    setTimeout(() => {
      setNum((n) => {
        console.log(22222, n); // 拿到 初始 值
        return 3
      })
      setNum((n) => {
        console.log(33333, n); // 3,拿到更新前的值是上一次更新的返回值
        return 4
      })
    }, 3000)
  }, []);

setState 什么时候同步,什么时候异步

强制 更新 触发render const [, setState] = useState(0); <div onClick={() => { setState({}) }}><div>

useEffect √

useEffect的作用是依赖变化的时候,执行函数(第一个参数),其中第二个参数为依赖。
第二个参数的作用:
虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。
不写“依赖”,可能会导致没必要的刷新,甚至无限刷新。
写上第二个参数,effect 会监测“依赖”是否变化,当“依赖”变化时才会刷新。
若依赖是一个空数组,effect 会判定依赖没有更新,所以只会执行一次。

effect 第一次被执行的生命周期:第一次 render(创建 DOM),执行 effect ,第二次 render,...

useEffect(() => {
  // do something
  return () => {
    // Clean up side effects
  };
}, []);

的第一个参数,可以返回一个函数,这个函数在组件卸载时执行一次用来清理副效应(订阅、定时器等),
实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

let timer = null;
function App() {
  const [width,setWidth] = useState(window.innerWidth);
  const [renderFatherState, setRenderFatherState] = useState(0);

  useEffect(()=>{
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize',handleResize);
    timer = setInterval(() => {
      setRenderFatherState(renderFatherState + 1);
    }, 1000)
    return ()=>{ // 清除订阅、定时器等副作用(这里有个错误:不相关 的副作用,不应该写在一起,正确写法看下文)
      window.removeEventListener('resize',handleResize);
      clearInterval(timer);
    }
  }, []);

  return (
    <div>
      <p>{width}</p>
      <p>{renderFatherState}</p>
    </div>
  )
};

注意:如果需要清除多个 不相关 的副作用,不应该写在一起。正确的写法是将它们分开写成两个useEffect()。
所以上述代码正确的写法:

let timer = null;
function App() {
  const [width,setWidth] = useState(window.innerWidth);
  const [renderFatherState, setRenderFatherState] = useState(0);

  useEffect(()=>{
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize',handleResize);
    return ()=>{
      window.removeEventListener('resize',handleResize);
    }
  }, []);

  useEffect(()=>{
    timer = setInterval(() => {
      setRenderFatherState(renderFatherState + 1);
    }, 1000)
    return ()=>{
      clearInterval(timer);
    }
  }, []);

  return (
    <div>
      <p>{width}</p>
      <p>{renderFatherState}</p>
    </div>
  )
};

useContext √

之前我们使用context (上下文)来解决多层嵌套传props,分三步

  1. createContext创建Context
  2. 使用Context.Provider组件 中的 value 属性提供数据
  3. Context.Provider的所有后代(是后代,不仅是子代)组件,都可以通过Context.Consumer使用数据数据
    例子一(react context 的使用):详细举例
    例子二(react hook,useContext 的使用):
      如果需要在组件之间共享状态(比如两个有共同父级的子组件之间),可以使用 useContext()。
      一层的数据传递,感觉没有必用 useContext,如果是多层嵌套的话,父传子的方式就显得很不好了,此时应该用到它。
    下面的例子:Father 中引用 Son,Son 中引用 Grandson,跳过 Son 组件在 Grandson 中直接使用 Father 中的上下文。
    提供状态的父组件:
import React, { createContext} from "react";
import Son from "./components/levelOne";

// 在函数外部传递当前上下文,导出
export const TestContext = createContext("默认值");

const Father = (props) => {

  // 不要在函数内部使用 createContext()
  // const FatherContext = createContext() 

  return (
    <div>
      <span>父级</span>
      <TestContext.Provider value={{ b: 2 }}>
        <Son/>
      </TestContext.Provider>
    </div >
  )
};

export default Father;

需要获取状态的子组件:

import React, { useContext } from "react";
import styles from "./index.less";

// 重点: 从父组件那里引入我们一开始导出的 TestContext 
// 就是父组件的这条代码: export const TestContext = createContext()
import { TestContext } from "../../index";

const Grandson = (props) => {
  // 通过上下文获取到的父组件传递过来的数据.
  // 也可以传递事件,子组件可以通过传递过来的数据触发父组件的事件
  const obj = useContext(TestContext);
  console.log(TestContext, obj);

  return (
    <div>
      <span>第二层子组件</span>
      <br />
      <span>从父级拿到的数据{obj.b}</span>
    </div>
  )
};

export default Grandson;

useReducer √

useRedecer 和 useState很像,当我们想要实现更加复杂的修改值的操作,可以使用 reducer。

工作流程和 redux 一样:先 dispatch 一个 action dispatch({ type: 'newState', newState: "更新后的值"}),reducer 根据 type,修改对应的 state。容器捕捉到状态的改变,触发 render() ,最后页面更新。

useRedecer 和 redux 区别:
1.useReducer 是 useState 的代替方案,用于 state 的复杂变化
2.useReducer 是单个组件的状态管理,组件间通讯还是需要 props
3.redux 是全局的状态管理,多组件共享数据

因此,useReducer 本质上只是 useState 的升级版,并不能代替 redux

可以使用 useReducer + useContext 模拟 redux 实现公共状态的管理,甚至是页面级别的 redux

使用举例:

import React, { useReducer } from 'react'

// 定义初始值
const initialSatate = {
  newState: "初始状态",
  count: 0,
};

// 定义修改规则
const reducer = (state, action) => {
  switch(action.type) {
    case 'newState': // 通常会这么用,case 到新状态时,就该原来得状态。其它不变
      return { ...state, newState: action.newState};
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      return false;
  }
};

const HooksT = () => {
  // 创建一个 reducer ,很像useState
  const [state, dispatch] = useReducer(reducer, initialSatate)
  return (
    <>
      count: {state.count}
      <button onClick={() => {dispatch({type: 'increment'})}}>increment</button>
      <button onClick={() => {dispatch({type: 'decrement'})}}>decrement</button>
    </>
  )
};
export default HooksT;

让我们来回忆一下 使用redux使用reducer

// 1.首先创建一个store index.store.js
export default function configStore(){
    const store = createStore(rootReducer,applyMiddleware(...middlewares))
    return store
}

// 2.引入store app.js
  render() {
    return (
      <Provider store={store}>
        <Index />
      </Provider>
    )
  }

// 3.定义action和创建reducder index.action.js index.reducer.js
export const ADD = 'ADD'
export const DELETE = 'DELETE'
function todos(state = INITAL_STATE, action) {
  switch action.type{
    case ADD:{...}
    case DELETE:{...}
  }
}

// 4.页面中使用reducer  component.js
export default connect(mapStateToProps, mapDispatchToProps)(Component);

太复杂了有没有,(使用dva可以简化写法)

不过 useReducer 不支持共享数据,可以结合 useContext 实现数据共享

import React, { useContext, createContext, useReducer } from 'react';
import Child from './Child.jsx';

// 定义初始值
const initialSatate = {
  newState: "初始状态",
};

// 定义修改规则
const reducer = (state, action) => {
  switch(action.type) {
    case 'newState': // 通常会这么用,case 到新状态时,就该原来得状态。其它不变
      return { ...state, newState: action.newState};
    default:
      return false;
  }
};

// 创建一个上下文 context
export const TestContext = createContext("默认值");

function Father(props) {
  // 创建一个 reducer ,很像useState
  const [state, dispatch] = useReducer(reducer, initialSatate);

  return (
    <TestContext.Provider value={{state, dispatch}}>
      父组件
      <Child></Child>
    </TestContext.Provider>
  );
}

export default Father;

// 下面是 孙子组件,在 Child 组件引用 Grandson 组件
import React, { useContext } from 'react';
import { TestContext } from "./index";

function Grandson(props) {
  const {state, dispatch} = useContext(TestContext);
  console.log("孙子组件", state);

  return (
    <div>
      孙子组件
      <button onClick={() => {
        dispatch({type: "newState", newState: "更改后的值"})
      }}>修改状态</button>
    </div>
  );
};

export default Grandson;

useCallback √

useCallBack
返回:一个缓存的回调函数。
参数:需要缓存的函数,依赖项。
使用场景:父组件更新时,通过props传递给子组件的函数也会重新创建,然后这个时候使用 useCallBack 就可以缓存函数不使它重新创建
使用及场景举例:

// 父组件
import React, { useState, useCallback } from 'react';
import styles from './index.less';
import Child from './Child.jsx';

function Father() {
  const [count, setCount] = useState(0);

  // 不缓存,每次 count 更新时都会重新创建
  const addFn = () => {
    setCount(count + 1);
  };

  // 使用 useCallBack 缓存
  const handleCountAddByCallBack = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  return (
    <div className={styles.Test_box}>
      <p>父组件</p>
      <Child add={addFn} addByCallBack={handleCountAddByCallBack}></Child>
    </div>
  );
}

export default Father;

// 子组件
import React, { useState } from 'react';
import styles from './Child.less';
import { Button } from 'antd';

function Child(props) {
  const { add, addByCallBack } = props;

  // 没有缓存,由于每次都创建,memo 认为两次地址都不同,属于不同的函数,所以会触发 useEffect
  useEffect(() => {
    console.log("Child----addFnUpdate");
  }, [add]);

  // 有缓存,memo 判定两次地址都相同,所以不触发 useEffect
  useEffect(() => {
    console.log("Child----addByCallBackFnUpdate");
  }, [addByCallBack]);

  return (
    <div className={styles.Child_box}>
      <Button onClick={() => add()}>点击 + 1</Button>
      <Button onClick={() => addByCallBack()}>点击 + 1</Button>
    </div>
  );
}

export default Child;

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
注意:
依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。

useMemo √

useMemo 是什么呢,它跟 memo 有关系吗, memo 的具体内容可以查看 React 中性能优化、 memo、PureComponent、shouldComponentUpdate 的使用,说白了 memo 就是函数组件的 PureComponent,用来做性能优化的手段,useMemo 也是,useMemo 在我的印象中和 Vue 的 computed 计算属性类似,都是根据依赖的值计算出结果,当依赖的值未发生改变的时候,不触发状态改变。

PureComponent,

它其实就是在帮我们做这样一件事:自动的帮我们编写 shouldComponentUpdate 方法, 避免我们为每个组件都编写一次的麻烦。我们只需要这样, 就可以一步到位

import React, { PureComponent } from 'react'

// 使用 PureComponent ,子组件 不再需要重复编写 shouldComponentUpdate 生命周期,来减少不必要的渲染。
class child extends PureComponent {
  // ...
}

memo

memo 可以提高性能,React.memo 认定两次地址是相同就可以避免子组件冗余的更新
memo 和 useMemo 具体如何使用呢,看下面例子:

import React, { memo, useMemo } from "react";
function App() {
  const [count, setCount] = useState(0);
  const add = useMemo(() => {
    return count + 1
  }, [count]);
  return (
    <div>
      点击次数: { count }
      <br />
      次数加一: { add }
      <button onClick={() => { setCount(count + 1) }}>点我</button>
    </div>
  )
};
export default memo(App) // 导出的时候包一下

useMemo

返回:一个缓存的值
参数:需要缓存的值(也可以是个计算然后再返回值的函数) ,依赖项。
使用场景:组件更新时,一些计算量很大的值也有可能被重新计算,这个时候就可以使用 useMemo 直接使用上一次缓存的值
使用及场景举例:

let timer = null;
function App() {
  const [renderFatherState, setRenderFatherState] = useState(0);
  const [count, setCount] = useState(0);

  useEffect(()=>{
    timer = setInterval(() => {
      setRenderFatherState(renderFatherState + 1);
    }, 1000);
    return ()=>{
      clearInterval(timer);
    };
  }, []);

  const add = useMemo(() => { // useMemo 返回一个缓存的值,通过依赖项来更新缓存。
    console.log("只会根据 count 状态,来触发函数")
    return count + 1;
  }, [count]);
  
  const addM = () => {
    console.log("不相干的状态的改变,比如:renderFatherState,都会触发 addM函数,然而这是不必要得消耗")
    return count + 1;
  });

  return (
    <div>
      <p>{add}</p>
      <p>{addM()}</p>
    </div>
  )
};

useRef √、

特点:

  • let ref = useRef(initial) 返回一个持久化的可变值 {current:initial}
    • 持久化:返回的 ref 对象在组件的整个生命周期内保持不变
    • 可变值:current 可以是任何类型的值
  • 当 ref 对象内容发生变化时,useRef 并不会通知你,也不会引发组件重新渲(只是一个记录值,并不会监听数据改变和重新渲染操作等)

作用(使用场景)

  • 获取元素【和类组件中的 ref 作用一样】
  • 获取对象【比如:form表单】
  • 获取子组件的属性方法【需要在组件中使用 useImperativeHandle, forwardRef】
  • 清除定时器【1.可以模仿类组件的方式,在 useEffect 中清除定时器。 2.使用 useRef】

例子:获取元素

import React, { useRef, useEffect } from 'react'
function A(){
  const inputR = useRef();

  useEffect(()=>{
      //页面渲染完成的时候执行
      inputR.current.focus()
  },[]);

  render() {
    return (
      <div>
        <input type="text" ref={inputR} />
        定义属性
      </div>
    );
  };
};

例子:获取子组件的属性方法

// 父组件
import React, { useState, useEffect, useRef } from "react";
import Child from './Child';

function Father() {
  const childRef = useRef(null)
  useEffect(() => {
    console.log(childRef.current);
  }, []);
  return (
    <div onClick={() => childRef.current.showName()}>
      <Child ref={childRef}></Child>
    </div>
  );
};
export default Father;

// 子组件
import React, { useState, useImperativeHandle, forwardRef } from "react";
/*
useImperativeHandle 和 forwardRef 配合一起使用

useImperativeHandle(ref, createHandle, [deps]);
1.ref:定义current 对象的 ref;
2.createHandle:一个函数,返回值时一个对象,即这个 ref 的 current;
3.对象 [deps]:即依赖列表,当监听的依赖发生变化,useImperativeHandle 才会重新将子组件的示例属性输出到父组件
4.ref 的 current 属性上,如果为空数组,则不会重新输出

在使用 useImperativeHandle 之前,要清楚 React 关于 ref 转发(透传)这个知识点,主要是使用 React.forwardRef 方法实现的,该方法返回一个组件,参数为函数(并不是函数组件),函数的第一个参数为父组件传递的 props,第二个给父组件传递的 ref ,其目的就是希望可以在封装组件时,外层组件可以通过ref直接控制内层组件或元素的行为。

正常情况下 ref 是不能挂载到函数组件上的,因为函数组件没有实例。React 官方为我们提供了 useImperativeHandle 一个类似实例的东西,帮助我们通过 useImperativeHandle 的第二个参数,把返回的对象的内容挂载到 父组件的 ref.current 上。

forwardRef 会创建一个 React 组件,这个组件能够将其接收的 ref 属性转发到其 组件树下的另一个组件中。
*/
function Child(props, ref) {
  const [text, setText] = useState("我是子级");
  useImperativeHandle(ref, () => ({
    text,
    setText,
    showName
  }))
  const showName = () => {
    alert("拿到子组件的方法和属性")
  };
  return (
    <div>
      {text}
    </div>
  );
};
export default forwardRef(Child);

例子:清除定时器

import React, { useState, useEffect, useRef } from "react";
// 模仿类组件的写法,在组件外面,即全局定义一个变量,接受定时器的返回,在组件卸载的时候,解除定时器
// let timer = null; 
function Test() {
  const [time, setTime] = useState(0);
  const timerRef = useRef();

  useEffect(() => {
    start();
  }, []);

  useEffect(() => {
    return () => {
      if (time > 10) stop();
    };
  }, [time]);

  const start = () => {
    timerRef.current = setInterval(() => {
      setTime((n) => { // 这里的 n 和 对应 effect 中的值是一致的,并且早于 effect
        return n + 1
      });
    }, 1000);
  };

  const stop = () => {
    timerRef.current && clearInterval(timerRef.current);
  };

  return (
    <div onClick={() => stop()}>
      {time}
    </div>
  );
};
export default Test;

useHistory √

react.js-Hooks 路由跳转
useHistory 钩子允许您访问可能用于导航的历史实例。

import { useHistory } from "react-router-dom";

function HomeButton() {
  let history = useHistory();

  function handleClick() {
    history.push("/home");
  }

  return (
    <button type="button" onClick={handleClick}>
      Go home
    </button>
  );
};

开发中遇到的问题与解决

react hooks 中,使用定时器形成闭包,而无法获取更新后的 state,解决方案:

  1. 如果不需要触发 render,此时可以使用 useRef 来定义这个变量,let ref = useRef(0); console.log(ref.curren) // 0
  2. 如果需要触发 render,不得不实用 useState 时,可以这样写:
cosnt [state, setState] = useState(0);

setState((n) =>{console.log(n)}) // n为最新值。
posted @ 2019-11-27 18:58  真的想不出来  阅读(3197)  评论(0编辑  收藏  举报