React 之 Hooks解析
一、概念
1. class组件的优势
- class组件可以定义自己的state,用来保存组件自己内部的状态
- 函数式组件不可以,因为函数每次调用都会产生新的临时变量
- class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑,比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次
- 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求
- class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等
- 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次
2. Class组件存在的问题
- 复杂组件变得难以理解
- 随着业务的增多,我们的class组件会变得越来越复杂
- 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除)
- 这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度
- 组件复用状态很难
- 在前面为了一些状态的复用需要通过高阶组件
- 像之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用
- 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,代码就会存在很多嵌套
- 这些代码让我们不管是编写和设计上来说,都变得非常困难
3. 为什么需要Hook
Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)
hooks:
- 它可以让我们在不编写class的情况下使用state以及其他的React特性
- 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决
- 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook
- 100% 向后兼容的:Hook 不包含任何破坏性改动
- 现在可用:Hook 已发布于 v16.8.0
Hook的使用场景:
- Hook的出现基本可以代替我们之前所有使用class组件的地方
- 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它
- Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用
4. 计数器案例
- 函数式组件结合hooks让整个代码变得非常简洁
- 并且再也不用考虑this相关的问题
类组件实现
// 快捷键 => rpce
import React, { PureComponent } from 'react';
export class CountClass extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: 0
};
}
counterChange(num) {
this.setState({
counter: this.state.counter + num
});
}
render() {
// 快捷键 => dob
const { counter } = this.state;
return (
<>
<div>当前记数 : {counter}</div>
<button onClick={(e) => this.counterChange(1)}>+1</button>
<button onClick={(e) => this.counterChange(-1)}>-1</button>
</>
);
}
}
export default CountClass;
Hook实现
// 快捷键盘 => rmc
import { memo, useState } from 'react';
const App = memo(() => {
const [counter, setCounter] = useState(0);
return (
<>
<div>当前计数 : {counter}</div>
<button onClick={(e) => setCounter(counter + 1)}>+1</button>
<button onClick={(e) => setCounter(counter - 1)}>-1</button>
</>
);
});
export default App;
Class组件和Hook对比
二、useState
State Hook => 用来创建state
1. 解析
useState来自react,需要从react中导入,它是一个hook
- 参数:初始化值,如果不设置为undefined
- 只有在第一次渲染的时候执行
- 返回值:数组,包含两个元素
- 元素一:当前状态的值(第一调用为初始化值)
- 元素二:设置状态值的函数
- 调用设置状态值的函数
- 设置一个新的值
- 组件重新渲染,并且根据新的值返回DOM结构( 相当于重新执行render函数 )
2. 规则
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用
3. 详解
State Hook的API就是 useState :
- useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同
- 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留
- useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值
- 如果没有传递参数,那么初始化值为undefined
- useState的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便
// 1. 直接传入
const [count, setCount] = useState(0);
setCount(100);
const [position, setPosition] = useState({ x: 0, y: 0 });
setPosition({ x: 100, y: 100 });
// 2. 传入方法 => 方法会立即执行
const [name, setName] = useState(() => {
return 'hello';
});
三、useEffect
Effect Hook => 用来创建生命周期
1. 概念
Effect Hook 可以完成一些类似于class中生命周期的功能
- 网络请求
- 手动更新DOM
- 一些事件的监听
- 对于完成这些功能的Hook被称之为 Effect Hook
2. 解析
useEffect的解析:
- 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作
- useEffect要求传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数
- 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数
3. 案例
页面的title总是显示counter的数字
类组件实现
// 快捷键 => rpce
import React, { PureComponent } from 'react';
export class CountClass extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: 66
};
}
componentDidMount() {
document.title = this.state.counter;
}
// 1. 使用这个生命周期函数的时候
componentDidUpdate() {
document.title = this.state.counter;
}
counterChange(num) {
this.setState(
{
counter: this.state.counter + num
},
() => {
// 2. 或者使用这个回调函数
document.title = this.state.counter;
}
);
}
render() {
// 快捷键 => dob
const { counter } = this.state;
return (
<>
<div>当前记数 : {counter}</div>
<button onClick={(e) => this.counterChange(1)}>+1</button>
<button onClick={(e) => this.counterChange(-1)}>-1</button>
</>
);
}
}
export default CountClass;
Hook实现
// 快捷键盘 => rmc
import { memo, useState, useEffect } from 'react';
const App = memo(() => {
const [counter, setCounter] = useState(999);
// 1. 直接赋值,因为每次都会重新渲染,所以每次都会重新赋值 => 不推荐
// document.title = counter;
// 2. 使用useEffect,当前传入的回调函数会在组件被渲染完成后,自动执行
useEffect(() => {
// 事件坚听、定时器、网络请求、订阅消息、手动修改DOM、修改ref等等
document.title = counter;
});
return (
<>
<div>当前计数 : {counter}</div>
<button onClick={(e) => setCounter(counter + 1)}>+1</button>
<button onClick={(e) => setCounter(counter - 1)}>-1</button>
</>
);
});
export default App;
4. 需要清除Effect
在class组件中,某些副作用的代码,需要在componentWillUnmount中进行清除
在useEffect中 :
- 传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B
- type EffectCallback = () => (void | (() => void | undefined))
- 这是 effect 可选的清除机制,每个 effect 都可以返回一个清除函数
- 如此可以将添加和移除订阅的逻辑放在一起
- 它们都属于 effect 的一部分
- React 会在组件更新和卸载的时候执行清除操作
useEffect(() => {
// 1. 事件坚听、定时器、网络请求、订阅消息、手动修改DOM、修改ref等等 => 第一次只执行这里,
const unsubcribe = sotre.subscribe(()=>{
})
function foo(){}
eventBus.on('xxx',foo)
// 2. 返回一个函数,这个函数会在组件要重新渲染或者被销毁的时候自动执行 => 之后都是先执行这里,再执行上面的
return ()=>{
unsubcribe()
eventBus.off('xxx',foo)
}
});
5. 使用多个Effect
一个函数式组件中,可以存在多个useEffect
React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
// 1. 修改标题
useEffect(() => {
document.title = counter;
});
// 2. 监听事件总线
useEffect(() => {
function foo() {}
eventBus.on('xxx', foo);
return () => {
eventBus.off('xxx', foo);
};
});
// 3. 监听redux中的数据变化
useEffect(() => {
const unsubcribe = sotre.subscribe(() => {});
return () => {
unsubcribe();
};
});
6. Effect性能优化
问题
默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题
- 某些代码只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情
- 比如网络请求、订阅和取消订阅
- 多次执行也会导致一定的性能问题
解决
useEffect实际上有两个参数
- 参数一:执行的回调函数
- 参数二:该useEffect在哪些state发生变化时,才重新执行(受谁的影响)
- 如果每次都要执行 => 只要重新渲染组件,都会执行
- 不传第二个参数即可
- 如果只需要执行一次
- 不依赖任何的内容,可以传入一个空的数组
- 相当于componentDidMount和componentWillUnmount
- 如果根据修改的值执行对应的effect
- 传入一个数组,数组中放需要监听的数据
- 如果每次都要执行 => 只要重新渲染组件,都会执行
案例
受count影响的Effect => 会重新执行
// 快捷键盘 => rmc
import { memo, useState, useEffect } from 'react';
const App = memo(() => {
const [counter, setCounter] = useState(999);
const [isShow, setIsShow] = useState(true);
// 监听counter的变化
useEffect(() => {
document.title = counter;
console.log('监听counter的变化');
}, [counter]);
// 监听isShow的变化
useEffect(() => {
console.log('监听isShow的变化');
}, [isShow]);
// 监听事件总线
useEffect(() => {
console.log('事件总线');
}, []);
// 监听redux中的数据变化
useEffect(() => {
console.log('订阅数据');
}, []);
// 发起网络请求
useEffect(() => {
console.log('发起网络请求');
}, []);
return (
<>
<div>当前计数 : {counter}</div>
<button onClick={(e) => setCounter(counter + 1)}>+1</button>
<button onClick={(e) => setCounter(counter - 1)}>-1</button>
<button onClick={(e) => setIsShow(!isShow)}>切换 {isShow ? '显示' : '隐藏'}</button>
</>
);
});
export default App;
四、useContext
在之前的开发中,要在组件中使用共享的Context有两种方式:
- 类组件可以通过 类名.contextType = MyContext方式,在类中获取context
- 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context
Context Hook允许通过Hook来直接获取某个Context的值
当Context传入的值更改时,会触发子组件的重新渲染
1. 创建context
import { createContext } from 'react';
const counterContext = createContext();
const themeContext = createContext();
export { counterContext, themeContext };
2. 配置context
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { counterContext, themeContext } from './context';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<counterContext.Provider value={{ counter: 666 }}>
<themeContext.Provider value={{ theme: 'dark', color: 'red' }}>
<App />
</themeContext.Provider>
</counterContext.Provider>
);
3. 使用context
import React, { memo, useContext } from 'react';
// 1. 导入context
import { CounterContext, ThemeContext } from './context';
const App = memo(() => {
// 2. 使用context
const count = useContext(CounterContext);
const theme = useContext(ThemeContext);
return (
<>
<div>App</div>
<div style={{ color: theme.color }}>count: {count.counter}</div>
<div>theme: {theme.color}</div>
</>
);
});
export default App;
五、useReducer
useReducer不是redux的某个替代品
useReducer仅仅是useState的一种替代方案:
- 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分
- 或者这次修改的state需要依赖之前的state时,也可以使用
1. 使用useState
import React, { memo, useState } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
return (
<>
<h2>当前计数: {count}</h2>
<button onClick={(e) => setCount(count + 1)}>+1</button>
<button onClick={(e) => setCount(count - 1)}>-1</button>
<button onClick={(e) => setCount(count + 5)}>+5</button>
<button onClick={(e) => setCount(count - 5)}>-5</button>
<button onClick={(e) => setCount(count + 100)}>+100</button>
</>
);
});
export default App;
2. 使用useReducer
import React, { memo, useReducer } from 'react';
// 1. 定义 reducer 函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, counter: state.counter + 1 };
case 'decrement':
return { ...state, counter: state.counter - 1 };
case 'add_number':
return { ...state, counter: state.counter + action.num };
case 'sub_number':
return { ...state, counter: state.counter - action.num };
default:
return state;
}
}
const App = memo(() => {
// 2. 使用 useReducer 函数,传入 reducer 函数和初始值,得到 state 和 dispatch 函数
const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} });
return (
<>
{/* 3. 使用 state 中的数据, 通过 state.属性名 来获取 */}
<h2>当前计数: {state.counter}</h2>
{/* 4. 使用 dispatch 函数,传入 action 对象,来触发 reducer 函数,从而改变 state */}
<button onClick={(e) => dispatch({ type: 'increment' })}>+1</button>
<button onClick={(e) => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={(e) => dispatch({ type: 'add_number', num: 5 })}>+5</button>
<button onClick={(e) => dispatch({ type: 'sub_number', num: 5 })}>-5</button>
<button onClick={(e) => dispatch({ type: 'add_number', num: 100 })}>+100</button>
</>
);
});
export default App;
六、useCallback
useCallback实际的目的是为了进行性能的优化 => 返回值是优化后的函数
如何进行性能的优化 :
- useCallback会返回一个函数的 memoized(记忆的) 值
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的
1. 初始
import React, { memo, useState } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
return (
<>
<h2>当前计数: {count}</h2>
{/* 每次点击+1,都会重新渲染App组件,导致子组件也会重新渲染,该箭头函数也会重新创建,浪费性能 */}
<button onClick={(e) => setCount(count + 1)}>+1</button>
</>
);
});
export default App;
2. 更改
import React, { memo, useState, useCallback } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
// useCallback 用于缓存函数,但是不会缓存函数中的变量,也就是传入的函数依然会重新创建,所以这样写是没用的
// 和原来的写法一样,没有任何优化
const handleBtnClick = useCallback((e) => {
setCount(count + 1);
});
return (
<>
<h2>当前计数: {count}</h2>
<button onClick={handleBtnClick}>+1</button>
</>
);
});
export default App;
3. 修复
import React, { memo, useState, useCallback } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
// 小心闭包陷阱 => 当不传依赖的时候,函数只会创建一次。依赖项中的 count 一直是初始值 0,所以每次点击按钮,count 都为 1,而不是累加 => 没有创建新的函数
// const handleBtnClick = useCallback(function (e) {
// setCount(count + 1);
// }, []);
// 传入第二个参数,依赖项数组,当依赖项发生变化时,才会重新创建函数,否则复用之前的函数,避免了闭包陷阱
const handleBtnClick = useCallback(function (e) {
setCount(count + 1);
}, [count]);
return (
<>
<h2>当前计数: {count}</h2>
<button onClick={handleBtnClick}>+1</button>
</>
);
});
export default App;
4. 解释
上方做法依然没有进行优化,当count改变的时候,依然会创建新的函数
和普通的没有区别
当把函数从父组件传递给子组件,父组件有其他数据更改时,该优化才能生效
普通函数效果
- 当点击修改count时
- 不管点击父组件还是子组件,因为handleBtnClick重新创建,所以都会重新渲染
- 当点击修改name的时候
- 子组件也会修改
- 因为App组件重新执行,handleBtnClick重新创建,子组件接收的props更改了
- 但这是不应该的
import React, { memo, useState, useCallback } from 'react';
const Home = memo((props) => {
console.log('Home组件被调用');
// 3. 子组件通过props接收父组件传递过来的函数
const { handleBtnClick } = props;
return (
<>
<h2>Home组件</h2>
{/* 4. 子组件通过props接收父组件传递过来的函数,并且调用 */}
<button onClick={handleBtnClick}>Home : +1</button>
{/* 假如有100个组件 => 全都会重新渲染 */}
</>
);
});
const App = memo(() => {
const [count, setCount] = useState(0);
const [name, setName] = useState('jack');
// 1. 使用普通方法定义的函数
const handleBtnClick = (e) => setCount(count + 1);
return (
<>
<h2>当前计数: {count}</h2>
<button onClick={handleBtnClick}>+1</button>
<h2>当前名称: {name}</h2>
<button onClick={(e) => setName(Math.random())}>修改名称</button>
<hr />
{/* 2. 把函数传递给子组件,子组件通过props接收 */}
<Home handleBtnClick={handleBtnClick} />
</>
);
});
export default App;
使用useCallback效果
- 当点击修改count时
- 不管点击父组件还是子组件,因为handleBtnClick重新创建,所以都会重新渲染
- 当点击修改name的时候
- 子组件并不会重新渲染
- 因为count没有更改,所以handleBtnClick函数不会重新创建,props没有改变
import React, { memo, useState, useCallback } from 'react';
const Home = memo((props) => {
console.log('Home组件被调用');
// 3. 子组件通过props接收父组件传递过来的函数
const { handleBtnClick } = props;
return (
<>
<h2>Home组件</h2>
{/* 4. 子组件通过props接收父组件传递过来的函数,并且调用 */}
<button onClick={handleBtnClick}>Home : +1</button>
{/* 假如有100个组件 => 该组件不重新渲染,其下的组件也都不会,大大优化了 */}
</>
);
});
const App = memo(() => {
const [count, setCount] = useState(0);
const [name, setName] = useState('jack');
// 1. 通过useCallback包裹函数,把函数传递给子组件
const handleBtnClick = useCallback(
function (e) {
setCount(count + 1);
},
[count]
);
return (
<>
<h2>当前计数: {count}</h2>
<button onClick={handleBtnClick}>+1</button>
<h2>当前名称: {name}</h2>
<button onClick={(e) => setName(Math.random())}>修改名称</button>
<hr />
{/* 2. 把函数传递给子组件,子组件通过props接收 */}
<Home handleBtnClick={handleBtnClick} />
</>
);
});
export default App;
5. 进一步优化
当count发生改变的时候,也使用同一个函数
做法一
将count依赖移除掉 => 缺点 : 闭包陷阱
// 小心闭包陷阱 => 当不传依赖的时候,函数只会创建一次。依赖项中的 count 一直是初始值 0,所以每次点击按钮,count 都为 1,而不是累加 => 没有创建新的函数
const handleBtnClick = useCallback(function (e) {
setCount(count + 1);
}, []);
做法二
做法一 + 使用useRef => 特点 : 在组件多次渲染时,返回的是同一个值
import React, { memo, useState, useCallback, useRef } from 'react';
const Home = memo((props) => {
console.log('Home组件被调用,只会在组件第一次渲染时调用');
const { handleBtnClick } = props;
return (
<>
<h2>Home组件</h2>
<button onClick={handleBtnClick}>Home : +1</button>
</>
);
});
const App = memo(() => {
const [count, setCount] = useState(0);
const [name, setName] = useState('jack');
// 1. useRef 保存的值,在组件重新渲染时,不会发生改变
const countRef = useRef();
// 2. 但是,可以通过修改 ref.current 的值,来达到保存数据的目的
countRef.current = count;
const handleBtnClick = useCallback(
(e) => {
setCount(countRef.current + 1);
},
// 3. 去除依赖项,让 useCallback 每次都返回同一个函数
[]
);
return (
<>
<h2>当前计数: {count}</h2>
<button onClick={handleBtnClick}>+1</button>
<h2>当前名称: {name}</h2>
<button onClick={(e) => setName(Math.random())}>修改名称</button>
<hr />
<Home handleBtnClick={handleBtnClick} />
</>
);
});
export default App;
6. 总结
- 使用useCallback和不使用useCallback定义一个函数是否会带来性能的优化
- 不会,和普通函数的定义没有区别
- 使用useCallback和不使用useCallback定义一个函数传递给子组件是否会带来性能的优化
- 会,当需要将一个函数传递给子组件时,最好使用useCallback进行优化,将优化后的函数传递给子组件
七、useMemo
useMemo实际的目的也是为了进行性能的优化 => 返回值是一个值
进行大量的计算操作,可以使用useMemo进行优化
1. 不依赖任何的值
只会被执行一次
import React, { memo, useMemo, useState } from 'react';
// 1. 写在函数组件外面的代码, 只会被执行一次,防止被多次创建
function calcNumTotal(num) {
console.log('calcNumTotal的计算过程被调用~');
let total = 0;
for (let i = 1; i <= num; i++) {
total += i;
}
return total;
}
const App = memo(() => {
const [count, setCount] = useState(0);
// 2.不依赖任何的值, 进行计算 => 只会被执行一次
const result = useMemo(() => {
return calcNumTotal(50);
}, []);
return (
<>
<h2>计算结果: {result}</h2>
<h2>计数器: {count}</h2>
<button onClick={(e) => setCount(count + 1)}>+1</button>
</>
);
});
export default App;
2. 依赖count
每次count发生变化, 都会重新计算
import React, { memo, useMemo, useState } from 'react';
// 1. 写在函数组件外面的代码, 只会被执行一次,防止被多次创建
function calcNumTotal(num) {
console.log('calcNumTotal的计算过程被调用~');
let total = 0;
for (let i = 1; i <= num; i++) {
total += i;
}
return total;
}
const App = memo(() => {
const [count, setCount] = useState(0);
// 2.依赖count, 进行计算 => 每次count发生变化, 都会重新计算
const result = useMemo(() => {
return calcNumTotal(count * 2);
}, [count]);
return (
<>
<h2>计算结果: {result}</h2>
<h2>计数器: {count}</h2>
<button onClick={(e) => setCount(count + 1)}>+1</button>
</>
);
});
export default App;
3. useMemo和useCallback的对比
/**
* useMemo和useCallback的对比. useCallback返回的是一个函数, useMemo返回的是一个值
* 1. useCallback(fn, []) => fn
* 2. useMemo(() => fn, []) => fn()
* 这两种写法表示的意思是一样的
*/
function fn() {}
// 返回值是一个函数 => useCallback
const increment = useCallback(fn, []);
// 返回值是一个值 => useMemo
const increment2 = useMemo(() => fn, []);
4. 对子组件渲染进行优化
如果对子组件传入相同内容的对象时,可以进行优化 => 因为每次执行函数,都会创建新对象,子组件就会重新渲染
如果传递给子组件的是个值的情况下,值不改变,子组件就不会重新渲染
import React, { memo, useMemo, useState } from 'react';
// 创建一个子组件
const HelloWorld = memo(function (props) {
console.log('HelloWorld被渲染~');
return <h2>Hello World</h2>;
});
function calcNumTotal(num) {
console.log('calcNumTotal的计算过程被调用~');
let total = 0;
for (let i = 1; i <= num; i++) {
total += i;
}
return total;
}
const App = memo(() => {
const [count, setCount] = useState(0);
const result = useMemo(() => {
return calcNumTotal(50);
}, []);
// 1. 每次都会创建一个新的对象,导致子组件的props发生变化
// const info = { name: 'why', age: 18 }; // 会导致子组件的props发生变化,从而导致子组件的重新渲染
// 2. 使用useMemo对子组件渲染进行优化,只有当info发生变化的时候,才会重新创建新的对象,否则使用缓存的对象 => 优化子组件的渲染
const info = useMemo(() => ({ name: 'why', age: 18 }), []); // 不会导致子组件的props发生变化,从而不会导致子组件的重新渲染
return (
<>
<h2>计算结果: {result}</h2>
<h2>计数器: {count}</h2>
<button onClick={(e) => setCount(count + 1)}>+1</button>
<HelloWorld result={result} info={info} />
</>
);
});
export default App;
5. 总结
- 进行大量的计算操作,是否有必须要每次渲染时都重新计算
- 可以使用useMemo进行优化
- 对子组件传递相同内容的对象时,使用useMemo进行性能的优化
- 父组件有不变的静态对象时,可以进行优化
八、useRef
useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变
ref是两种用法:
- 用法一:引入DOM(或者组件,但是需要是class组件)元素
- 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变
引入DOM
import React, { memo, useRef } from 'react';
const App = memo(() => {
// 1. useRef() 生成一个 ref 对象
const titleRef = useRef();
//3. 通过 ref 对象的 current 属性获取引用的元素
const getTitleRef = () => {
console.log(titleRef.current);
};
return (
<>
{/* 2. 将 ref 对象传递给需要引用的元素 */}
<div ref={titleRef}>App111</div>
<button onClick={getTitleRef}>获取titleRef</button>
</>
);
});
export default App;
保存数据
使用ref保存上一次的某一个值
import React, { memo, useCallback, useRef, useState } from 'react';
const App = memo(() => {
const [count, setCount] = useState(0);
const countRef = useRef(0);
countRef.current = count;
const handleClick = useCallback((e) => {
// 如果直接使用 count,会有闭包问题 !!! => 因为这里不依赖外界,所以只会创建一次函数, count 是在 handleClick 函数创建时就已经存在了,而不是每次渲染时才创建
// setCount(count + 1);
// 如果使用 countRef.current,就不会有闭包问题 !!! => 因为这里依赖外界,所以每次渲染都会创建新的函数, countRef.current 是在 handleClick 函数执行时才存在的
setCount(countRef.current + 1);
}, []);
return (
<>
<div>App : {count}</div>
<button onClick={handleClick}>+1</button>
</>
);
});
export default App;
九、useImperativeHandle
在父组件中,通过传递ref绑定子组件,获取的权限太高
可以通过useImperativeHandle,来约束外界的权限
import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react';
// 1. 创建子组件 => 接收ref
const Child = memo(
forwardRef((props, ref) => {
// 2. 创建自己内部的ref
const childRef = useRef();
// 4. 暴露给父组件的方法 => 通过useImperativeHandle暴露给父组件
useImperativeHandle(ref, () => ({
focus: () => {
childRef.current.focus();
},
setValue(value) {
childRef.current.value = value;
}
}));
// 3. 将自己内部的ref绑定
return <input ref={childRef} />;
})
);
const App = memo(() => {
// 5. 创建父组件的ref
const parentRef = useRef();
function handleDOM() {
// 6. 调用子组件暴露给父组件的方法 => parentRef.current拿到的是useImperativeHandle返回的对象
// console.log(parentRef.current)
parentRef.current.focus();
parentRef.current.setValue('哈哈哈');
// 无法直接操作子组件的value,因为没有暴露给父组件
// parentRef.current.value = ""
}
return (
<>
<Child ref={parentRef}></Child>
<button onClick={handleDOM}>DOM操作</button>
</>
);
});
export default App;
十、useLayoutEffect
useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已
- useEffect
- 会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新
- useLayoutEffect
- 会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新
如果希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect
官方更推荐使用useEffect而不是useLayoutEffect
import React, { memo, useEffect, useLayoutEffect } from 'react';
const App = memo(() => {
useLayoutEffect(() => {
console.log('useLayoutEffect', '第二执行');
});
useEffect(() => {
console.log('useEffect', '第三执行');
});
console.log('App', '第一执行');
return <div>App</div>;
});
export default App;
十一、自定义Hook
自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性
注意 : 名称需要用useXXX
1. 打印生命周期
所有的组件在创建和销毁时都进行打印
import { memo, useEffect, useState } from 'react';
// 1. 自定义hook,命名必须以use开头 => 生命周期
const useLife = (name) => {
useEffect(() => {
console.log(name + '被创建');
return () => {
console.log(name + '被销毁');
};
}, [name]);
};
const FirstComponent = memo(() => {
// 2. 使用自定义hook
useLife('firstComponent');
return <h1>firstComponent</h1>;
});
const SecondComponent = memo(() => {
// 2. 使用自定义hook
useLife('secondComponent');
return <h1>secondComponent</h1>;
});
const App = memo(() => {
// 2. 使用自定义hook
useLife('App');
const [isShow, setIsShow] = useState(true);
return (
<>
<h1>App</h1>
<button onClick={() => setIsShow(!isShow)}>切换</button>
{isShow && <FirstComponent />}
{isShow && <SecondComponent />}
</>
);
});
export default App;
2. Context的共享数据
创建context/index.js
import { createContext } from 'react';
const CounterContext = createContext();
const ThemeContext = createContext();
export { CounterContext, ThemeContext };
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { CounterContext, ThemeContext } from './context';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<CounterContext.Provider value={{ counter: 666 }}>
<ThemeContext.Provider value={{ theme: 'dark', color: 'red' }}>
<App />
</ThemeContext.Provider>
</CounterContext.Provider>
);
App.jsx
import { memo, useEffect, useContext } from 'react';
import { CounterContext, ThemeContext } from './context';
// 1. 自定义hook,命名必须以use开头 => context抽取共享
const useSelfContext = (name) => {
const counterContext = useContext(CounterContext);
const themeContext = useContext(ThemeContext);
return { counterContext, themeContext };
};
const FirstComponent = memo(() => {
// 2. 使用自定义hook
const { counterContext, themeContext } = useSelfContext();
return (
<>
<h1>firstComponent</h1>
<div>
counterContext {'=>'} {counterContext.counter}
</div>
<div>
themeContext {'=>'} {themeContext.theme}
</div>
</>
);
});
const SecondComponent = memo(() => {
// 2. 使用自定义hook
const { counterContext, themeContext } = useSelfContext();
return (
<>
<h1>secondComponent</h1>
<div>
counterContext {'=>'} {counterContext.counter}
</div>
<div>
themeContext {'=>'} {themeContext.theme}
</div>
</>
);
});
const App = memo(() => {
return (
<>
<h1>App</h1>
<FirstComponent />
<SecondComponent />
</>
);
});
export default App;
3. 获取滚动位置
import { memo, useEffect, useContext, useState } from 'react';
// 1. 自定义hook,命名必须以use开头 => 获取滚动位置
const useScrollPosition = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(
() => {
function scrollFn() {
console.log('scroll', window.scrollY, window.scrollX);
setPosition({ x: window.scrollX, y: window.scrollY });
}
window.addEventListener('scroll', scrollFn);
return () => {
window.removeEventListener('scroll', scrollFn);
};
},
// 不依赖,只执行一次
[]
);
return position;
};
const FirstComponent = memo(() => {
// 2. 使用自定义hook
const position = useScrollPosition();
return (
<>
<h1>
firstComponent : {position.x} - {position.y}
</h1>
</>
);
});
const SecondComponent = memo(() => {
return (
<>
<h1>secondComponent</h1>
</>
);
});
const App = memo(() => {
return (
<div style={{ height: '2000px' }}>
<h1>App</h1>
<FirstComponent />
<SecondComponent />
</div>
);
});
export default App;
4. localStorage数据存储
import { memo, useEffect, useState } from 'react';
// 自定义hook,命名必须以use开头 => localStorage数据存储
const useLocalStorage = (key) => {
// 1. 通过 localStorage 获取数据,初始化 state
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem(key)) || '';
});
// 2. 监听 localStorage 变化,更新 state
useEffect(() => {
localStorage.setItem(key, JSON.stringify(data));
}, [data, key]);
// 3. 返回 state 和更新 state 的函数
return [data, setData];
};
const SecondComponent = memo(() => {
// 1. 使用自定义 hook
const [name, setName] = useLocalStorage('name');
return (
<>
<h1>secondComponent : {name}</h1>
<button onClick={() => setName(Math.random().toFixed(2))}>setName</button>
</>
);
});
const App = memo(() => {
const [age, setAge] = useLocalStorage('age');
return (
<div>
<h1>App</h1>
<h1>age : {age}</h1>
<button onClick={() => setAge((Math.random() + 10).toFixed(2))}>setAge</button>
<hr />
<SecondComponent />
</div>
);
});
export default App;
十二、redux hooks
在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect:
- 但是这种方式必须使用高阶函数结合返回的高阶组件
- 并且必须编写:mapStateToProps和 mapDispatchToProps映射的函数
在Redux7.1开始,提供了Hook的方式,再也不需要编写connect以及对应的映射函数了
配置
安装
npm i react-redux @reduxjs/toolkit
目录结构
store/modules/counter.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
counter: 66,
message: 'Hello Redux'
},
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
// Use the PayloadAction type to declare the contents of `action.payload`
incrementAction(state, { payload }) {
state.counter += payload;
},
messageChangeAction(state, { payload }) {
state.message = payload;
}
}
});
export default counterSlice.reducer;
export const { incrementAction, messageChangeAction } = counterSlice.actions;
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './modules/counter';
const store = configureStore({
reducer: {
counter: counterReducer
// Add the generated reducer as a specific top-level slice
}
});
export default store;
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { Provider } from 'react-redux';
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
之前的使用方法
import React, { memo } from 'react';
import { connect } from 'react-redux';
import { incrementAction } from './store/modules/counter';
const App = memo((props) => {
// 4. 接收状态和dispatch
const { counter, increment } = props;
return (
<>
<div>App</div>
<div>counter: {counter}</div>
<button onClick={() => increment(5)}>+5</button>
<button onClick={() => increment(-5)}>-5</button>
</>
);
});
// 1. 映射状态
const mapStateToProps = (state) => ({
counter: state.counter.counter
});
// 2. 映射dispatch
const mapDispatchToProps = (dispatch) => ({
increment(num) {
dispatch(incrementAction(num));
}
});
// 3. connect高阶组件
export default connect(mapStateToProps, mapDispatchToProps)(App);
hooks中使用
- useSelector : 将state映射到组件中
- 参数
- 参数一:将state映射到需要的数据中 => 是个回调函数
- 参数二:可以进行比较来决定是否组件重新渲染
- useSelector默认会比较我们返回的两个对象是否相等
- 比较方式 const refEquality = (a, b) => a === b
- 也就是必须返回两个完全相等的对象才可以不引起重新渲染
- 参数
- useDispatch : 接获取dispatch函数
- useStore : 获取当前的store对象
基本使用
import React, { memo } from 'react';
// 1. 导入
import { useSelector, useDispatch } from 'react-redux';
import { incrementAction } from './store/modules/counter';
const Home = memo((props) => {
const state = useSelector((state) => ({
message: state.counter.message
}));
console.log('Home render');
return (
<>
<div>Home</div>
<div>message: {state.message}</div>
</>
);
});
const App = memo((props) => {
// 2. 获取store中的数据, 通过useSelector映射到组件中
const state = useSelector((state) => ({
counter: state.counter.counter
}));
// 3. 获取dispatch, 通过useDispatch映射到组件中
const dispatch = useDispatch();
const incrementHandle = (num) => {
// 4. 调用dispatch, 传入action
dispatch(incrementAction(num));
};
console.log('App render');
return (
<>
<div>App</div>
<div>counter: {state.counter}</div>
<button onClick={(e) => incrementHandle(5)}>+5</button>
<button onClick={(e) => incrementHandle(-5)}>-5</button>
<hr />
<Home />
</>
);
});
export default App;
存在问题
子组件home使用了memo : memo包裹的组件,只有当props改变时,才会重新渲染
但是App组件更改了counter后,Home组件也跟着刷新了
Home组件更改message后,App也重新渲染了
原因 : 因为useSelector默认监听的是整个state,只要有一个地方改变了,全部重新渲染
解决方法
在使用useSelector时,第二个参数传入浅层比较函数shallowEqual即可
// 导入入shallowEqual
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
// ...
const state = useSelector(
(state) => ({
message: state.counter.message
}),
shallowEqual
);
// ...
const state = useSelector(
(state) => ({
counter: state.counter.counter
}),
shallowEqual
);