React基础知识汇总

238 阅读10分钟

React默认: 父组件有更新,子组件则无条件更新

核心概念

setState

setState()会通知React重新渲染组件及其子组件。setState()视为请求而不是立即更新组件的命令。为了优化性能,React会延迟调用它,并通过一次传递更新多个组件。React并不会保证state的变更立即生效,它会批量推迟更新。

setState(partialState, callback)

  • partialState:object|function-用于产生与当前state合并的子集, 若是function 则接收上一次的state和props
  • callback: state更新之后被调用,能够获取最新的state值

使用setState需要注意的三件事

不能直接修改State

  如果需要更改state的值并重新渲染组件,需要使用setState进行更新。构造函数式唯一可以给this.state赋值的地方.

// 错误写法: 能够改变count的值,但是无法重新渲染组件
this.state.count = 2;

// 正确写法: 能够修改count的值并重新渲染组件
this.setState({ count: 2 });

State的更新可能是异步的

  出于性能考虑,React可能会把多个setState()调用合并成一个调用。

  • 在组件生命周期或React合成事件中,setState是异步;(异步操作,但特定情况下只会执行一次,性能被优化)
  • 在setTimeout或者原生dom事件中,setState是同步;(完全就是同步函数)
this.setState({ count: 2 });
console.log(count); // 依然是0,无法直接获取最新值

若想要获取最新值有以下三个方式:

  • 在回调中获取状态值
this.setState(() => ({
    count: 2
}), () => {
    console.log(this.state.count); // 2
});
  • setTimeout(React 18中无法使用)

    setTimeout()和原生dom事件都是属于js线程中的异步操作(涉及eventLoop和React源码)参考地址

    • 这两个函数里面传入的回调函数都会在相应的时候被加入到消息队列
    • 然后代码中同步函数执行完的时候再被调用执行
setTimeout(() => {
    this.setState({
       count: 2
     });
    console.log('setTimeout', this.state.count);
}, 0)
  • 原生事件中修改状态(React 18中无法使用)
 componentDidMount() {
    document.getElementById('host').addEventListener('click', () => {
       this.setState({
         count: 2
       });
       console.log('count', this.state.count);
    });
 }

image.png

State的更新会被合并

  • 多次使用setState更新state中 相同属性
    • partialStateObject,多次更新只会实现最后一次设置,React源码中会使用Object.assign(target, ...source)进行合并,只取最后一次的值

    • partialStatefunction,多个state进行合并时,每次遍历,都会执行一次partialState

setCounter = val => {
    // partialState为Object,每次handleClick执行时,count只会加2
    this.setState({
      count: this.state.count + val
    });
    
    // partialState为function,每次handleClick执行时,count会将每次的函数执行结果叠加
    // ! partialState中数据必须以上次为基础
    // correct
    // this.setState((prevState) => ({
    //  count: prevState.count + val
    // }));
    
    // wrong state是异步获取的
    // this.setState(() => ({
    //  count: this.state.count + val
    // }));
  }

handleClick = () => {
   this.setCounter(1); // 多次执行修改同一个属性,会被合并执行 只会执行一次
   this.setCounter(2); // 最终每次只会 + 2,不会加 3
}

  若不想多次执行被合并,则在setState中第一个参数 partialState 使用 function,将对象进行return。每次的function不会被合并。

class SetStatePage extends Component {
  state: {
    counter: 0
  }

  setCounter = v => {
    // 若setState是一个函数,会单独执行每一个当前值参数的更新 链式更新
    this.setState(state => {
      return {
        counter: state.counter + v
      }
    });
  }
  
  changeCounter () {
     setCounter(1);
     setCounter(2);
  }
  
  render() {
    return <button onClick={this.
    
    changeCounter () {
    
    }}>{counter}</button>;
  }
}

生命周期

image.png

Hooks

  Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数

  Hook就是JavaScript函数,但是使用它们有两个额外的规则

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用

useState

const [state, setState] = useState(initialState);

useState不同与class组件中的setState, useState更新state变量总是 替换 而不是 合并

 若新的state需要通过原先的state计算得出:

  • 那么可以将函数传递给setState。 该函数接收先前的state,返回一个更新后的值
  • 使用 useReducer
import { useState } from "react";

