React(九):其他Hook、自定义Hook、Redux和Hook联动

一、其他Hook

1.useContext

之前我们使用Context给下边传值很麻烦:Context实现隔代通信

但是今天!它来了!

它就是useContext这个Hook,使用步骤:

1、找个文件偷摸儿创建两个Context

import { createContext } from "react";

let myContext = createContext();
let themeContext = createContext();

export {myContext, themeContext};

2、找到要接收值的组件标签,外边包上刚才偷摸儿创建的Context,可以包多层,每层都传个值。

import React, { memo } from 'react';
import Context from './Context';
import { myContext, themeContext } from './MyContext';

const App = memo(() => {
  return (
    <div>
      <h1>App</h1>
      
      <myContext.Provider value={{ name: 'zzy', age: 18 }}>
        <themeContext.Provider value={{color:'red',fontSize:'30px'}}>
          <Context />
        </themeContext.Provider>
      </myContext.Provider>
    </div>
  )
})

export default App

3、来到组件内,直接使用useContext接收,参数就是偷摸儿创建的那俩玩意儿,返回的就是你传的value对象,比用Consumer包裹再通过回调拿方便多了

import React, { memo, useContext } from 'react'
import { myContext, themeContext } from './MyContext'
const Context = memo(() => {
    
    let my = useContext(myContext);
    let theme = useContext(themeContext);
    console.log(my, theme);//{name: 'zzy', age: 18} {color: 'red', fontSize: '30px'}
    
    return (
        <div>
            <h1>Context</h1>
            <h2>{my.name}-{my.age}</h2>
            <h2 style={{ color: theme.color, fontSize: theme.fontSize }}>Theme</h2>
        </div>
    )
})

export default Context

而且当组件上层最近的 <xxxxx.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新的值,也就是说数据更新是响应式的。

2.useCallback

这个东西板儿b可以作为项目性能优化的亮点,具体是怎么使用的呢?我们一点点来看

(1)引出性能弱点

我们来看下面这个计数器案例,每次点击按钮修改counter的数据,函数组件就会重新执行,那每次就会重新定义一个increment函数。

虽然每次函数组件执行完,垃圾回收机制会将定义的increment函数回收,但是这种不必要的重复定义是会影响性能的。

import React, { memo, useState } from 'react'

const App = memo(() => {
  const [counter, setCounter] = useState(0)
  
  function increment() {
    setCounter(counter + 1)
  }

  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={() => increment()}>+1</button>
    </div>
  )
})

export default App

(2)子组件重复渲染的问题

上面这个案例中,虽然组件自身看起来好像没什么问题,但是设想一下,有一天App要把这个increment函数传给它的子组件<Son/>,那么每次在AppSon中更改counter的值,increament都会重新定义,那么也就意味着子组件的props接收的increament发生改变(记得复习下memo),子组件<Son/>会重新渲染。如果此时子组件<Son/>里还有一百个子组件<GrandSon/>,那岂不是这一百个组件都要跟着重新渲染,想想就可怕!

import React, { memo, useState, useCallback } from 'react'

const Son = memo((props) => {
  console.log("Son组件被重新渲染")
  return (
    <div>
      <button onClick={props.increment}>Counter+1</button>1<GrandSon/>子组件
      第2<GrandSon/>子组件      
      第3<GrandSon/>子组件
      ......100<GrandSon/>子组件
    </div>
  )
})

const App = memo(() => {
  const [counter, setCounter] = useState(0)
  const [message, setMessage] = useState("哈哈哈哈")
  
  function increment() {
    setCounter(counter + 1)
  }

  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={increment}>Counter+1</button>
      <button onClick={() => setMessage("呵呵呵呵")}>修改message</button>
      <Son increment={increment}/>
    </div>
  )
})

export default App

如果此时我们再在App中定义一个message的修改逻辑,那么每次修改message的值,同样App重新渲染,进而重新创建increment函数,导致SonGrandSon也重新渲染。

(3)第一波性能优化

如果使用useCallback,可以避免上面的问题。

import React, { memo, useState, useCallback } from 'react'

const Son = memo((props) => {
  console.log("Son组件被重新渲染")
  return (
    <div>
      <button onClick={props.increment}>Counter+1</button>1<GrandSon/>子组件
      第2<GrandSon/>子组件      
      第3<GrandSon/>子组件
      ......100<GrandSon/>子组件
    </div>
  )
})

const App = memo(() => {
  const [counter, setCounter] = useState(0)
  const [message, setMessage] = useState("哈哈哈哈")
  
  const increment = useCallback(function() {
    setCounter(counter + 1)
  }, [counter])

  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={increment}>Counter+1</button>
      <button onClick={() => setMessage("呵呵呵呵")}>修改message</button>
      <Son increment={increment}/>
    </div>
  )
})

export default App

