React从入门到出门

131 阅读12分钟

React从入门到出门

本文主要介绍函数组件及其常用hook

声明式

jsx

  • 只能返回一个根元素(底层被转化为了对象,不能在一个函数中返回多个对象),可以在外层包裹Fragment(空标签)一般写为<></>,如果要给该标签传值,需调用:
import {Fragment} from 'react'
<Fragment key={}>
</Fragment>
  • 混入JS表达式时使用{}。
  • 标签必须闭合。
  • 驼峰命名法给属性命名(className)。
  • 换行用()包裹。

组件

封装起来具有独立功能的UI部件。 React的一切都是基于组件的,将页面划分为功能独立,逻辑完整,功能可复用的单元。可组合、可维护、可重用。

  • React应用程序由组件构成,首字母大写。标签字母小写的React解析为HTML标签,标签首字母大写的才会被视为组件。
  • 使用组件,通过重用同一组件,提高开发效率,通过小组件、大组件的使用进行开发。

组件的本质就是类和函数,但是与常规类和函数不同的是,组件承载了渲染视图的UI和更新视图的方法。

函数组件

函数组件的每一次更新,都是把函数重新执行:

  • 产生一个新的闭包。
  • 在闭包中所有创建函数的操作,都会:重新创建新的堆内存(也就是函数都会重新创建)。
  • 不要尝试给函数组件prototype绑定属性或方法,即使绑定了也没有任何作用,因为React对函数组件的调用,是采取直接执行函数的方式,而不是new。

Props

  • 通过Props进行组件通信。
  • Props是组件的唯一参数。
  • 可以使用展开语法转发props,但不要过度使用。
    function Parent (props) {     
        return (
            <>
                <Child name={'A'} age={12}/>
                <Child {...props}/>
            </>
        )
    }

    function Child ({name='a', age}) {
        return (
            <div>{name}-{age}</div>
        )
    }
    function GrandParent () {
        return (
            <Parent name={"a"} age={10}/>
        )
    }

纯函数

仅执行计算操作,不做其他操作。

  • 只负责自己的任务。
  • 输入相同,则输出相同。

Hook

  • 只在最顶层使用

useState

为组件添加状态,根据先前的state更新state。

import {useState} from 'react';
const [count, setCount] = useState(0);
const incrementCount = () => {
    setCount(count + 1);
}
return (
    <div>
        <p>{count}</p>
        <button onClick={incrementCount}>+</button>
    </div>
)
  1. State用于保存渲染间数据。

  2. State setter 更新变量并触发React再次渲染组件。

  3. state完全私有于声明它的组件。

  4. 将state视为只读。

    1. 对于对象,改变其state时对其结构然后进行属性覆盖。
    2. 对于数组,避免使用能够改变原数组的方法,(push、pop等)应对其进行拷贝后再操作。
    const [student, setStudent] = setState({name:'A', age: 9})
    function addAge () {
        setStudent({
            ...student,
            age: 19
        })
    }
  1. 状态提升:多个组件状态同步修改时,将状态移到最近的父级,通过props状态传递给组件。
  2. 调用set不会更新已经运行代码中的状态变量。更新函数可以获取待定状态并从中计算下一个状态。
function increment () {    
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
}
//只会+1

function increment () {
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
}
//会+3

Tip

在更新state时,如果传入了相同的state,视图不会更新。

  • 处理逻辑中,会浅比较两次的state

如:当需要更改对象中的属性时,应该将对象浅拷贝

export default function App () {
    const [wdy, setWdy] = useState({name: 'wdy'});
    const handleClick = () => {
        wdy.name = 'WDY';
        setWdy(wdy)    //×
        setWdy({...wdy}) //√
    }
    return <button onClick={handleClick}>change</button>
}

类组件的setState和函数组件的useState的异同:

相同点:

  • setState和useState更新视图,底层都调用了scheduleUpdateOnFiber方法,而且事件驱动情况下都有批量更新规则。

不同点:

  • 在不是pureComponent组件模式下,setState不会浅比较两次state的值,只要调用setState,在没有其他优化手段的前提下,就会执行更新。但是useState中的dispatchAction会默认比较两次state是否相同,然后决定是否更新组件。
  • setState有专门监听state变化的回调函数,可以获取最新的state;但是在函数组件中,只能通过useEffect来执行state变化引起的副作用。
  • setState在底层处理逻辑上主要和老state进行合并处理,而useState更倾向于重新赋值。

useReducer

处理复杂状态。 参数一:纯函数(state,action)

参数二:初始化state

参数三(可选):用于计算初始值的函数

import {useReducer} from 'react';
function reducer (state, action) {
    switch (action.type) {
        case 'increment':
            return state + 1;
        case 'decrement':
            return state - 1;
        default :
            throw new Error();
    }
}
function double (x) {
    return x*2;
}
function Counter ({a}) {
    const [count, dispatch] = useReducer(reducer, a, double);

    return (
        <div>
            <p>count: {count}</p>
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </div>
    )
}

useContext

将信息深入传递,无需手动将porps层层传递。

