Hook与Redux的基础使用-更新中

464 阅读9分钟

Hook与Redux

1.React Hook

1.1React Hook是什么

Hook是什么

React Hooks是React v16.8 中的新增的钩子API,它为函数组件提供状态、生命周期等原本 class 组件中提供的 React 功能,可以理解为通过 Hooks 为函数组件钩入 class 组件的特性,需要注意的是Hook 只能在函数组件中使用。虽然有了 Hooks,但 React 官方并没有计划从 React 库中移除 class

为什么要有Hook

两个角度:1 组件的状态逻辑复用 2 class 组件自身的问题

  1. 组件的状态逻辑复用:

    • 在 Hooks 之前,组件的状态逻辑复用经历了:mixins(混入)、HOCs(高阶组件)、render-props 等模式
    • (早已废弃)mixins 的问题:1 数据来源不清晰 2 命名冲突
    • HOCs、render-props 的问题:重构组件结构,导致组件形成 JSX 嵌套地狱问题
  2. class 组件自身的问题:

    • 选择:函数组件和 class 组件之间的区别以及使用哪种组件更合适
    • 需要理解 class 中的 this 是如何工作的
    • 相互关联且需要对照修改的代码被拆分到不同生命周期函数中
  • 相比于函数组件来说,不利于代码压缩和优化,也不利于 TS 的类型推导

总结:正是由于 React 原来存在的这些问题,才有了 Hooks 来解决这些问题

学习Hooks之前,先补充一个数组解构简化的知识点

相当于创建了两个变量(可以是任意的变量名称)分别获取到对应索引的数组元素

const arr = ['aaa', 'bbb']
const [a, b] = arr
// a => arr[0]
// b => arr[1]

const [state, setState] = arr

1.2useState 声明状态

当你想要在函数组件中,使用组件状态时,useState就派上用场了,它可以为函数组件提供状态(state)

import React, { Component, useState } from 'react';

export default function App() {
  // 调用useState(状态默认值),该方法会返回一个数组,第一个元素为声明的状态,第二个为更新状态的函数
  //更新状态的方法:调用更新函数传入新值,注意,不可直接修改原状态,要使用新值覆盖旧值
  // useState可以声明任意的数据类型
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('hello React');
  const [list, setList] = useState([1, 2, 3]);
  const [person, setPerson] = useState({ name: '张三', age: 888 });
  const [isShow, setIsShow] = useState(true);
  return (
    <div>
      <h2>
        userstate里的数据:{count}--{msg}
      </h2>
      <button onClick={() => setCount(count + 1)}>数字+1</button>
      <button onClick={() => setMsg(msg + '~')}>文字+ ~</button>
      <br />
      <button onClick={() => setIsShow(!isShow)}>点击切换显示隐藏 ~</button>
      {isShow && <h3>用于切换显示隐藏的内容</h3>}
      {list.map((item) => (
        <h4> 数组map后的列表标签---{item}</h4>
      ))}
      <h2>对象里的属性:---{person.name}</h2>
    </div>
  );
}

1.3 受控组件-Hook

在react中,受控组件时我们时常需要用到的,那么在hook语法下,受控组件又是如何实现的呢?

import React, { useState } from 'react';

export default function App() {
  // 受控组件-hook版
  // 1.useState()声明状态
  const [iptVal, setIptVal] = useState('');
  const [checkbox, setcheckbox] = useState(false);
  return (
    <div>
      {/* 2.将状态绑在表单组件上,配合onChange 事件和事件对象获取表单元素实时的值 */}
      文本框:
      <input type="text" value={iptVal} onChange={(e) => setIptVal(e.target.value)} />
      <br />
      复选框:
      <input type="checkbox" checked={checkbox} onChange={(e) => setcheckbox(e.target.checked)}
      />
    </div>
  );
}

1.4useEffect Hook