上面代码中使用useCallback,就可以做到每次只有counter的值改变时,才会重新定义increment函数从而重新渲染Son

message等其他值的改变不会重新定义increment,依然使用之前的increment,这样的话子组件props接收的值就不会变从而不会重新渲染。

(4)终极性能优化

上面的代码看起来已经实现了我们想要的优化效果:让子组件只在特定的值改变时重新渲染。但是实际上我们还可以进一步优化:

如果我们想做到,不管哪个值改变,我都不要让子组件因为这个函数的重新定义而重新渲染,因为函数的逻辑本身是没变的,为什么要让子组件重新渲染呢?不太好

比较直观的做法是把依赖数组置空,意思是谁都不要影响我,谁都不能让我重新定义

const increment = useCallback(() => {
  setCounter(counter + 1)
}, [])

这样确实不会让子组件重新渲染了,但是这样会产生闭包陷阱,也就是说increment只在第一次定义的话,回调永远都是第一次的回调,counter永远都是第一次的值0,这样无论我们修改多少次counter, 页面展示的数据永远是 0 + 1 的结果。

解决方案就是useRef:

useRef函数在组件多次进行渲染时, 返回的对象在当前生命周期永远指向同一个地址;
我们就可以每次App重新执行将最新的counter储存到useRef.current属性中,然后每次都拿着 useRef.current做运算。

import React, { memo, useState, useCallback } from 'react'

const Son = memo((props) => {
  console.log("Son组件被重新渲染")
  return (
    <div>
      <button onClick={props.increment}>Counter+1</button>1<GrandSon/>子组件
      第2<GrandSon/>子组件      
      第3<GrandSon/>子组件
      ......100<GrandSon/>子组件
    </div>
  )
})

const App = memo(() => {
  const [counter, setCounter] = useState(0)
  const [message, setMessage] = useState("哈哈哈哈")
  
  // 组件进行多次渲染, 返回的是同一个ref对象
  const counterRef = useRef()
  // 每次都将最新的counter保存到ref对象current属性中
  counterRef.current = counter

  const increment = useCallback(() => {
    // 在修改数据时, 引用保存到ref对象current属性的最新的值
    setCounter(counterRef.current + 1)
  }, [])

  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={increment}>Counter+1</button>
      <button onClick={() => setMessage("呵呵呵呵")}>修改message</button>
      <Son increment={increment}/>
    </div>
  )
})

export default App

搞定,虽然没太懂,不过确实很屌

3.useMemo

useMemo返回的也是一个 memoized(有记忆的) 值; 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

  • 参数一: 传入一个回调函数
  • 参数二: 传入一个数组, 表示依赖, 什么都不依赖传入空数组; 如果不传则该函数什么都不会做, 无意义

举个例子:我们定义一个计算累加的函数calcNumTotal, 在App组件中调用这个函数计算结果。

import React, { memo } from 'react'
import { useState } from 'react'

// 定义一个函数求和
function calcNumTotal(num) {
  let total = 0
  for (let i = 1; i <= num; i++) {
    total += i
  }
  return total
}

const App = memo(() => {
  const [counter, setCounter] = useState(10)

  return (
    <div>
      {/* couter改变, 组件重新渲染, 意味着calcNumTotal函数也会重新执行, 重新计算结果 */}
      <h2>计算结果: {calcNumTotal(100)}</h2>

      <h2>当前计数: {counter}</h2>
      <button onClick={() => setCounter(counter + 1)}>+1</button>
    </div>
  )
})

export default App

但是counter改变时, App组件就会重新渲染, 那么calcNumTotal函数又会重新计算; 但是counter的改变和calcNumTotal函数并没有关系, 却要重新渲染; 这种类似的场景我们就可以使用useMemo进行性能优化:

import React, { memo, useMemo, useState } from 'react'

// 定义一个函数求和
function calcNumTotal(num) {
  console.log("calcNumTotal函数被调用")

  let total = 0
  for (let i = 1; i <= num; i++) {
    total += i
  }
  return total
}

const App = memo(() => {
  const [counter, setCounter] = useState(10)

  let result = useMemo(() => {
    return calcNumTotal(50)
  }, [])
  
  return (
    <div>
      {/* couter改变, 组件重新渲染, 意味着calcNumTotal函数也会重新执行, 重新计算结果 */}
      <h2>计算结果: {result}</h2>

      <h2>当前计数: {counter}</h2>
      <button onClick={() => setCounter(counter + 1)}>+1</button>
    </div>
  )
})

export default App

这样我们就实现counter发生变化, 而calcNumTotal函数不需要重新计算结果

4.useCallback和useMemo的区别

useMemo拿到的传入回调函数的返回值, useCallback拿到的传入的回调函数本身;

简单来说useMemo是让你不要每次都调用函数拿返回值, useCallback是让你不要每次都重新定义函数