import {createContext} from 'react';
export const CountContext = createContext(0);

import {countContext, useContext} from '';
export default function Counter () {
    const count = useContext(CountContext);
}

import {CountContext} from '';
export default function AnotherCounter(count) {
    <CounterContext.Provider value={count}>
    </CounterContext.Provieder>
}
  • 正常传递porps,可以更清晰看到哪些组件用了哪些数据。
  • 抽象组件并将jsx作为children传递。 以上两种方案均不适合,考虑useContext。
  • 使用createContext来创建context对象,使用Provider修改其中的值,function组件使用useContext的hook来取值,class组件使用Consumer来取值。

useRef

引用一个不需要渲染的值,返回一个可变的ref对象,该对象可以在整个组件的生命周期内保留数据。

函数组件每一次render,函数上下文会重新执行,那么有一种情况就是,在执行一些事件方法改变数据或者保存新数据的时候,有没有必要更新视图,有没有必要把数据放到state中。如果视图层更新不依赖想要改变的数据,那么state改变带来的更新效果就是多余的。

useRef可以创建出一个ref原始对象,只要组件没有销毁,ref对象就一直存在。

参数一:ref对象的current属性的初始值。

import {useRef} from 'react';
funciton TextInput () {
    const inputRef = useRef();

    function focusInput = () {
        inputRef.current.focus();
    }

    return (
        <div>
            <input type="text" ref={inputRef}/>
            <button onClick={focusInput}>Fouc</button>
        </div>
    )
}
  • 改变ref不会触发重新渲染

useEffect

脱围机制。组件与外部系统同步,执行副作用。 参数一:setup函数,当组件被添加到DOM时,setup函数将运行。 参数二(可选):setup代码中引用的所有响应式值的列表。

  • 请求数据、定时器等异步逻辑,通常放入useEffect中。

  • 根据传入依赖项的不同,会有不同的执行表现:

    • 没有依赖项 组件初始渲染 + 组件更新执行
    • 空数组依赖 只在初始渲染时执行一次
    • 添加特定依赖项 组件初始渲染 + 特性依赖项发生变化时执行
import {useEffect} from 'react';
useEffect (() => {
    // setup

    return () => {
        //cleanup 组件卸载时自动执行
    }
}, [依赖项数组])

function App () {
    const [count, setCount] = useState(0);
    //没有依赖项 组件初始渲染 + 组件更新执行
    useEffect (() => {
        console.log('执行');
    })
    //空数组依赖 只在初始渲染时执行一次
    useEffect (() => {
        console.log('执行');
    },[])
    //添加特定依赖项 组件初始渲染 + 特性依赖项发生变化时执行
    useEffect (() => {
        console.log('执行');
    },[count])
    
    return (
        <div>
            <button onClick=(() => setCount(count + 1))>+{count}</button>
        </div>
    )
}
  • 工作原理

    • 调度副作用:当组件内部调用useEffect时,实际上是将一个副作用函数及其依赖项数组排队等待执行,这个函数并不会立即执行。
    • 提交阶段(Commit Phase):React渲染组件并且执行了所有的纯函数组件或类组件的渲染方法后,会进入到所谓的提交阶段,在这个阶段,React将计算出的新视图(新的DOM节点)更新到屏幕上,一旦这个更新完成,React就知道现在可以安全地执行副作用函数了,因为这不会影响到正在屏幕上显示的界面。
    • 副作用执行:提交阶段完成后,React会处理所有排队的副作用。如果组件是首次渲染,所有的副作用都会执行。如果组件是重新渲染,React会首先对比副作用的依赖项数组:如果依赖项未变,副作用不会执行;如果依赖项改变或没有提供依赖项:副作用再次执行。
    • 清理机制:如果副作用函数返回了一个函数,那么这个函数将被视为清理函数。在执行当前副作用之前,以及组件卸载前,React会先调用上一次渲染中的清理函数。这样确保了不会有内存泄漏,同时能撤销上一次副作用导致的改变。
    • 延迟副作用:尽管useEffect会在渲染之后执行,但是它是异步执行的,不会阻塞浏览器更新屏幕。这意味着React会等待浏览器完成绘制后,在执行副作用函数,以此来确保副作用处理不会导致用户可见的延迟。
    • 这种机制,useEffect允许开发者以一种优化的方式来处理组件中可能存在的副作用,而不需要关心渲染的具体时机。退出清理功能确保了既是组件被多次快速创建和销毁,应用程序也能保持稳定和性能。

useLayoutEffect

参数一:setup函数。

参数二:setup代码中引用的所有响应式值的列表。

与useEffect区别:useEffect在DOM更新之后异步执行,useLayOutEffect在DOM更新之前同步执行。

useInsertionEffect

专为CSS-in-JS打造,只有在使用CSS-in-JS库并且需要注入样式才使用。

可以在布局副作用触发之前将元素插入到DOM中。

useMemo

防止重新计算的频率过高。每次重新渲染时能够缓存计算的结果。

const cachedValue = useMemo(create,deps)

参数一create:要缓存计算值的函数。

参数二deps:所有在参数一的函数中使用的响应式变量组成的数组。

