目录
扩展学习资料
名称 | 链接 |
React Hooks 官方文档 | |
useEffect 完整指南 |
React Hooks 编写函数组件
Hooks使命
- 逻辑与UI分离 :React官方推荐在开发中将逻辑部分与视图部分解耦,便于定位问题和职责清晰。
- 函数组件拥有state:在函数组件中如果要实现类似拥有state的状态,必须要将组件转成class组件。
- 逻辑组件复用:社区一直致力于逻辑层面的复用,像render props / HOC【高阶组件】,不过他们都有对应的问题。Hooks是目前为止相对完美的解决方案。
Hooks解决了什么问题
render props
- 通过渲染props来实现逻辑组件复用。
- Avatar组件时一个渲染头像的组件,里面包含其中一些业务逻辑,User组件时纯UI组件展示用户名称。
- render props 通过嵌套组件实现,在真实的业务中,会出现嵌套多层,以及梳理props不清晰的问题。
export default function App() {
return (
<div className="APP">
<Avatar name="云">
{name => <User name={name} />}
</Avatar>
</div>
);
}
HOC【高阶组件】
- 通过对现有组件进行扩展、增强的方式来实现复用,通常采用包裹方法来实现。
- 高阶组件的实现会额外地增加元素层级,使得页面元素的数量更加臃肿
class Avatar extends Component{
render() {
return <div> {this.props.name} </div>;
}
}
function HocAvatar(Component) {
return ()=><Component name="云" />;
}
export default HocAvatar(Avatar);
Hooks
- React16.8引入的Hooks,使得实现相同功能而代码量更少成为现实。
- 通过使用Hooks,不仅在硬编码层面减少代码的数量,同样的在编译之后的代码也会更少。
import React, {useState} from "react";
export function HooksAvatar() {
const [name, setName] = useState("云");
return <>{name}</>;
}
Hooks原理
并不是复杂的diff逻辑,仅仅是一个数组arrays
Hooks的demo
- 下面是一个非常简单的Hook API,创建了name和setName。在页面上展示name,按钮的点击事件修改name。
- 那么在这个过程中setState是如何实现的呢?
import React, {useState} from 'react';
import {render} from 'react-dom';
function App() {
const [name, setName] = useState('云');
return (
<div>
<div>{name}</div>
<button type="button" onClick={() => setName('hooks 原理')}>修改</button>
</div>
);
}
useState源码解析
- useState API虽然是在react中引入的,其内部实现是在react-reconciler包中完成的。
- 在try/catch代码部分,调用了mountState方法。
- 顺着这个方法,我们去探寻一些mountState的实现。
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDec = 'useState';
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcherOnMountInDEV;
try {
// 调用mountState(初始化值)
return mountState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
mountState源码解析
mountState方法内:
- 先返回当前执行中的Hook对象,Hook对象内包含memoizedState/baseState/queue等属性。
- 接着会将下次要渲染的state值和修改state的方法返回。
- 最后将这两个值以数组的形式返回。
如果方法里面有多个useState方法,如何让这些按期望的顺序执行呢?
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 返回当前正在运行的hook对象
const hook = mountWorkInProgressHook();
if(typeof initialState === 'function') {
// 是函数,就执行函数,拿return 值
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
// 字符串,就赋值
hook.memoizedState = hook.baseState = initialState;
// 定义一个队列,相当于queue = {...}
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRebderedState: (initialState: any),
});
// 这个dispatch就是我们在外面拿到的哪个修改值的方法
const dispatch: Dispatch<BasicStateAction<S>,>
= (queue.dispatch = (dispatchAction,bind(null, currentlyRenderingFiber, queue, ) : any));
// [name, setName] = useState(initialState)
return [hook.memoizedState, dispatch];
}
- 在初始化时,每一次申明useState就左图所示,会生成一对state/setter映射。接着每次渲染都会按照这个顺序从数组最小下标遍历到最大值。
- 在前面代码(mountState)中,我们说会先返回一个hook对象。state值(memoizedState)和返回的setXXX都会关联到这个hook对象。因此在触发某一个setXXX方法的时候可以正确地设置memoizedState值。
- 所以初始化执行useState时,不能写在条件判断里,会打乱顺序。要在后面业务中判断。
Hooks应用
Hooks 实践
Hooks官方API
- useState: 函数组件中的state方法
- useEffect(fn, time):函数组件处理副作用的方法【异步接口,setTimeout...】useEffect(fn, null) ~=componentDidMount
- useReducer:另一种‘useState’,跟redux有点类似
- useRef:返回一个突变的ref对象,对象在组件的生命周期内一直存在。【执行一些一直都在的方法】
- useCustom:自定义Hooks组件
倒计时组件
CountDown组件-1
倒计时组件在我们日常的开发中十分常见。我们就以这个组件的开发来实践Hooks。
- ·首先声明基本变量,这里申明了三个变量,分别表示按钮默认的文案、倒计时的数字、是否开启倒计时。
- 绑定button点击事件。
const maxTine = 10;
const [countDownText, setCountDownText] = useState('获取验证码');
const [timerNum,setTimerNum] = useState(maxTime);
const [boolCountDown,setBoolCountDown] = useState(false);
const handleClick = () => {
// 开始倒计时
}
return (
<div>
<button type="button" onClick={handleClick}>
{boolCountDown ? `${timerNum}s`} : countDownText}
</button>
</div>
)
CountDown组件-2
- 处理点击事件,判断当前是否在倒计时中,否则开启倒计时。
- useEffect依赖boolCountDown变量,然后开启计时。这里因为倒计时是突变的,所以把这部分代码放到refCountDown.current属性中。
- 将具体的倒计时判断逻辑方法赋值给current属性。至此倒计时组件完成。
const refCountDown = useRef(null);// 挂载倒计时的载体
const handleClick = () => {
if(boolCountDown) {
setBoolCountDown(true);
}
};
refCountDown.current = () => { // 具体逻辑处理放在current属性上
if (timerNum > 0) {
setTimerNum(timerNum - 1);
} else {// 倒计时结束,重置状态
setTimerNum(maxTime);
setBoolCountDown(false);
setCountDownText('重新获取');
}
}
useEffect(()=>{
if(boolCountDown) { // true 开始倒计时
const timer = setInterval(()=>{
refCountDown.current();
}, 1000);
return () => clearInterval(timer);
}
}, [boolCountDown]); // 检测到boolCountDown变化,就会执行
Hooks完整版倒计时组件
import React, { useState, useEffect, useRef } from "react";
export default function CountDown() {
const maxTime = 60;
const [countDownText, setCountDownText] = useState("获取验证码");
const [timerNum, setTimerNum] = useState(maxTime);
const [boolCountDown, setBoolCountDown] = useState(false);
const refCountDown = useRef(null);
const handleClick = () => { // 按钮事件是否开启
if (!boolCountDown) {
setBoolCountDown(true);
}
};
refCountDown.current = () => { // 如果timerNum不绑定在这里,它不会实时变化
if (timerNum > 0) {
setTimerNum(timerNum - 1);
} else {
setTimerNum(maxTime);
setBoolCountDown(false);
setCountDownText("重新获取");
}
};
// const otherCountDown = () => {
// console.log(timerNum, "timerNum");
// setTimerNum((timerNum) => timerNum - 1);
// };
useEffect(() => {
if (boolCountDown) {
const timer = setInterval(() => {
// otherCountDown();
refCountDown.current();
}, 1000);
return () => clearInterval(timer);// 执行一次,就清空一下定时
}
}, [boolCountDown]); // 根据这个参数监控的,想当componentDidMount使用就置空参数,就会只执行一次
return (
<div>
<button type="button" onClick={handleClick}>
{boolCountDown ? `${timerNum}s` : countDownText}
</button>
</div>
);
}
练习
1.用Hooks 实现一个倒计时组件
2.用class完成一个倒计时组件