useCallback(fn, [])
uesMemo(() => fn, [])

上面这两行表达的是同一个意思

5.useRef

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。

使用步骤:

  1. 创建ref:let titleRef = useRef();
  2. 绑定ref:<h1 ref={titleRef}>App</h1>
  3. 拿到ref:titleRef.current
import React, { memo, useRef } from 'react';

const App = memo(() => {
  //1.创建ref
  let titleRef = useRef();
  let inputRef = useRef();

  function showTitle() {
    // 3.拿到ref
    console.log(titleRef.current);
  }

  function focusInput() {
    console.log(inputRef.current);
    inputRef.current.focus();
  }

  return (
    <div>
      {/* 2.绑定ref */}
      <h1 ref={titleRef}>App</h1>
      <button onClick={showTitle}>查看App标题的dom</button>

      <input type="text" ref={inputRef}/>
      <button onClick={focusInput}>点击获取焦点</button>
    </div>
  )
})

export default App

它还有一个特点,就是利用useRef生成的对象在整个生命周期中一直都指向同一个地址

例如下面代码, 在我们修改counter时, App组件会重新渲染, 那么info对象也会重新在堆内存中开辟一个新的内存空间; 意味着我们每修改一次counter, 拿到是一个新的info对象

import React, { memo, useState, useRef } from 'react'

const App = memo(() => {
  const [counter, setCounter] = useState(10)

  function increment() {
    setCounter(counter + 1)
  }

  // 定义一个对象
  const info = {}

  return (
    <div>
      <h2>当前计数: {counter}</h2>
      <button onClick={increment}>+1</button>
    </div>
  )
})

export default App

我们可以使用useRef, 因为useRef不管渲染多少次, 返回的都是同一个ref对象

import React, { memo, useState, useRef } from 'react'

const App = memo(() => {
  const [counter, setCounter] = useState(10)

  function increment() {
    setCounter(counter + 1)
  }

  // 定义一个对象
  const infoRef = useRef()

  return (
    <div>
      <h2>{infoRef.current.name}-{infoRef.current.age}</h2>
      <h2>当前计数: {counter}</h2>
      <button onClick={increment}>+1</button>
    </div>
  )
})

export default App

useRef和useCallback一起使用, 可以解决闭包陷阱的问题, 在上面有对应的案例。

二、自定义Hook

必须以use开头噢

1.如何自定义?

自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。我感觉这东西就和高阶组件一个作用。

例如有这样一个需求: 所有的组件在创建和销毁时都进行打印

在这里插入图片描述

如果每个组件我们都单独编写是非常繁琐的, 并且有许多重复代码; 我们可以将实现这样逻辑相同的代码抽离为一个自定义的Hook,在其他的组件中调用自定义Hook即可。

2.自定义Context共享的hook

先创建两个Context:

import { createContext } from "react";

const userContext = createContext();
const tokenContext = createContext();

export {
    userContext,
    tokenContext
}

去入口文件中包裹一下,并给个值:

root.render(
  <userContext.Provider value={{name:'zzy', age: 18}}>
    <tokenContext.Provider value={'zzy1314'}>
      <App />
    </tokenContext.Provider>
  </userContext.Provider>
);

好,那么接下来我们来使用一下这玩意儿,正常情况下我们是这样用的:

import React, { memo, useContext } from 'react';
import { userContext,tokenContext } from './context';

const Son1 = memo(() => {
  const userInfo = useContext(userContext);
  const token = useContext(tokenContext);
  return (
    <div>
      <h1>Son1</h1>
      <h2>{userInfo.name}-{token}</h2>
    </div>
  )
})

接下来重点来了,如果我们要在每个组件都获取一下Context的值,我们这个案例就两个还好,如果有五个呢?难道每个组件都要写5行代码分别获取每个Context嘛?

这里就可以用自定义hook,对这个逻辑进行封装,在逻辑中我们获取这个玩意儿,并且以数组的形式返回出去,这样在组件中调用它并解构,就可以了:

在这里插入图片描述

3.自定义获取窗口滚动位置的hook

在组件中监听鼠标滚轮的位置, 如多个组件中都需要监听鼠标滚轮的数据,那么是很麻烦的一件事,每个组件都要写个监听和移除监听:

在这里插入图片描述

我们就可以封装到一个自定义的Hook中,我们和刚才那个Context写到一起吧:

在这里插入图片描述

这样就可以实现响应式更新,为什么是响应式的呢?我觉得是这样,在自定义hook中使用了useState,并且一滚动就会调用setScrollYscrollY进行更新,我们是可以监测到scrollY的改变并重新渲染的,而返回出去的scrollY和我们接的myScrollY指向的是一个东西,那你说组件是不是也能监测到myScrollY呢:

在这里插入图片描述

4.自定义同步更新本地存储的hook