返回值cachedValue:执行create的返回值。

  • 函数组件每次更新,都是把函数重新执行:

    • 产生新的闭包。
    • 内部代码重新执行。
  • 在函数每一次重新执行的时候,如果依赖的状态值没有发生改变,此操作逻辑不去执行,只有依赖值发生变化,我们再去执行即可。

    • 第一次渲染组件的时候,callback会执行。
    • 后期只有依赖的状态值发生改变,callback才会执行。
    • 每次会把callback执行的返回结果赋值给xxx。
    • 具备缓存效果(计算缓存),在依赖的状态值没有发生改变,callback没有触发执行的时候,xxx获取的是上一次计算出来的结果。
  • 优化的Hook函数

    • 如果函数组件中,有消耗性能的和时间的计算操作,则尽可能用useMemo缓存起来,设置对应的依赖。
    • 这样可以保证,当非依赖的状态发生改变,不会去处理一些没必要的操作,提高组件的更新状态。
import {useMemo, useState} from 'react';
function Counter ({a, b}) {
    const [count, setCount] = useState(0);
    const value = useMemo(() => {
        return a + b;
    }, [a, b])

    return (
        <div>
            <p>{a = ${a} b = ${b} count = ${count}}</p>
            <p>{value = ${value}}</p>
            <button onClick={() => setCount(count + 1)}>+</button>
        </div>
    )
}

function App () {
    return (
        <Counter a={1} b={1}/>
    )
}

useMemo会记录上一次执行create的返回值,并把它绑定在函数组件对应的fiber对象上,只要组件不销毁,缓存值就一直存在,但是deps中如果有一项改变,就会重新执行create,返回值作为新的值记录在fiber对象上。

useCallback

减少组件重新渲染次数,防止回调函数被无限制地创建。

参数一:回调函数。

参数二:依赖项数组。

const xxx = useCallback(callback, [dependencies])
  • 组件第一次渲染,useCallback执行,创建一个函数callback,赋值给xxx。

  • 组件后续每一次更新,判断依赖的状态值是否改变:

    • 如果改变,则重新创建新的函数堆,赋值给xxx。
    • 如果没有改变或者没有设置依赖值,则xxx获取的一直是第一次创建的函数,不会创建新的函数出来。
    • 或者说,基于useCallback,可以始终获取第一次创建函数的堆内存地址(或者说函数引用)。
  • useCallback不要滥用,并不是所有组件内部的函数,都拿其处理会更好。

    • 虽然减少了堆内存的开辟。
    • 但是useCallback本身也有自己的处理逻辑和缓存的机制,也会消耗时间。
  • 使用范围:

    • 父组件嵌套子组件,父组件要把一个内部的函数,基于属性传递给子组件,此时传递的这个方法,基于useCallback处理一下会更好。
    • 函数组件用React.memo包起来,类组件使用React.PureComponent
import { useState, useCallback } from 'react';
function App() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

useImperativeHandle

自定义由ref暴露出来的句柄

参数一ref:接受forwardRef传递过来的ref

参数二createHandle:处理函数,返回值作为暴露给父组件的ref对象

参数三deps:依赖项deps,依赖项更改形成新的ref对象

自定义Hook

js函数,可以使用已经存在的Hooks,通常情况以use开头。

//自定义

import {useState} from 'react';
function useCounter (initialCount) {
    const [count, setCount] = useState(initialCount);
    const increment = () => setCount(count + 1);
    const decrement = () => setCount(count - 1);

    return {count, increment, decrement}
}
export default useCounter;

//使用
import useCounter from '';
function Counter () {
    const [count, increment, decrement] = useCounter(0);
    return
    <div>
        <p>Count: {count}</p>
        <button onClick={increment}>+</button>
        <button onClick={decrement}>-</button>
    </div>
}

Api

e.stopPropagation

阻止事件转播

e.preventDefault

阻止浏览器默认行为

React.createElement

创建React元素

ReactDOM.createRoot

创建根容器,用来存放React元素

forwardRef

  • 允许组件使用ref将DOM节点暴露给父组件
  • 在多个组件中转发ref
  • 暴露命令式句柄而非DOM节点
const SomeComponent = forwardRef(render);

高阶组件 HOC

高阶组件是一个函数,接收一个组件作为参数,并返回一个新的增强组件。这个增强组件可以包含一些共用逻辑和状态,用于实现代码复用和抽象。

解决大量的代码复用,逻辑复用问题

  • 一般以with开头
  • 不在render中使用HOC
  • 尽可能透明地操作原始组件
import React from 'react';

function withLoading(Component) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (!isLoading) return <Component {...props} />;
    return <div>Loading...</div>;
  }
}

function MyComponent({ data }) {
  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

const MyComponentWithLoading = withLoading(MyComponent);

function App() {
  const [isLoading, setIsLoading] = React.useState(true);
  const [data, setData] = React.useState([]);

  React.useEffect(() => {
    //获取data isLoading
  }, []);

  return (
    <div>
      <MyComponentWithLoading isLoading={isLoading} data={data} />
    </div>
  );
}