之前我们使用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
而且当组件上层最近的
更新时,该 Hook 会触发重新渲染,并使用最新的值,也就是说数据更新是响应式的。
这个东西板儿b可以作为项目性能优化的亮点,具体是怎么使用的呢?我们一点点来看
我们来看下面这个计数器案例,每次点击按钮修改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
上面这个案例中,虽然组件自身看起来好像没什么问题,但是设想一下,有一天App要把这个increment
函数传给它的子组件
,那么每次在App
和Son
中更改counter
的值,increament
都会重新定义,那么也就意味着子组件的props
接收的increament
发生改变(记得复习下memo),子组件
会重新渲染。如果此时子组件
里还有一百个子组件
,那岂不是这一百个组件都要跟着重新渲染,想想就可怕!
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
函数,导致Son
和GrandSon
也重新渲染。
如果使用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接收的值就不会变从而不会重新渲染。
上面的代码看起来已经实现了我们想要的优化效果:让子组件只在特定的值改变时重新渲染。但是实际上我们还可以进一步优化:
如果我们想做到,不管哪个值改变,我都不要让子组件因为这个函数的重新定义而重新渲染,因为函数的逻辑本身是没变的,为什么要让子组件重新渲染呢?不太好
比较直观的做法是把依赖数组置空,意思是谁都不要影响我,谁都不能让我重新定义
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
搞定,虽然没太懂,不过确实很屌
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函数不需要重新计算结果
useMemo拿到的传入回调函数的返回值, useCallback拿到的传入的回调函数本身;
简单来说useMemo
是让你不要每次都调用函数
拿返回值, useCallback
是让你不要每次都重新定义函数
useCallback(fn, [])
uesMemo(() => fn, [])
上面这两行表达的是同一个意思
useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。
使用步骤:
let titleRef = useRef();
App
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
一起使用, 可以解决闭包陷阱的问题, 在上面有对应的案例。
必须以use开头噢
自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。我感觉这东西就和高阶组件一个作用。
例如有这样一个需求: 所有的组件在创建和销毁时都进行打印
如果每个组件我们都单独编写是非常繁琐的, 并且有许多重复代码; 我们可以将实现这样逻辑相同的代码抽离为一个自定义的Hook,在其他的组件中调用自定义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,对这个逻辑进行封装,在逻辑中我们获取这个玩意儿,并且以数组的形式返回出去,这样在组件中调用它并解构,就可以了:
在组件中监听鼠标滚轮的位置, 如多个组件中都需要监听鼠标滚轮的数据,那么是很麻烦的一件事,每个组件都要写个监听和移除监听:
我们就可以封装到一个自定义的Hook中,我们和刚才那个Context写到一起吧:
这样就可以实现响应式更新,为什么是响应式的呢?我觉得是这样,在自定义hook中使用了useState
,并且一滚动就会调用setScrollY
对scrollY
进行更新,我们是可以监测到scrollY
的改变并重新渲染的,而返回出去的scrollY
和我们接的myScrollY
指向的是一个东西,那你说组件是不是也能监测到myScrollY
呢:
如果有一天,我们要实现这样的功能:点击按钮修改本地存储中对应的key的value值:
那么如果我们有多个token要实现实时同步更新呢?(指的是组件内状态和本地存储值同步),这时就可以封装一个自定义hook:
那么这样的话,每次我们要改谁,只需要传过去一个key 的值,自定义hook就会设置一个修改本地存储的方法,然后返回该key对应的value值
和修改该value的方法
,我们只需要在组件修改的时候调用该方法,就可以是实现组件内状态和本地存储值同步。
在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux库中的connect:复习react-redux:组件中使用connext
但是这种方式必须使用高阶函数结合返回的高阶组件;
并且必须编写:mapStateToProps
和 mapDispatchToProps
映射的函数
在Redux7.1开始,提供了Hook的方式,在函数组件中再也不需要编写connect以及对应的映射函数了。
我们按照RTK的方式配置好redux,然后来到组件中……
注意,这里引入hook都是从react-redux引入的,不是从react引入
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
参数一: 要求传入一个回调函数, 参数为state
,返回一个对象,对象中可以自定义key
的名字,value
就是读取redux中的数据。
const myState = useSelector((state) => {
return {
fuckCount: state.home.counter
}
参数二: shallowEqual,可以进行比较来决定是否组件重新渲染;
调用useSelector
会返回一个对象,这个对象就是我们回调里返回的对象(我暂且这么理解),然后再组件中我们就可以用state中的数据了:
<h2>{myState.fuckCount}h2>
之前我们总是很麻烦的获取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调用了。
通过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定义了两个数据:counter
和message
,在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);