Skip to content

React入门

useState

从 React 引入 useState

js
import { useState } from 'react'

使用时,可以传入初始值

js
const [count, setCount] = useState(0)

调用 useState 后返回两个参数:当前的 state(count),以及用于更新它的函数(setCount)

js
console.log(count) // 0

setCount(1)

当然 setCount 还可以接收一个函数

js
setCount((state) => state + 1)

需要注意的是 setCount 执行后,在本次函数执行上下文中,是获取不到最新的 state 值的

js
const handleClick = () => {
    setCount(2)
    console.log(count) // 0
}

原因很简单,函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量重新声明,所以改变的 state ,只有在下一次函数组件执行时才会被更新。

注意事项

在使用 useState 的更新函数更新 state 的时候,记得不要传入相同的 state,这样会使视图不更新。比如更新对象属性值

js
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 支持定义变量的修改方式。

js
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 第二个参数就是传给这个函数的参数。

js
const [res, dispatch] = useReducer(reducer, 'zero', (param) => {
    return {
        result: param === 'zero' ? 0 : 1
    }
})

注意事项

useReducer 和 useState 一样,在修改对象某个值时,需要返回新的对象才有效果。这是因为 React 推崇的是数据不可变。

这时要是对象太大太深,解构新对象也很麻烦怎么办?

js
return {
    ...state,
    a: {
        ...state.a,
        c: {
            ...state.a.c,
            e: state.a.c.e + action.num,
        },
    },
}

这是我们可以借助一个第三方的库来解决这个问题 immer

npm install --save immer

用法相当简单,只有一个 produce 的 api

js
function reducer(state, action) {
    switch(action.type) {
        case 'add':
            return produce(state, (state) => {
                state.a.c.e += action.num
            })
    }
    return state;
}

在 useState 上也可以

js
const [obj, setObj] = useState({ a: 1, b: { c: 2, d: 3 }})

setObj(produce(state, (state) => {
    state.b.c += 5
}))

useEffect

从 React 引入 useEffect

js
import { useEffect } from 'react'

使用 useEffect

js
useEffect(() => {
    console.log(1)
})

useEffect 接收两个参数 第一个是函数,第二个是依赖项。根据依赖项的不同,会有不同的效果。
不传依赖项

  1. 不传依赖项:每次渲染后都执行
  2. 依赖项为空数组:它第一次渲染之后执行
  3. 依赖项为非空数组:依赖项发生改变后执行

useEffect 清除机制 可以在传入的函数中 return 一个行的函数进行一些操作

js
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。

js
const inputRef = useRef(null)

return (
    <div>
        <input ref={inputRef}></input>
    </div>
)

需要注意的是变量名称需要和 dom 上定义的 ref 一致,并且 ref 的内容是保存在 current(inputRef.current) 属性上的。

forwardRef

如果父组件想获取子组件的 ref,那么就需要用到 forwardRef

js
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 对象。
js
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。

js
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;
  1. 用 createContext 创建 context
  2. 在 Aaa 里面使用 xxxContext.Provider 修改它的值
  3. 然后在 Ccc 里面用 useContext 取出来

memo

memo 的作用是只有 props 变的时候,才会重新渲染被包裹的组件

js
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 的值缓存起来,只有在依赖的值改变时才重新进行计算。这种优化有助于避免在每次渲染时都进行高开销的计算

js
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 缓存的就是函数

js
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 情况下包裹函数

js
const bbbCallback = function () {
        // xxx
}

那么 Bbb 组件每次都会被重新渲染,这是因为 Aaa 重新渲染时是会重新生成函数 bbbCallback

useCallback 要与 memo 共同使用才有效

组件通信

值传递

js
// 父组件
const [count, setCount] = useState(0)

return (
    <div>
        <Children count={count} />
    </div>
)

// 子组件
function Children(props) {
    return (
        <button>
            Clicked {props.count} times
        </button>
    )
}

当然我们也可以直接对 props 进行解构

js
// 子组件
function Children({ count }) {
    return (
        <button>
            Clicked {count} times
        </button>
    )
}

同理,传递的值是函数的话,那么就可以实现子组件向父组件通信

js
// 父组件
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>
    )
}

跨组件

函数跨组件之间的传值主要有三个步骤:

  1. 导入并调用createContext方法,得到Context对象,导出
js
import { createContext } from 'react'
export const Context = createContext()
  1. 使用 Provider 组件包裹根组件,并通过 value 属性提供要共享的数据
js
return (
    <Context.Provider value={ 这里放要传递的数据 }>
            <根组件的内容/>
    </Context.Provider>
)
  1. 在任意后代组件中,如果希望获取公共数据: 导入useContext;调用useContext(第一步中导出的context) 得到value的值
js
import React, { useContext } from 'react'
import { Context } from './index'
const 函数组件 = () => {
    const 公共数据 = useContext(Context)
    return ( 函数组件的内容 )
}

生命周期

生命周期

生命周期的三个阶段

初始化阶段

由ReactDOM.render()触发---初次渲染

  1. constructor()
  2. getDerivedStateFromProps
  3. render()
  4. componentDidMount()

更新阶段

由组件内部this.setSate()或父组件重新render触发

  1. getDerivedStateFromProps
  2. shouldComponentUpdate()
  3. render()
  4. getSnapshotBeforeUpdate
  5. componentDidUpdate()

卸载组件

由ReactDOM.unmountComponentAtNode()触发

  1. componentWillUnmount()

生命周期详解

constructor

constructor 在类组件创建实例时调用,而且初始化的时候执行一次。通常用于初始化组件的状态和绑定方法。

js
constructor(props) { 
    super(props);
    this.state = { count: 0 }; 
    this.handleClick = this.handleClick.bind(this); 
}

在函数体中,需要先写super(props)

render

是用来返回组件的UI结构,它是一个纯函数,其中不应该包含任何副作用或改变状态的操作

js
import React,{ Component } from 'react'

export default class Hello extends Component{
	render(){
		return <h2>Hello,React!</h2>
	}
}

componentDidMount

这个函数是在组件挂载到DOM后执行的,可以在这里获取数据、进行一些异步请求或DOM操作。

js
componentDidMount() { 
    // 发起API请求或其他初始化操作 
    fetchData().then(data => { 
        this.setState({ data 
        }); 
    }); 
}

getDerivedStateFromProps

一个静态方法,用于在组件接收新的 props 时计算并返回新的 state

  • nextProps 父组件新传递的 props
  • prevState 传入 getDerivedStateFromProps 待合并的 state
js
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
js
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 信息
js
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

这个函数是在组件卸载前执行的,可以在这里进行一些清理工作,比如取消订阅、清除定时器、取消异步请求或移除事件监听器等。