在react官网中(zh-hans.reactjs.org/docs/hooks-…

// 不带副作用的情况:
// 该函数的(主)作用:计算两个数的和
function add(a, b) {
  return a + b
}

// 带副作用的情况:
let c = 1
function add(a, b) {
  // 因为此处修改函数外部的变量值,而这一点不是该函数的主作用,因此,就是:side effect(副作用)
  c = 2
  return a + b
}

// 带副作用的情况:
function add(a, b) {
  // 因为 console.log 会导致控制台打印内容,所以,也是对外部产生影响,所以,也是:副作用
  console.log(a)
  return a + b
}

// 没有副作用:
function getName(obj) {   // 纯函数:固定输入、固定输出
  return obj.name 
}

// 有副作用:
function getName(obj) {
  // 此处直接修改了参数的值,也是一个副作用
  obj.name = '大飞哥'
  return obj.name
}
const o = { name: '小马哥' }
fn(o)

在了解什么是函数的副作用后,我们再来看一下useEffect这个钩子函数时如何模拟挂载后 / 更新后 / 及卸载的生命周期的

挂载与更新

import React, { useEffect, useState } from 'react';

export default function App() {
  const [isShow, setIsShow] = useState(true);
  return (
    <div>
      <button onClick={() => setIsShow(!isShow)}>点击卸载或显示子组件</button>
      {isShow && <Child></Child>}
    </div>
  );
}

function Child() {
  const [count, setCount] = useState(0);
  const [testNoyilai, setTestNoyilai] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  const handleClickNo = () => {
    setTestNoyilai(testNoyilai + 1);
  };
  /*语法一: useEffect(回调函数,[]) 此时useEffect只会在组件挂载后触发 ,相当于 componentDidMount 函数
   useEffect(() => {
    console.log('只在子组件挂载时触发');
  }, []); */
    
    /*语法二!!!非常不推荐使用(容易出bug): 此时useEffect会在组件挂载后以及数组里的依赖项状态更新后触发 ,相当于 componentDidMount + componentDidUpdate 函数,但是会非常消耗性能
   useEffect(() => {
    console.log('组件内任何useState状态更新了都会触发');
  }); */

  /* 语法三:useEffect(回调函数,[依赖项]) 此时useEffect会在组件挂载后以及数组里的依赖项状态更新后触发,相当于 componentDidMount + componentDidUpdate 函数,非依赖项数组里的状态即使更新了,也不会触发函数*/
  useEffect(() => {
    console.log(
      '子组件挂载或更新都会触发,且拿到依赖性的最新值---->',
      count,
      '非依赖项---->',
      testNoyilai
    );
  }, [count]);
  return (
    <div>
      <h2>子组件内容---{count}</h2>
      <h2>子组件非依赖项内容---{testNoyilai}</h2>
      <button onClick={handleClick}>点击数字+1</button>
      <button onClick={handleClickNo}>点击非依赖性数字+1</button>
    </div>
  );
}

挂载与卸载

import React, { useEffect, useState } from 'react';

export default function App() {
  const [isShow, setIsShow] = useState(true);
  return (
    <div>
      <button onClick={() => setIsShow(!isShow)}>卸载或显示组件</button>
      {isShow && <Child />}
    </div>
  );
}
function Child() {
  const resizeFn = () => {
    console.log('窗口大小变化了');
  };
  /* 卸载函数的语法:写在useEffect(回调函数返回的函数里面,[])
  useEffect(() => {
    return () => {
      //卸载函数代码区域
    };
  }, []);
    */
  /* 合并写法
  useEffect(() => {
    window.addEventListener('resize', resizeFn);
    return () => {
      console.log('在useEffect的回调函数里返回的函数,组件卸载时触发');
      window.removeEventListener('resize', resizeFn);
    };
  }, []); */
  // 分开写法-挂载
  useEffect(() => {
    window.addEventListener('resize', resizeFn);
  }, []);
  // 分开写法-卸载
  useEffect(() => {
    return () => {
      console.log('在useEffect的回调函数里返回的函数,组件卸载时触发');
      window.removeEventListener('resize', resizeFn);
    };
  }, []);

  return <h2>子组件</h2>;
}

在对useEffect()如何模拟生命周期函数有基本认识后,我们还需要做个额外的小扩展

useEffect()中发送数据请求

import React, { useEffect, useState } from 'react';
import request from './utils/request';

export default function App() {
  const [list, setList] = useState([]);、
  
  //将请求函数单独封装成异步处理函数
  const getData = async () => {
    const res = await request.get('/v1_0/channels');
    console.log(res);
    setList(res.data.channels);
  };
  // 不能将async写在这个箭头函数前useEffect(async () => {},推荐请求函数另外封装,
  useEffect(() => {
    getData();

    return () => {};
  }, []);

  return (
    <div>
      <ul>
        {list.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

2.Redux

前言:当我们有多个组件需要共享和使用相同的state时, 可能会变得很复杂,尤其是当这些组件位于应用程序的不同部分时。有时这可以通过 “提升状态”到父组件来解决,但这并不总是有效。这时候我们就需要有一个全局状态管理的工具来实现复杂的状态需求, 而随着该需求应运而生的就是Redux --->react 中最流行的状态管理工具之一

2.1Redux是什么

概念:Redux( Redux 中文官网 ) 是一个使用叫做“action”的事件来管理和更新应用状态的模式和工具库它以集中式Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

Redux核心概念:

  • action(动作): action 是一个具有 type 字段的普通JavaScript对象,描述应用程序中发生了什么的事件
  • reducer(函数): reducer 接收当前的 state 和一个 action 对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState
  • store(仓库):一个用于储存 Redux 应用的状态 的对象,它通过 传入一个 reducer 来创建的,并且有一个名为 getState 的方法 ,用于获取状态
  • dispatch (更新的方法): store.dispatch()是更新 state 的唯一方法,调用时要传入一个 action 对象

2.2Redux基础使用

  1. 创建仓库,获取状态,修改状态

    // 1.下载导入 legacy_createStore,重命名为createStore
    import { legacy_createStore as createStore } from 'redux';
    
    // 2调用createStore(),接收一个函数作为参数,在该函数的参数上定义state的初始值,并在函数内return
    const store = createStore(function (
      state = { count: 100, msg: 'hello redux' },
      //修改第二步:reducer函数的第二个参数action可以拿到 store.dispatch(参数对象)里的参数对象,
      action
    ) {
        //修改第三步:根据type判断,计算并返回新的state
      if (action.type === 'add') {
        return {
          ...state,
          count: state.count + 1,
        };
      }
      if (action.type === 'drop') {
        return {
          ...state,
          count: state.count - 1,
        };
      }
      return state;
    });
    // 通过store.getState()可以拿到定义的state
    console.log('001----->', store.getState());
    // store.dispatch({}) 对象参数必须有type属性
    //修改第一步:当调用store.dispatch({ type: 'add' })时,会重新触发reducer函数,并根据代码逻辑返回新的state
    store.dispatch({ type: 'add' });
    store.dispatch({ type: 'add' });
    store.dispatch({ type: 'add' });
    console.log('002----->', store.getState());
    store.dispatch({ type: 'drop' });
    console.log('003----->', store.getState());
    
    
  2. subscribe监听状态变化

    import { legacy_createStore as createStore } from 'redux';
    const store = createStore(function (
      state = { count: 100, msg: 'hello redux' },
      action
    ) {
      if (action.type === 'add') {
        return {
          ...state,
          count: state.count + 1,
        };
      }
      if (action.type === 'drop') {
        return {
          ...state,
          count: state.count - 1,
        };
      }
      return state;
    });
    store.dispatch({ type: 'add' });
    store.dispatch({ type: 'add' });
    store.dispatch({ type: 'add' });
    
    store.dispatch({ type: 'drop' });
    
    // store.subscribe可以监听到在它调用之后的状态更新,并拿到每一个更新的最新值
    // store.subscribe会返回一个取消监听的函数,调用返回的函数即可取消监听
    const unSubFn = store.subscribe(() => {
      //调用store.subscribe后,取消监听前,每一次store.dispatch的调用都会触发打印
      console.log('007----->', store.getState());
    });
    store.dispatch({ type: 'add' });
    store.dispatch({ type: 'add' });
    store.dispatch({ type: 'add' });
    
    // 取消监听
    unSubFn();
    
    store.dispatch({ type: 'add' });
    store.dispatch({ type: 'add' });
    
    
  3. store的推荐写法,单文件创建导出store

    // 1.下载导入 legacy_createStore,重命名为createStore
    import { legacy_createStore as createStore } from 'redux';
    
    // 推荐写法一:使用变量声明 state初始值
    const initState = { count: 100, msg: 'hello redux' };
    // 推荐写法二:将reducer 函数单独声明管理
    const reducerFn = (state = initState, action) => {
      // 推荐写法三,将关于action.type的判断用 Switch语句声明
      // action.type为要判断的键
      switch (action.type) {
        // 当键值为'xxx’时要执行的代码
        case 'add':
          return { ...state, count: state.count + action.payload };
    
        //   默认的代码块,
        default:
          return state;
      }
    };
    const store = createStore(reducerFn);
    
    export default store;
    

2.3使用react-redux简化redux使用

  1. index.js文件

    /*
      学习目标: react-redux简化 Redux的使用
    */
    //  1. 下包 npm i react-redux
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    //  2. 导入Provider组件
    import { Provider } from 'react-redux';
    // 导入store
    import store from './store';
    
    ReactDOM.render(
      //  3. 使用Provider组件包裹住App组件, 并且设置store属性
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    );
    
  2. App.js文件

    import { useState } from 'react';
    import { useDispatch, useSelector } from 'react-redux';
    
    export default function App() {
      const [show, setShow] = useState(true);
      return (
        <div>
          <h1>Redux基础案例</h1>
          <button onClick={() => setShow(!show)}>点击切换卸载挂载</button>
          {show && <Son />}
        </div>
      );
    }
    
    function Son() {
      //  4. useSelector取出数据
      // 💥useSelector相当于在第三方包的源码内部,调用了useState 和store.subscribe
      const count = useSelector((state) => state.count);
      // 5. 使用useDispatch的返回函数 --》dispatch函数
      const dispatch = useDispatch();
    
      return (
        <div>
          <h2>子组件</h2>
          <h2>count:{count}</h2>
    
          <button onClick={() => dispatch({ type: 'add' })}>+1</button>
          <button onClick={() => dispatch({ type: 'des' })}>-1</button>
        </div>
      );
    }
    

小结react-redux的两个方法:

  1. useSelector(获取redux状态):const 状态变量 = useSelector((state) => state.状态名);
  2. useDispatch(更新redux状态):
    1. 获取返回的函数:const dispatch = useDispatch()
    2. 调用返回函数并传入action对象修改:dispatch({ type: 'add' })