目录
2、类组件可以使用ref,函数组件不能,需要用React.forwardRef
4.useReducer +useContext实现redux
hook中没有this,所以函数组件或者其它地方都可以使用箭头函数
一、useRef
useRef是用来在组件不同渲染之间共用一些数据的,它的作用和我们在类组件里面为this赋值是一样的。
获取页面上的元素,比如Echarts等等。
1、语法
useRef接收null作为初始值,它的返回值是一个ref对象,
这个对象的.current属性就是该数据的最新值。
使用useRef的一个最简单的情况就是在函数组件里面获取DOM对象的引用
如果写多个ref={test},绑定了多个相同的,只能获取到一个,就是最后一个。不会报错不会报警告
import {useRef} from "react"
const inputRef= useRef(null)
<input ref={inputRef} type='text' />
inputRef.current.focus() //使用那个元素
2、类组件可以使用ref,函数组件不能,需要用React.forwardRef
import React, { useState, useEffect, useRef } from 'react'
// 函数组件,这样写不能使用ref,得使用React.forwardRef
let Box = () => <div ref={test2}></div>
//类组件,可以使用ref
class Box2 extends React.Component {
constructor() { super() }
render() {
return <div ref={test3}></div>
}
}
export default function App() {
let mybox = useRef()
let test2 = useRef()
let test3 = useRef()
return (
<div>
<p ref={mybox}></p>
</div>
)
}
3、案例:
import { useRef, useEffect } from 'react'
import ReactDOM from 'react-dom'
const AutoFocusInput = () => {
const inputRef = useRef(null)
//这里的inputRef 与下面的ref关联
useEffect(() => {
// 组件挂载后自动聚焦
inputRef.current.focus()
}, []) //[],就代表只运行一次
return (
<input ref={inputRef} type='text' />
)
}
ReactDOM.render(<AutoFocusInput />, document.getElementById('root'))
在上面代码中inputRef其实就是一个{current: input节点}对象,只不过它可以保证在组件每次渲染的时候拿到的都是同一个对象。
二、useCallback
1.useCallback引入
我们在定义函数组件的时候时常在函数体内定义一些内嵌函数(比如事件),
这些内嵌函数会在组件每次重新渲染的时候被重新定义
如果它们作为props传递给了子组件的话,即使其它props的值没有发生变化,
它都会使子组件重新渲染,而无用的组件重渲染可能会产生一些性能问题。
==>无用的组件重渲染
(父组件将函数传入给子组件,重新渲染的时候被函数就会被重新定义)
每次重新生成新的内嵌函数还有另外一个问题
就是当我们把内嵌函数作为dependency传进useEffect的dependencies数组的话,
因为该函数频繁被重新生成,所以useEffect里面的effect就会频繁被调用。
为了解决上述问题,React允许我们使用 useCallback来记住当前定义的函数,
并在下次组件渲染的时候 返回之前定义的函数 而不是使用新定义的函数。
简而言之useCallback就是
把我们在 函数组件 内部定义 的 函数 保存起来,
当组件重新渲染时还是使用之前的,就不会被重新定义一次
useCallback的返回值是官方规定的新函数
2.语法
import {useCallback} from "react"
const memoizedCallback = useCallback(callback, dependencies)
useCallback接收两个参数,
第一个参数是需要被记住的函数,
第二个参数是这个函数的dependencies,
只有dependencies数组里面的元素的值 发生变化时 useCallback才会 返回新定义的函数,
否则useCallback都会返回之前定义的函数。
(如果第二个参数不填,就跟useEffect一样,每次都会运行。)
3.案例:
import React, { useCallback } from 'react'
import useSearch from 'hooks/useSearch'
import ReactDOM from 'react-dom'
//items列表可能包含上千个数据,子组件就会被渲染多次
const HugeList = ({ items, onClick }) => {
return (
<div>
{
items.map((item, index) => (
<div key={index} onClick={()=>onClick(index)}>
{item}
</div>
))
}
</div>
)
}
const MemoizedHugeList = React.memo(HugeList)
const SearchApp = ({ searchText }) => {
const handleClick = useCallback(item => {
console.log(item)
}, [])
const items = useSearch(searchText)
return (<MemoizedHugeList items={items} onClick={handleClick} />)
}
ReactDOM.render(<SearchApp />, document.getElementById('root'))
//定义了一个HugeList组件,由于这个组件需要渲染一个大的列表(items),所以每次重渲染都是十分消耗性能的,因此使用了React.memo函数来让该组件只有在onClick函数和items数组发生变化的时候才被渲染,接着我在SearchApp里面使用MemoizedHugeList,由于要避免该组件的重复渲染,所以我使用了useCallback来记住定义的handleClick函数,这样在组件后面渲染的时候,handleClick变量指向的都是同一个函数,所以MemorizedHugeList只有在items发生变化时才会重新渲染
4.滥用useCallback--影响性能
任何优化都会有代价,useCallback也是一样的。
当我们在函数组件里面调用useCallback函数的时候,
React背后要做一系列计算才能保证当dependencies不发生变化的时候,
我们拿到的是同一个函数,
因此如果我们滥用useCallback的话,并不会带来想象中的性能优化,反而会影响到我们的性能
第二个参数dependencies来限制useCallback的执行条件
一般是属性、状态、函数改变了,就会执行
如果该函数没有刷新页面,没有改变,不会去重新渲染页面 ,没必要用 useCallback==>
就是在当前组件中 没有可能发生 更新模板或者属性改变 的情况下,
组件内部的函数没必要用 useCallback
错误使用案例:
import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'
const Mybtn = () => {
const handleClick = useCallback(() => {
console.log('666')
}, [])
return (
<button onClick={handleClick}>btn</button>
)
}
ReactDOM.render(<Mybtn />, document.getElementById('root'))
//上面例子使用的useCallback没有起到任何优化代码性能的作用,反而由于hook内部机制的运行,它消耗的计算资源其实比没有优化之前还多,相当于:
import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'
const Mybtn = () => {
const inlineClick = () => {
console.log('666')
}
const handleClick = useCallback(inlineClick, [])
return (
<button onClick={handleClick}>btn</button>
)
}
ReactDOM.render(<Mybtn />, document.getElementById('root'))
三、useMemo
1.useMemo的引入
useMemo和useCallback的作用十分类似
只不过它允许你 记住 任何类型的变量(不只是函数)
useMemo的返回值由我们自己设定,可以是函数、变量....
useCallback的返回值是官方设定的函数
2.语法
import {useMemo} from "react"
const memoizedValue = useMemo(() => valueNeededToBeMemoized, dependencies)
useMemo接收一个函数,
该函数的返回值就是需要被记住的变量,
当useMemo的第二个参数dependencies数组里面的元素的值没有发生变化的时候,memoizedValue使用的就是上一次的值。
(如果不用,就会造成每次都要运行那个函数,就降低了性能,最后值也一样,就没必要)
3.案例:
例子中calculatePrimes是用来计算素数的,因此每次调用它都需要消耗大量的计算资源。
为了提高组件渲染的性能,我们可以使用useMemo来记住计算的结果,
当iterations和multiplier保持不变的时候,
我们就不需要重新执行calculatePrimes函数来重新计算了,直接使用上一次的结果即可。
import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'
const RenderPrimes = ({ iterations, multiplier }) => {
const primes = React.useMemo(
() => calculatePrimes(iterations, multiplier),
[iterations,multiplier]
)
return (
<div>
Primes! {primes}
</div>
)
}
ReactDOM.render(<RenderPrimes />, document.getElementById('root'))
四、useContext
1.useContext的引入
多层级的传值 ==> props层层传递
我们知道React中组件之间传递参数的方式是props,
假如我们在父级组件中定义了某些状态,而这些状态需要在该组件深层次嵌套的子组件中被使用的话就需要将这些状态以props的形式层层传递,这就造成了props drilling的问题。
为了解决这个问题,
React允许我们使用Context来在 父级组件和底下任意层次的子组件之间传递状态。
在函数组件中我们可以使用useContext Hook来使用Context。
2.语法:
useContext接收一个context对象为参数,
该context对象是由React.createContext函数生成的。
useContext的返回值是当前context的值,
这个值是由最邻近的<MyContext.Provider>来决定的。
一旦在某个组件里面使用了useContext这就相当于该组件订阅了这个context的变化,
当最近的<MyContext.Provider>的context值发生变化时,
使用到该context的子组件就会被触发重渲染,且它们会拿到context的最新值。
import React, { useContext} from 'react'
import MyContext from "./MyContext.jsx"
const value = useContext(MyContext) //provider传过来的值
//value 在组件中的任何地方可以使用,如果不写的话,只能在consumer里面使用
3.案例:
src/ctx/userctx.jsx
import React from "react";
let Carctx=React.createContext({
msg:"hello"
})
export default Carctx;
App.jsx
import React, { useState } from 'react'
import Box1 from "./Box1.jsx"
import Userctx from "./ctx/userctx"
export default function App() {
//将changeObj传过去,子组件也可以改obj,相当于反向传值
let [obj, changeObj] = useState({ msg: "app数据", id: 1 })
return (
<Userctx.Provider value={[obj, changeObj]}>
<Box1></Box1>
</Userctx.Provider>
)
}
Box2.jsx
import React, { useContext } from "react";
import Userctx from "./ctx/userctx.jsx"
export default function Box2() {
var [obj, changeObj] = useContext(Userctx) //provider提供的数据
let change = () => {
//因为useState是全量替代,修改数据的方法有三种:
//方法一:
changeObj({ msg: "修改msg", id: 2 })
// //方法二:
// changeObj({ msg: "修改msg", id: obj.id })
// //方法三:常用
// //先把obj里面的需要修改的属性值改了,在将obj整体传入
// //需要注意:传一个obj是不对的,它会检测到没有变化,因为是同一个引用,就需要...obj
// obj.msg="修改msg"
// changeObj({...obj})
}
return (
<Userctx.Consumer>
{(ctx) => {
return (
<div>
<p>{obj.msg}----{obj.id}</p>
<button onClick={change}>点击</button>
</div>
)
}}
</Userctx.Consumer>
)
}
4.使用时避免无用渲染
如果一个函数组件使用了useContext(SomeContext)的话它就订阅了这个SomeContext的变化,这样当SomeContext.Provider的value发生变化的时候,这个组件就会被重新渲染。
这里有一个问题就是,我们可能会把很多不同的数据放在同一个context里面,而不同的子组件可能只关心这个context的某一部分数据,当context里面的任意值发生变化的时候,无论这些组件用不用到这些数据它们都会被重新渲染,这可能会造成一些性能问题.
错误使用案例:
问题:
因为useState后面的数据全部写在了一起,其中任何一个属性变了,就会全部刷新
但是消费者只用了对象中的一部分数据,但是一个数据改变了,其它没使用的属性也会跟着刷新,就是无用渲染
ChildrenComponent只使用到了appContext的.theme属性,可是当appContext其它属性例如configuration被更新时,ChildrenComponent也会被重新渲染,而ChildrenComponent调用了一个十分耗费性能的ExpensiveTree组件,所以这些无用的渲染会影响到我们页面的性能
import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'
const AppContext = React.createContext()
const ChildrenComponent = () => {
//因为这行代码,对象里的任何属性变了,都会重新刷新。
const [appContext] = useContext(AppContext)
const theme = appContext.theme
return (
<div>
<ExpensiveTree theme={theme} />
</div>
)
}
const App = () => {
const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})
return (
<AppContext.Provider value={[appContext, setAppContext]}>
<ChildrenComponent />
</AppContext.Provider>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
5.解决上面这个问题的方法有下面三种:
5.1拆分Context
这个方法是最被推荐的做法,和useState一样,
我们可以将不需要同时改变的context拆分成不同的context,让它们的职责更加分明,
这样子组件只会订阅那些它们需要订阅的context从而避免无用的重渲染。
App.jsx
import React, { useState } from 'react'
import Userctx from './ctx/userctx'
import Carctx from './ctx/carctx'
import Box1 from "./Box1.jsx"
export default function App() {
let [obj,changeObj]=useState({msg:"app组件提供的数据",id:10,user:{usertoken:"abcfsdf"}})
let [car,changecar]=useState({info:"car信息"})
return (
<Userctx.Provider value={[obj,changeObj]}>
<Carctx.Provider value={[car,changecar]}>
<Box1></Box1>
</Carctx.Provider>
</Userctx.Provider>
)
}
Box1.jsx
import React,{useContext} from 'react'
import Box2 from "./Box2.jsx"
import carctx from './ctx/carctx.jsx'
export default function Box1() {
let [car,changecar]=useContext(carctx)
return (
<div>
<h1>box1--{car.info}</h1>
<Box2></Box2>
</div>
)
}
Box2.jsx
import React, { useEffect, useContext } from 'react'
import userctx from './ctx/userctx'
import carctx from './ctx/carctx'
export default function Box2() {
let [obj, changeObj] = useContext(userctx)
let [car, changecar] = useContext(carctx)
let look = () => { console.log(obj) }
let change1 = () => {
obj.msg = "66666box2吧数据修改了"
changeObj({ ...obj })
}
useEffect(() => {
console.log(obj)
}, [])
return (
<userctx.Consumer>
{(ctx) => {
return (<div>
<h1>Box2--{car.info}</h1>
<h2>box2-----{obj.msg}---{obj.id}</h2>
{/* <button onClick={()=>{console.log(ctx)}}>look</button> */}
<button onClick={look}>look</button>
<button onClick={change1}>change</button>
</div>)
}}
</userctx.Consumer>
)
}
5.2拆分组件,使用memo来优化消耗性能的组件
如果出于某些原因你不能拆分context
仍然可以通过 将消耗性能 的 组件和父组件的 其他部分 分离开来
并且 使用 memo函数 来 优化消耗性能 的组件
import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'
const AppContext = React.createContext()
const ExpensiveComponentWrapper = React.memo(({ theme }) => {
return (
<ExpensiveTree theme={theme} />
)
})
const ChildrenComponent = () => {
const [appContext] = useContext(AppContext)
const theme = appContext.theme
return (
<div>
<ExpensiveComponentWrapper theme={theme} />
</div>
)
}
const App = () => {
const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})
return (
<AppContext.Provider value={[appContext, setAppContext]}>
<ChildrenComponent />
</AppContext.Provider>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
5.3 不拆分组件,也可以使用useMemo来优化
哪个组件有无用渲染,就将其用useMemo来包裹起来
eg:
const [obj, setObj] = useState({ user: { color: 'red' }, car: { showTips: false }})
问题:Box1用了user,Box2用了car,如果Box1修改了theme中的属性值,那么Box2也会刷新,但其实Box2使用的值根本没变,所以没必要刷新Box2
解决:
在Box2中使用useMemo来包裹起来,让useMemo返回那个标签
App.jsx
import React, { useState } from 'react'
import Userctx from './ctx/userctx'
import Box1 from "./Box1.jsx"
export default function App() {
let [obj,changeObj]=useState({user:{msg:"app组件提供的user数据",id:10},car:{info:"app组件提供的car数据"}})
return (
<Userctx.Provider value={[obj,changeObj]}>
<Box1></Box1>
</Userctx.Provider>
)
}
Box1.jsx
import React, { useContext, useMemo } from 'react'
import Box2 from "./Box2.jsx"
import userctx from './ctx/userctx.jsx'
function MyMemoBox2() {
let [user, changeuser] = useContext(userctx)
let Box1 = () => {
console.log("这个函数打印代表了当前组件重新创建")
let change1 = () => {
user.user.msg = "box1修改了user的数据"
changeuser(JSON.parse(JSON.stringify(user)))//因为user.user是引用数据 useMemo(Box1,[user.user])监听的是引用 所有要深拷贝
}
return (<div>
<h1 onClick={change1}>box166--{user.user.msg}</h1>
<Box2></Box2>
</div>)
}
return useMemo(Box1, [user.user])
}
export default MyMemoBox2
Box2.jsx
import React, { useEffect, useContext } from 'react'
import userctx from './ctx/userctx'
export default function Box2() {
let [user, changeuser] = useContext(userctx)
let change1 = () => {
user.car.info = "box2修改了数据"
changeuser({ ...user })
}
return (<div>
<h1>box2--{user.car.info}</h1>
<button onClick={change1}>box2修改car中数据</button>
</div>)
}
五、useReducer
1.useReducer引入
useReducer用最简单的话来说就是允许我们在函数组件里面像使用redux一样通过reducer和action来管理我们组件状态的变换
2.语法:
useReducer和useState类似,都是用来管理组件状态的,只不过和useState的setState不一样的是,useReducer返回的dispatch函数是用来触发某些改变state的action而不是直接设置state的值,至于不同的action如何产生新的state的值则在reducer里面定义。
useReducer接收的三个参数分别是:
reducer:这是一个函数,它的签名是(currentState, action) => newState,从它的函数签名可以看出它会接收当前的state和当前dispatch的action为参数,然后返回下一个state,也就是说它负责状态转换的工作。
initialArg:如果调用者没有提供第三个init参数,这个参数代表的是这个reducer的初始状态,如果init参数有被指定的话,initialArg会被作为参数传进init函数来生成初始状态。
init:这是一个用来生成初始状态的函数,它的函数签名是(initialArg) => initialState,从它的函数签名可以看出它会接收useReducer的第二个参数initialArg作为参数,并生成一个初始状态initialState
const [state, dispatch] = useReducer(reducer, initialArg, init?)
3.案例:
import React, { useState, useReducer } from 'react'
let todoId = 1
const reducer = (currentState, action) => {
switch(action.type) {
case 'add':
return [...currentState, {id: todoId++, text: action.text}]
case 'delete':
return currentState.filter(({ id }) => action.id !== id)
default:
throw new Error('Unsupported action type')
}
}
const Todo = ({ id, text, onDelete }) => {
return (
<div>
{text}
<button
onClick={() => onDelete(id)}
>
remove
</button>
</div>
)
}
const App = () => {
const [todos, dispatch] = useReducer(reducer, [])
const [text, setText] = useState('')
return (
<>
{
todos.map(({ id, text }) => {
return (
<Todo
text={text}
key={id}
id={id}
onDelete={id => {
dispatch({ type: 'delete', id })
}}
/>
)
})
}
<input onChange={event => setText(event.target.value)} />
<button
onClick={() => {
dispatch({ type: 'add', text })
setText('')
}}
>
add todo
</button>
</>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
4.useReducer +useContext实现redux
import React from "react"
let context1=React.createContext(null)
export default context1
import { useRef, useEffect, useCallback, useState, useMemo, useReducer } from 'react'
import Mycontext from "./MyContext.jsx"
import Mybox2 from "./Mybox2.jsx"
function Mybox() {
let [state,dispach]=useReducer((state,action)=>{
if(action.type=="msg"){
state.msg= action.value
}
else if(action.type=="age"){
state.age= action.value
}
console.log(state)
return JSON.parse(JSON.stringify(state))
},{msg:"hello",age:18})
// useReducer+useContext ==>做出redux框架的功能(全局数据共享)
//1.给根组件生成一个数据容器(不是用useState生成的,是用useReducer)
//2.把这个数据容器(是一个hook容器)传给子代组件使用,使用的技术是useContext
return (
<Mycontext.Provider value={[state,dispach]}>
<h1>1</h1>
<p>{state.msg}</p>
<Mybox2></Mybox2>
</Mycontext.Provider>
)
}
export default Mybox
import {useReducer,useContext} from 'react'
import context1 from "./MyContext.jsx"
import MyBox3 from "./MyBox3.jsx"
export default function Mybox2() {
const [state,dispacth] = useContext(context1)
console.log(state);
let fn=()=>{
console.log(6666)
dispacth({type:"msg",value:"1234567"})
}
return (
<div>
<h1>2</h1>
<p>{state.msg}</p>
<button onClick={fn}>修改</button>
<MyBox3></MyBox3>
</div>
)
}
import React,{useContext} from 'react'
import context2 from "./MyContext.jsx"
export default function Mybox3() {
let [state,dispacth]=useContext(context2)
return (
<div>
<h1>3</h1>
<p>{state.msg}</p>
</div>
)
}
5.useReducer vs useState(面试)
useReducer和useState都可以用来管理组件的状态,它们之间最大的区别就是,useReducer将状态和状态的变化统一管理在reducer函数里面,这样对于一些复杂的状态管理会十分方便我们debug,因为它对状态的改变是封闭的。而由于useState返回的setState可以直接在任意地方设置我们状态的值,当我们组件的状态转换逻辑十分复杂时,它将很难debug,因为它是开放的状态管理。总体的来说,在useReducer和useState如何进行选择的问题上我们可以参考以下这些原则:
-
useState情况使用
- state的值是JS原始数据类型,如number, string和boolean等 - state的转换逻辑十分简单 - 组件内不同的状态是没有关联的,它们可以使用多个独立的useState来单独管理
-
useReducer情况使用
- state的值是object或者array - state的转换逻辑十分复杂, 需要使用reducer函数来统一管理 - 组件内多个state互相关联,改变一个状态时也需要改变另一个,放在同一个state内使用reducer来统一管理 - 状态定义在父级组件,不过需要在深层次嵌套的子组件中使用和改变父组件的状态,可以同时使用useReducer和useContext两个hook,将dispatch方法放进context里面来避免组件的props drilling - 如果你希望你的状态管理是可预测的和可维护的,请useReducer - 如果你希望你的状态变化可以被测试,请使用useReducer