React入门
useState
从 React 引入 useState
import { useState } from 'react'
使用时,可以传入初始值
const [count, setCount] = useState(0)
调用 useState 后返回两个参数:当前的 state(count),以及用于更新它的函数(setCount)
console.log(count) // 0
setCount(1)
当然 setCount 还可以接收一个函数
setCount((state) => state + 1)
需要注意的是 setCount 执行后,在本次函数执行上下文中,是获取不到最新的 state 值的
const handleClick = () => {
setCount(2)
console.log(count) // 0
}
原因很简单,函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量重新声明,所以改变的 state ,只有在下一次函数组件执行时才会被更新。
注意事项
在使用 useState 的更新函数更新 state 的时候,记得不要传入相同的 state,这样会使视图不更新。比如更新对象属性值
const [ state , dispatchState ] = useState({ name:'alien' })
const handleClick = ()=>{ // 点击按钮,视图没有更新。
state.name = 'Alien'
dispatchState(state) // 直接改变 `state`,在内存中指向的地址相同。
}
这是因为在 useState 的更新函数处理逻辑中,会浅比较两次 state ,发现 state 相同,不会开启更新调度任务; demo 中两次 state 指向了相同的内存空间,所以默认为 state 相等,就不会发生视图更新了。
解决问题: 把上述的 dispatchState 改成 dispatchState({...state}) 根本解决了问题,浅拷贝了对象,重新申请了一个内存空间。
useReducer
useReducer 和 useState 一样,都能创建一个双向绑定的变量。但是 useReducer 支持定义变量的修改方式。
function reducer(state, action) {
switch(action.type) {
case 'add':
return {
result: state.result + action.num
}
case 'minus':
return {
result: state.result - action.num
}
}
return state;
}
function App() {
const [res, dispatch] = useReducer(reducer, { result: 0 });
return (
<div>
<div onClick={() => dispatch({ type: 'add', num: 2 })}>加</div>
<div onClick={() => dispatch({ type: 'minus', num: 1 })}>减</div>
<div>{res.result}</div>
</div>
);
}
export default App
例如上面的例子,在定义 useReducer 值的时候,就定义好了该值的加减方式。
它还有另一种重载,通过函数来创建初始数据,这时候 useReducer 第二个参数就是传给这个函数的参数。
const [res, dispatch] = useReducer(reducer, 'zero', (param) => {
return {
result: param === 'zero' ? 0 : 1
}
})
注意事项
useReducer 和 useState 一样,在修改对象某个值时,需要返回新的对象才有效果。这是因为 React 推崇的是数据不可变。
这时要是对象太大太深,解构新对象也很麻烦怎么办?
return {
...state,
a: {
...state.a,
c: {
...state.a.c,
e: state.a.c.e + action.num,
},
},
}
这是我们可以借助一个第三方的库来解决这个问题 immer
npm install --save immer
用法相当简单,只有一个 produce 的 api
function reducer(state, action) {
switch(action.type) {
case 'add':
return produce(state, (state) => {
state.a.c.e += action.num
})
}
return state;
}
在 useState 上也可以
const [obj, setObj] = useState({ a: 1, b: { c: 2, d: 3 }})
setObj(produce(state, (state) => {
state.b.c += 5
}))
useEffect
从 React 引入 useEffect
import { useEffect } from 'react'
使用 useEffect
useEffect(() => {
console.log(1)
})
useEffect 接收两个参数 第一个是函数,第二个是依赖项。根据依赖项的不同,会有不同的效果。
不传依赖项
- 不传依赖项:每次渲染后都执行
- 依赖项为空数组:它第一次渲染之后执行
- 依赖项为非空数组:依赖项发生改变后执行
useEffect 清除机制 可以在传入的函数中 return 一个行的函数进行一些操作
useEffect(() => {
const test = setInterval(() => {
console.log(1)
})
return () => clearInterval(test)
})
useLayoutEffect
useLayoutEffect 与 useEffect 的相同点在于用户一致,而不同点在于:
- 执行时机是不同的。useLayoutEffect在当前帧paint流程之前,useEffect在当前帧paint流程之后
- useEffect callback 的执行是异步的,而 useLayoutEffect callback 的执行是同步的
- useEffect callback 里面的「状态更新是非批量的」(也就是说,会分配到不同的渲染帧里面),而useLayoutEffect callback 里面的「状态更新是批量」
useRef
useRef 功能与 vue 的 ref 一致,都是获取指定 dom。
const inputRef = useRef(null)
return (
<div>
<input ref={inputRef}></input>
</div>
)
需要注意的是变量名称需要和 dom 上定义的 ref 一致,并且 ref 的内容是保存在 current(inputRef.current) 属性上的。
forwardRef
如果父组件想获取子组件的 ref,那么就需要用到 forwardRef
import { useRef } from 'react';
import { useEffect } from 'react';
import React from 'react';
const Guang = (props, ref) => {
return <div>
<input ref={ref}></input>
</div>
}
const WrapedGuang = React.forwardRef(Guang);
function App() {
const ref = useRef(null);
useEffect(()=> {
console.log('ref', ref.current)
ref.current?.focus()
}, []);
return (
<div className="App">
<WrapedGuang ref={ref}/>
</div>
);
}
第一个参数为 props(父组件传递的参数对象),第二个参数为 ref
useImperativeHandle
但有的时候,我不是想把原生标签暴露出去,而是暴露一些自定义内容。
这时候就需要 useImperativeHandle 的 hook 了。
useImperativeHandle 接受三个参数:
- 第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
- 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
- 第三个参数 deps :依赖项 deps,依赖项更改形成新的 ref 对象。
import { useRef } from 'react';
import { useEffect } from 'react';
import React from 'react';
import { useImperativeHandle } from 'react';
const Guang = (props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
aaa() {
inputRef.current?.focus();
}
}
}, [inputRef]);
return <div>
<input ref={inputRef}></input>
</div>
}
const WrapedGuang = React.forwardRef(Guang);
function App() {
const ref = useRef(null);
useEffect(()=> {
console.log('ref', ref.current)
ref.current?.aaa();
}, []);
return (
<div className="App">
<WrapedGuang ref={ref}/>
</div>
);
}
useContext
跨任意层组件传递数据,我们一般用 Context。
import { createContext, useContext } from 'react';
const countContext = createContext(111);
function Aaa() {
return <div>
<countContext.Provider value={222}>
<Bbb></Bbb>
</countContext.Provider>
</div>
}
function Bbb() {
return <div><Ccc></Ccc></div>
}
function Ccc() {
const count = useContext(countContext);
return <h2>context 的值为:{count}</h2>
}
export default Aaa;
- 用 createContext 创建 context
- 在 Aaa 里面使用 xxxContext.Provider 修改它的值
- 然后在 Ccc 里面用 useContext 取出来
memo
memo 的作用是只有 props 变的时候,才会重新渲染被包裹的组件
function Aaa() {
const [, setNum] = useState(1)
useEffect(() => {
setInterval(() => {
setNum(Math.random())
}, 2000)
}, [])
return <div>
<MemoBbb count={2}></MemoBbb>
</div>
}
function Bbb(props) {
console.log('bbb render');
return <h2>{props.count}</h2>
}
const MemoBbb = memo(Bbb)
当组件 Aaa 每2秒触发重渲染时,通过使用 memo 包裹组件 Bbb,使组件 Bbb 并不会重渲染。也就是 bbb render 只会打印一次。
useMemo
如果我们想把 props 的值缓存起来,只有在依赖的值改变时才重新进行计算。这种优化有助于避免在每次渲染时都进行高开销的计算
function Aaa() {
const [count, setCount] = useState(2)
const count2 = useMemo(() => {
return count * 10;
}, [count]);
const add = () => {
setCount(count + 1)
}
const reduce = () => {
setCount(count-+ 1)
}
return <div>
<span onClick={add}>加</span><span onClick={reduce}>减</span>
<MemoBbb count={count2}></MemoBbb>
</div>
}
function Bbb(props) {
console.log('bbb render');
return <h2>{props.count}</h2>
}
const MemoBbb = memo(Bbb)
useMemo 要与 memo 共同使用才有效
useCallback
useMemo 缓存的是值,那么 useCallback 缓存的就是函数
function Aaa() {
const [count, setCount] = useState(2)
const count2 = useMemo(() => {
return count * 10;
}, [count]);
const bbbCallback = useCallback(function () {
// xxx
}, []);
return <div>
<span onClick={add}>加</span><span onClick={reduce}>减</span>
<MemoBbb count={count2} callbakc={bbbCallback}></MemoBbb>
</div>
}
function Bbb(props) {
console.log('bbb render');
return <h2>{props.count}</h2>
}
const MemoBbb = memo(Bbb)
如果在不使用 useCallback 情况下包裹函数
const bbbCallback = function () {
// xxx
}
那么 Bbb 组件每次都会被重新渲染,这是因为 Aaa 重新渲染时是会重新生成函数 bbbCallback
useCallback 要与 memo 共同使用才有效
组件通信
值传递
// 父组件
const [count, setCount] = useState(0)
return (
<div>
<Children count={count} />
</div>
)
// 子组件
function Children(props) {
return (
<button>
Clicked {props.count} times
</button>
)
}
当然我们也可以直接对 props 进行解构
// 子组件
function Children({ count }) {
return (
<button>
Clicked {count} times
</button>
)
}
同理,传递的值是函数的话,那么就可以实现子组件向父组件通信
// 父组件
const [count, setCount] = useState(0)
const addFn = () => { //定义一个函数
setCount(count + 1) //调用修改数据的方法
}
return (
<div>
<Children count={count} :add={addFn} />
</div>
)
// 子组件
function Children({ count, addFn }) {
return (
<button onClick={addFn}>
Clicked {count} times
</button>
)
}
跨组件
函数跨组件之间的传值主要有三个步骤:
- 导入并调用createContext方法,得到Context对象,导出
import { createContext } from 'react'
export const Context = createContext()
- 使用 Provider 组件包裹根组件,并通过 value 属性提供要共享的数据
return (
<Context.Provider value={ 这里放要传递的数据 }>
<根组件的内容/>
</Context.Provider>
)
- 在任意后代组件中,如果希望获取公共数据: 导入useContext;调用useContext(第一步中导出的context) 得到value的值
import React, { useContext } from 'react'
import { Context } from './index'
const 函数组件 = () => {
const 公共数据 = useContext(Context)
return ( 函数组件的内容 )
}
生命周期
生命周期的三个阶段
初始化阶段
由ReactDOM.render()触发---初次渲染
- constructor()
- getDerivedStateFromProps
- render()
- componentDidMount()
更新阶段
由组件内部this.setSate()或父组件重新render触发
- getDerivedStateFromProps
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate
- componentDidUpdate()
卸载组件
由ReactDOM.unmountComponentAtNode()触发
- componentWillUnmount()
生命周期详解
constructor
constructor 在类组件创建实例时调用,而且初始化的时候执行一次。通常用于初始化组件的状态和绑定方法。
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
在函数体中,需要先写super(props)
render
是用来返回组件的UI结构,它是一个纯函数,其中不应该包含任何副作用或改变状态的操作
import React,{ Component } from 'react'
export default class Hello extends Component{
render(){
return <h2>Hello,React!</h2>
}
}
componentDidMount
这个函数是在组件挂载到DOM后执行的,可以在这里获取数据、进行一些异步请求或DOM操作。
componentDidMount() {
// 发起API请求或其他初始化操作
fetchData().then(data => {
this.setState({ data
});
});
}
getDerivedStateFromProps
一个静态方法,用于在组件接收新的 props 时计算并返回新的 state
- nextProps 父组件新传递的 props
- prevState 传入 getDerivedStateFromProps 待合并的 state
class MyComponent extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
// 根据 nextProps 和 prevState 计算并返回新的 state
if (nextProps.value !== prevState.value) {
return { value: nextProps.value };
}
return null; // 如果不需要更新 state,返回 null
}
constructor(props) {
super(props);
this.state = {
value: props.value,
};
}
render() {
return <div>{this.state.value}</div>;
}
}
getSnapshotBeforeUpdate
它在组件更新(即将应用新props或state并重新渲染)之前触发。它允许你捕获组件更新前的一些信息,并在组件更新后使用这些信息。
- prevProps 更新前的props
- preState 更新前的state
getSnapshotBeforeUpdate(prevProps, prevState) {
// 捕获组件更新前的滚动位置
if (prevProps.items.length < this.props.items.length) {
const scrollHeight = this.myRef.current.scrollHeight;
const scrollTop = this.myRef.current.scrollTop;
return { scrollHeight, scrollTop };
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 使用snapshot来恢复滚动位置
if (snapshot !== null) {
this.myRef.current.scrollTop = snapshot.scrollTop + (this.myRef.current.scrollHeight - snapshot.scrollHeight);
}
}
TIP
- 触发时机:getSnapshotBeforeUpdate() 在render() 方法被调用后、组件DOM更新前触发,通常用于在更新前捕获一些DOM信息。
- 接收两个参数:这个生命周期方法接收两个参数:prevProps、prevState。你可以使用这些参数来比较前后的props和state。
- 返回值:getSnapshotBeforeUpdate() 方法应该返回一个值(通常是一个对象),它将成为componentDidUpdate() 方法的第三个参数。这个返回值通常用于保存一些DOM相关的信息,比如滚动位置。
- 通常和componentDidUpdate()一起使用:getSnapshotBeforeUpdate() 结合componentDidUpdate(prevProps, prevState, snapshot) 使用,snapshot参数是getSnapshotBeforeUpdate() 的返回值。你可以在componentDidUpdate() 中使用snapshot来执行DOM操作或其他一些操作。
componentDidUpdate
它在组件更新(即render() 方法执行后)后被调用。
- prevProps 更新之前的 props
- prevState 更新之前的 state
- snapshot 为 getSnapshotBeforeUpdate 返回的快照,可以是更新前的 DOM 信息
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.props.data !== prevProps.data) {
console.log('this.props中的数据变了');
}
// 使用getSnapshotBeforeUpdate()的返回值
if (snapshot !== null) {
console.log('Snapshot from getSnapshotBeforeUpdate:', snapshot);
}
}
componentWillUnmount
这个函数是在组件卸载前执行的,可以在这里进行一些清理工作,比如取消订阅、清除定时器、取消异步请求或移除事件监听器等。