如果有一天,我们要实现这样的功能:点击按钮修改本地存储中对应的key的value值:

在这里插入图片描述

那么如果我们有多个token要实现实时同步更新呢?(指的是组件内状态和本地存储值同步),这时就可以封装一个自定义hook:

在这里插入图片描述

那么这样的话,每次我们要改谁,只需要传过去一个key 的值,自定义hook就会设置一个修改本地存储的方法,然后返回该key对应的value值修改该value的方法,我们只需要在组件修改的时候调用该方法,就可以是实现组件内状态和本地存储值同步。

三、Redux中的Hook

在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux库中的connect:复习react-redux:组件中使用connext

但是这种方式必须使用高阶函数结合返回的高阶组件;
并且必须编写:mapStateToPropsmapDispatchToProps映射的函数

在Redux7.1开始,提供了Hook的方式,在函数组件中再也不需要编写connect以及对应的映射函数了。

我们按照RTK的方式配置好redux,然后来到组件中……

注意,这里引入hook都是从react-redux引入的,不是从react引入

import { shallowEqual, useDispatch, useSelector } from 'react-redux';

1.useSelector映射state到组件

参数一: 要求传入一个回调函数, 参数为state,返回一个对象,对象中可以自定义key的名字,value就是读取redux中的数据。

const myState = useSelector((state) => {
  return {
    fuckCount: state.home.counter
  }

参数二: shallowEqual,可以进行比较来决定是否组件重新渲染;

调用useSelector会返回一个对象,这个对象就是我们回调里返回的对象(我暂且这么理解),然后再组件中我们就可以用state中的数据了:

<h2>{myState.fuckCount}</h2>

2.useDispatch映射dispatch函数

之前我们总是很麻烦的获取dispatch函数,现在不用了,只需要:

  const dispatch = useDispatch();

就可以在组件任意地方直接调用dispatch派发action,举例:

import React, { memo } from 'react';
import {  useDispatch, useSelector } from 'react-redux';
import { addNumber } from './store/modules/home';

const App = memo(() => {
  const myState = useSelector((state) => {
    return {
      count: state.home.counter
    }
  });

  const dispatch = useDispatch();
  
  function changeNum(num) {
    dispatch(addNumber(num))
  }
  
  return (
    <div>
      <h1>App</h1>
      <h2>{myState.count}</h2>
      <button onClick={e => changeNum(1)}>点击修改state中的counter</button>
      <Son />
    </div>
  )
})

export default App

上面的案例中,我们点击按钮就可以直接派发action,不用像之前一样还要通过props调用了。

3.shallowEqual性能优化

通过useSelector获取redux数据有个特点,就是数据修改可以实现响应式(这是肯定的),不过有个性能不好的地方就是,redux中如果state对象中任何数据发生改变,那么用到state对象中数据的地方,都会重新渲染,我们看下面的例子:

import React, { memo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { addNumber, changeMsg } from './store/modules/home';

const Son = memo(() => {
  const { msg } = useSelector((state) => {
    return {
      msg: state.home.message
    }
  }, shallowEqual);
  console.log('子组件渲染');
  const dispatch = useDispatch();

  function changeMsgHandle() {
    dispatch(changeMsg('你好'))
  }
  return (
    <div>
      <h1>Son</h1>
      <h2>{msg}</h2>
      <button onClick={changeMsgHandle}>修改message</button>
    </div>
  )
})

const App = memo(() => {
  const myState = useSelector((state) => {
    return {
      count: state.home.counter
    }
  }, shallowEqual);

  const dispatch = useDispatch();
  function changeNum(num) {
    dispatch(addNumber(num))
  }
  console.log('父组件渲染', myState.count)
  return (
    <div>
      <h1>App</h1>
      <h2>{myState.count}</h2>
      <button onClick={e => changeNum(1)}>点击修改state中的counter</button>
      <Son />
    </div>
  )
})

export default App

我们在redux定义了两个数据:countermessage,在App和它的子组件Son中分别展示两个数据并添加对应的修改操作,此时我们如果修改App中的counter,父组件自然重新渲染,但是你会发现子组件也一起渲染了!子组件Son可是包了memo啊!props又没变,怎么会重新渲染呢?

这就是因为如果我们修改了redux中state对象中的数据,那么所有用到该数据的地方都会重新渲染,在Son中修改message同样会导致App的重新渲染,这显然很离谱(修改某个组件,结果其他用redux的地方也一起渲染,这非常可怕)

解决办法就是引入shallowEqual函数作为useSelector的第二个参数,React已经帮我们封装好了,原理就是做一个浅层比较。

import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 const { msg } = useSelector((state) => {
    return {
      msg: state.home.message
    }
  }, shallowEqual);
 const myState = useSelector((state) => {
   return {
     count: state.home.counter
   }
 }, shallowEqual);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值