一、useState
-
useState 可以让函数组件也可以拥有 state 状态
-
语法: const [xxx, setXxx] = React.useState(initValue)
-
useState 接受一个初始值参数,该参数只会在组件初始渲染中起作用,后续渲染时会被忽略,在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state
-
useState 无论调用多少次,相互之间是独立的。react是根据useState出现的顺序来保证多个useState 之间是相互独立
-
setXxx 两种用法:
setXxx(newValue),参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值。如果多次连续调用,最后只会更新最近一次的值(合并更新一次状态)setXxx(value => newValue), 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值。如果多次连续调用,参数 value 是最新的值,会更新多次状态
-
setXxx 是采用“异步直接赋值”的形式,并不会像类组件中的setState()那样做“异步对比累加赋值”。这里说的异步和类组件中 setState 中的异步是同一个意思,都是为了优化 React 渲染性能而故意为之。也就是无法在 setXxx 之后立即拿到最新的值
// useState 异步回调获取不到最新值及解决方案
import React, { useState, useEffect } from 'react';
const App = () => {
const [arr, setArr] = useState([0]);
useEffect(() => {
console.log(arr); // 此处可以获取最新到的 state 值
}, [arr]);
const handleClick = () => {
setArr([...arr, 1]);
}
return (
<>
<button onClick={handleClick}>change</button>
</>
);
}
export default App;
-
修改状态时,如果是复杂数据类型则需要先复制出一份,修改某属性后再整体赋值
-
修改状态时,但是如果新值和当前值完全一样,是不会引发页面重新渲染的,通过React官方文档可以知道,修改状态的时候,Hook会使用
Object.is()来对比当前值和新值,结果为 true 则不渲染,结果为 false 就会重新渲染
1-1 useState 是相互独立的
let showFruit = true;
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
if(showFruit) {
const [name, setName] = useState('hhh');
showFruit = false;
}
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
// 测试
// 第一次渲染组件
useState(42); //将 age 初始化为42
useState('hhh'); //将 name 初始化为 hhh
useState([{ text: 'Learn Hooks' }]); // 将 todos 初始化为 [{ text: 'Learn Hooks' }]
// 重新渲染渲染组件
useState(42); // 读取状态变量 age 的值(这时候传的参数42直接被忽略)
// useState('banana'); // 重新渲染没有去执行 useState('banana')
useState([{ text: 'Learn Hooks' }]); // 此时读取到的却是状态变量 fruit 的值,导致报错
1-2 解决 useState 多次渲染的问题
// 以下代码执行完会导致页面渲染两遍
// 原因是,在React中,同步代码会合并渲染,异步代码不会合并渲染。
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(async () => {
const res = await axios.get("xxx");
setLoading(false);
setData(res);
}, []);
// 以下代码只会渲染一次,它会将 setLoading 和 setData 进行合并。这个其实和类组件是一样的,在异步函数中不会合并setState。
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => {
setLoading(false);
setData({ a: 1 });
}, []);
// 解决方案一:多个状态合并到一个状态中
const [request, setRequest] = useState({ loading: true, data: null });
useEffect(async () => {
const res = await axios.get("xxx");
setRequest({ loading: false, data: res });
}, []);
// 缺点:如果只想 setState 一个依赖项时,需要将别的依赖项也要传进去,否则这个值会丢失。React内部并不会帮你做去合并
// setRequest({ data: res }); // 就会导致loading值丢失了。
// 解决方案是使用setRequest({ ...request, data: res }) 或者 setRequest((prevState) => ({ ...prevState, data: res }));
// 解决方案二:写一个自定义合并依赖项的hook
const useMergeState = (initialState) => {
const [state, setState] = useState(initialState);
const setMergeState = (newState) =>
setState((prevState) => ({ ...prevState, newState }));
return [state, setMergeState];
};
const [request, setRequest] = useMegeState({ loading: false, data: null });
useEffect(async () => {
const res = await axios.get("xxx");
setRequest({ loading: true, data: res });
// setRequest({ data: { a: 1 } }); // loading 状态不会丢失,还是 true
}, []);
// 解决方案三:使用 useReducer
const initState = { loading: false, data: null }
function loginReducer(state, action) {
if(action.type==='login'){
return {...state,...action.newState}
}
}
const [state, dispatch] = useReducer(loginReducer,initState);
useEffect(async () => {
const res = await axios.get("xxx");
dispatch({ type: 'login', newState: {loading: true, data: res} });
// setRequest({ data: { a: 1 } }); // loading 状态不会丢失,还是 true
}, []);
二、useEffect
-
useEffect 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子),副作用指的是置身于组件渲染之外,绑定事件,发起网络请求,访问修改dom都称为副作用
-
发送网络请求,访问修改dom,记录日志,这些副作用都是在组件卸载前无需清除的,因为在执行完这些操作之后,就可以忽略;但是像绑定事件,设置定时器,订阅外部数据这些副作用时候必须清除的,否者会引起内存泄漏
-
语法:useEffect(effect,[deps])
-
第一个参数为定义的执行函数(可以在此处执行副作用),第2个参数是依赖关系(可选参数)。若一个函数组件中定义了多个useEffect,那么他们实际执行顺序是按照在代码中定义的先后顺序来执行的
-
useEffect 第一次被调用是 render 之后,相当于
componentDidMount,之后的调用相当于componentDidUpdate, useEffect 会返回一个回调函数,这个回调函数的执行时机很重要,他的作用是清除上一次作用遗留下来的状态,比如一个组件在第三次,第五次,第七次渲染之后执行useEffect中的执行函数,那么回调函数就会在就会在组件第四次,第六次,第八次渲染之前执行,就是为了清除上一次渲染遗留下来的状态,如果 useEffect 只在第一次渲染之后执行,那么他返回的回调函数只会在组件卸载之前调用相当于componentWillUnmount -
useEffect 意义:关注点分离,不同的事情分开放
import React, { useState, useEffect } from 'react'
export default function App() {
const [count, setCount] = useState(0)
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
const onResize = () => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}
// 相当于 componentDidMount 和 componentDidUpdate
useEffect(() => {
document.title = count
})
// 相当于 componentDidMount 和 componentWillUnmount
useEffect(() => {
window.addEventListener('resize', onResize, false)
return () => {
window.removeEventListener('resize', onResize, false)
}
}, [])
return (
<div>
<button
onClick={() => { setCount(count + 1) }}
>
count+1
</button>
<h1>{size.height}||{size.width}</h1>
</div>
)
}
2-1 如何判断多个值都改变了才执行 useEffect 内部方法?
import React, { useState, useEffect, useRef } from "react"
const One = () => {
const [a, setA] = useState(0)
const [b, setB] = useState(0)
const ref = useRef({ a, b })
useEffect(() => {
let { a: prevA, b: prevB } = ref.current
console.log("更新前:", prevA, prevB)
console.log("更新后:", a, b)
if (prevA !== a && prevB !== b) {
console.log("update!")
ref.current = { a, b }
}
}, [a, b])
return (
<>
<h1>{a + b}</h1>
<button onClick={(_) => setA((d) => d + 1)}>Chang A</button>
<button onClick={(_) => setB((d) => d + 1)}>Chang B</button>
</>
)
}
export default One
三、useRef
- useRef 两大作用:
- 获取DOM元素的节点,获取子组件的实例
- 渲染周期之间共享数据的存储(state不能存储跨渲染周期的数据,因为state的保存会触发组件重渲染)
- 语法: const ref = React.useRef(initialValue)
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue);返回的 ref 对象在组件的整个生命周期内保持不变。可以保存任何类型的值包括dom、对象等任何可辨值
3-1 获取dom元素节点
const RefDemo = () => {
const domRef = useRef(null)
useEffect(() => {
// domRef.current 指向已挂载到 DOM 上的文本输入元素
console.log("ref:deom-init", domRef, domRef.current)
})
return (
<div>
<div
onClick={() => {
console.log("ref:deom", domRef, domRef.current)
domRef.current.focus()
domRef.current.value = "hh"
}}
>
<label>这是一个dom节点</label>
<input ref={domRef} />
</div>
</div>
)
}
3-2 获取子组件实例
-
useImperativeHandle(ref,createHandle,[deps])可以自定义暴露给父组件的实例值。如果不使用,父组件的ref(chidlRef)访问不到任何值(childRef.current==null) -
useImperativeHandle应该与forwradRef搭配使用 -
因为函数组件没有实例。所以需要通过
React.forwardRef会创建一个 React 组件,这个组件能够将其接受的ref属性转发到其组件树下的另一个组件中 -
React.forward 接受渲染函数作为参数,React 将使用 prop 和 ref 作为参数来调用此函数
const Child = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
say: sayHello,
}))
const sayHello = () => {
alert("hello,我是子组件")
}
return <h3>子组件</h3>
})
const RefDemo = () => {
const childRef = useRef(null)
useEffect(() => {
console.log("ref:child-init", childRef, childRef.current)
})
const showChild = () => {
console.log("ref:child", childRef, childRef.current)
childRef.current.say()
}
return (
<div style={{ margin: "100px", border: "2px dashed", padding: "20px" }}>
<p onClick={showChild} style={{ marginTop: "20px" }}>
点我看子组件
</p>
<Child ref={childRef} />
</div>
)
}
3-3 渲染周期之间共享存储的数据
// 把定时器设置成全局变量使用 useRef 挂载到 current 上
import React, { useState, useEffect, useRef } from "react";
function App() {
const [count, setCount] = useState(0);
// 因为函数组件只要更新了,timer 就会被重新为 null,所以函数组件需要借助 useRef 存储变量
// const timer = null
// 把定时器设置成全局变量使用 useRef 挂载到 current 上
const timer = useRef();
// 首次加载 useEffect 方法执行一次设置定时器
useEffect(() => {
timer.current = setInterval(() => {
setCount(count => count + 1);
}, 1000);
}, []);
// count 每次更新都会执行这个副作用,当 count > 5 时,清除定时器
useEffect(() => {
if (count > 5) {
clearInterval(timer.current);
}
});
return <h1>count: {count}</h1>;
}
export default App;