export default function SetStatePage() {
  const [user, setUser] = useState({ name: "焦糖瓜子", age: 18 });

  function handleClick() {
    setUser(({ age }) => ({ age: age + 1 }));
    console.log('user', user); // { age: 19 }, name属性会丢失
    // 不会因为对象被替换而导致属性丢失
    // setUser(user => ({ ...user, age: user.age + 1}));
  }

  return (
    <div>
      我的名字: {user.name}, 青春正好,年仅{user.age}
      <button onClick={handleClick}>年复一年</button>
    </div>
  );
}

useEffect

Effect Hook 可以让你在函数组件中执行副作用操作,useEffect Hook可以看做componentDidMountcomponentDidUpdatecomponentWillUnmount这三个生命周期函数的组合

useEffect会在每次渲染之后执行,React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。React保证了每次运行effect的同时,DOM都已经更新完毕

  在React组件中有两种常见的副作用操作: 需要清除的和不需要清除的。

  • 需要清除的副作用: 在useEffect中返回一个函数,React会在组件卸载的时候执行清除操作。React 会在每次执行当前 effect 之前对上一个 effect 进行清除

useLayoutEffect

  其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新

useEffectuseLayoutEffect 的区别: useEffect是异步执行,在浏览器渲染之后执行。useLayoutEffect是同步执行,在浏览器渲染之前更新,会阻塞了浏览器的绘制

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useCallback返回一个 缓存的回调函数

useMemo的区别: 1、useCallback组件初次渲染不会执行。2、两者缓存不一样,useCallback缓存的是函数,useMemo缓存的是值

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo缓存的是一个值,传入useMemo的函数会在渲染期间执行

高级特性

函数组件

  • 纯函数, 输入props, 输出JSX
  • 没有实例,没有生命周期,没有state
  • 不能扩展其他方法
  • 函数组件中可以使用hooks

非受控组件

在一个受控组件中,表单数据是由React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理

refs

defaultValue defaultChecked

手动操作DOM元素

非受控组件使用场景

  • 必须手动操作DOM元素,setState实现不了
  • 文件上传 <input type='file'>
  • 某些富文本编辑器,需要传入DOM元素

受控组件 vs 非受控组件:优先使用受控组件,符合React设计原则。 必须操作DOM元素时,再使用非受控组件

Protals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案

  一个 portal 的典型用例是当父组件有 overflow: hiddenz-index 样式时,但你需要子组件能够在视觉上 “跳出” 其容器。例如,对话框、悬浮卡以及提示框。

  尽管 portal 可以被放置在 DOM 树 中的任何地方,但在任何其他方面,其行为和普通的 React 子节点 行为一致。由于 portal 仍存在于 React 树, 且与 DOM 树 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的

import { Component } from "react";
import ReactDOM from "react-dom";

export default class PortalsPage extends Component {
  render() {
    // 正常渲染
    // return <div className="model"> {this.props.children} </div>;

    // 使用portals渲染到body上
    return ReactDOM.createPortal(
      <div className="model"> {this.props.children} </div>,
      document.body // 需要渲染到的 DOM节点
    );
  }
}

Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法

API

Context.createContext

export const themeColor = React.createContext('red');

  创建一个Context对象。当React渲染了订阅这个Context对象的组件,这个组件会从组件树离自身最近匹配Provider中读取当前的context值。若组件所处的树种没有匹配到Provider时,其defaultValue参数才会生效(即Provider没有提供value值)。

React-Router源码中,route获取最近匹配的Provider,给useRouteMatch和useParams提供最近的RouterContext)

Context.contextType

image.png

  挂载在class上的contextType属性会被重赋值为一个由React.createContext()创建的Context对象。此属性可以使用this.context来消费最近Context上的那个值。通过contextType只能够订阅单一Context,若想订阅多个需要使用Consumer

Context.Provider

<ThemeContext.Provider value={color: 'gold'}>

image.png

  Provider接收一个value属性传递给消费组件,一个Provider可以和多个消费组件对应,多个ProVider也可以嵌套使用,里层数据会覆盖外层数据。

  当Provider的value值变化时,它内部所有消费组件都会重新渲染。注意, 若传递对象给value时,防止每次渲染都重新渲染消费组件,value状态可以提升到父节点的state中

Context.Consumer

<ThmemContext.Consumer>
{
 value => ()
}
</ThmemContext.Consumer>

  一个 React 组件可以订阅 context 的变更,此组件可以让你在函数式组件中可以订阅context

  这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的Provider提供的value值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue

image.png

异步组件

  • import 异步加载js模块
  • React.lazy   React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。以下代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包。
// 使用之前
import OtherComponent from './OtherComponent';

// 使用之后
const OtherComponent = React.lazy(() => import('./OtherComponent'));
  • React.Suspense   在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做 优雅降级
import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </div>
  );
}

  fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个懒加载组件

  

性能优化

React默认: 父组件有更新,子组件则无条件更新

shouldComponentUpdate (SCU)

此方法仅作为 性能优化的方式 而存在。不要企图依赖此方法"阻止"渲染。 应该 优先考虑使用内置的 PureComponent 组件

  根据shouldComponentUpdate的返回值,判断 React组件 的输出是否受当前stateprops更改的影响。 默认行为是 state每次发生变化组件都会重新渲染。 不建议shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会 损害性能

import React, { Component } from "react";

export default class ShouldComponentUpdatePage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0,
      name: "function",
    };
  }

  handleCounter = () => {
    this.setState({
      counter: this.state.counter + 1,
    });
  };

  handleName = () => {
    this.setState({
      name: this.state.name,
    });
  };

  render() {
    const { counter } = this.state;
    return (
      <div>
        ShouldComponentUpdatePage - {counter}
        <button onClick={this.handleName}>修改name</button>
        <button onClick={this.handleCounter}>修改counter</button>
        <FunctionChildren name={this.state.name} />
        {/* 没有被 React.memo包裹的组件,每次父组件更新 子组件也会更新 */}
        {/* <ChildrenPage /> */}
        {/* React.memo包裹的组件,不会重复进行渲染 */}
        {/* <MemoedChildrenPage /> */}
      </div>
    );
  }
}

class FunctionChildren extends Component {
  componentDidUpdate() {
    console.log("FunctionChildren Update");
  }

  /* shouldComponentUpdate函数返回 false 则组件不会重新渲染 */
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.name === nextProps.name) return false;
    return true;
  }

  render() {
    return <div>props-{this.props.name}</div>;
  }
}

PureComponent

PureComponent 会对 propsstate 进行 浅层比较 ,并减少了跳过必要更新的可能性

import React, { Component } from "react";

export default class ShouldComponentUpdatePage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0,
      name: "function",
    };
  }

  handleCounter = () => {
    this.setState({
      counter: this.state.counter + 1,
    });
  };

  handleName = () => {
    this.setState({
      name: this.state.name,
    });
  };

  render() {
    const { counter } = this.state;
    return (
      <div>
        ShouldComponentUpdatePage - {counter}
        <button onClick={this.handleName}>修改name</button>
        <button onClick={this.handleCounter}>修改counter</button>
        <FunctionChildren name={this.state.name} />
        {/* 没有被 React.memo包裹的组件,每次父组件更新 子组件也会更新 */}
        {/* <ChildrenPage /> */}
        {/* React.memo包裹的组件,不会重复进行渲染 */}
        {/* <MemoedChildrenPage /> */}
      </div>
    );
  }
}

class FunctionChildren extends React.PureComponent {
  componentDidUpdate() {
    console.log("FunctionChildren Update");
  }

  // shouldComponentUpdate(nextProps, nextState) {
  //   if (this.props.name === nextProps.name) return false;
  //   return true;
  // }

  render() {
    return <div>props-{this.props.name}</div>;
  }
}

React.memo

  函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果

import React, { Component } from "react";

export default class ShouldComponentUpdatePage extends Component {
  constructor (props) {
    super(props);
    this.state = {
      counter: 0
    }
  }
  
  handleCounter = () => {
    this.setState({
      counter: this.state.counter + 1
    })
  }

  render() {
    const { counter } = this.state;
    return <div>
      ShouldComponentUpdatePage - {counter}
      <button onClick={this.handleCounter}>修改counter</button>
      {/* 没有被 React.memo包裹的组件,每次父组件更新 子组件也会更新 */}
      <ChildrenPage />
      {/* React.memo包裹的组件,不会重复进行渲染 */}
      <MemoedChildrenPage />
    </div>
  }
}

function ChildrenPage() {
  console.log('ChildrenPage update');
  return <div>我是底部栏</div>
}

const MemoedChildrenPage = React.memo(ChildrenPage)

immutable.js

HOC高阶组件

高阶组件实际上是一个高阶函数,接收一个组件作为参数,返回一个新组建

Render Props