React深入学习

416 阅读7分钟

setState

setState是同步更新的,但是表现为异步,为什么设计成异步的呢?

  • 如果每次调用 setState 都进行一次更新, 那么意味着 render 函数会被频繁的调用界面重新渲染, 这样的效率是很低的

最好的方法是获取到多个更新, 之后进行批量更新

  • 如果同步更新了 state, 但还没有执行 render 函数, 那么stateprops不能保持同步

stateprops不能保持一致性, 会在开发中产生很多的问题

总结:setState设计为异步, 可以显著的提高性能

如何获取异步更新后的值?

  • 方式一: setState的回调

    • setState接收两个参数: 第二个参数是回调函数(callback), 这个回调函数会在state更新后执行

    import React from "react"; class App extends React.Component { constructor(props) { super(props); this.state = { name: "nihao", }; } shouldComponentUpdate(nextProps, nextState, nextContext) { console.log("hello", this.state.name);//方式二 return true; }

    render() { return (

    {this.state.name}

    <button onClick={() => { this.setState({ name: "liweike" }, () => { console.log(this.state.name);//方式一 }); }} > 点我更新值
    ); } } export default App;

  • 方式二: componentDidUpdate生命周期函数

setState表现出来一定是异步的吗?

1、在组件生命周期或者React合成事件中,setState是异步的

2、在setTimeout或者原生DOM事件中,setState是同步的

验证一:在setTimeout的更新——>同步更新

import React from "react";
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "nihao",
    };
  }

  render() {
    return (
      <div>
        <h1>{this.state.name}</h1>
        <button
          onClick={() => {
            setTimeout(() => {
              this.setState({ name: "liweike" });
              console.log(this.state.name);//打印liweike
            });
          }}
        >
          点我更新值
        </button>
      </div>
    );
  }
}
export default App;

验证二:在原生DOM事件——>同步更新

import React from "react";
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "nihao",
    };
  }
  componentDidMount() {
    document.getElementById("btn").addEventListener("click", (e) => {
      this.setState({ name: "liweile" });
      console.log(this.state.name);//打印liweile
    });
  }

  render() {
    return (
      <div>
        <h1>{this.state.name}</h1>
        <button id="btn">点我更新值</button>
      </div>
    );
  }
}
export default App;

需要注意的是在hook写法中,这两种都没有用

import { useState } from "react";
const App = function () {
  const [name, setName] = useState("nihao");
  return (
    <div>
      <h1>{name}</h1>
      <button
        onClick={() => {
          setTimeout(() => {
            setName("liweile");
            console.log(name);//打印nihao,没有获取到更新后的值
          });
        }}
      >
        点我更新
      </button>
    </div>
  );
};
export default App;


import { useState, useEffect } from "react";
const App = function () {
  const [name, setName] = useState("nihao");
  useEffect(() => {
    document.getElementById("btn").addEventListener("click", (e) => {
      setName("liweile");
      console.log(name);//打印nihao,没有获取到更新后的值
    });
  }, []);
  return (
    <div>
      <h1>{name}</h1>
      <button id="btn">点我更新</button>
    </div>
  );
};
export default App;

对于Hook写法的setState注意点

1、当state所定义的state类型为Object或者Array时,在回调中直接setState是无法成功的

function App() {
  const [obj,setObj] = useState({
    num:1
  });
  const clickMe = () => {
     setObj(v => {
       let newObj = v
       newObj.num = v.num + 1 // 直接修改num的值不成功
      return newObj
     })
  }
    return (
     <button onClick={clickMe}>{obj.num}</button>
    );
}

此时num的值一直为1。

原因

由于Object为引用类型,setState通过回调函数的形式赋值,其参数v存的是obj的地址,此时let newObj = v操作将newObj指向obj的地址,由于react中state是只读的,因此newObj.num = v.num + 1这个操作相当于obj.num = obj.num +1,因此无法成功。

解决方案

通过浅拷贝或者深拷贝(相关资料网上很多)可解决此问题,将代码修改如下:

function App() {
  const [obj,setObj] = useState({
    num:1
  });
  const clickMe = () => {
    setObj(v => {
      let newObj = Object.assign({},v)   // 对v进行浅拷贝
      newObj.num = v.num + 1
      return newObj
    })
  }
    return (
     <button onClick={clickMe}>{obj.num}</button>
    );
}

修改state的同时需要使用state的值时,建议使用函数的方式修改并进行相关的使用操作

setState中数据的合并

当我们多次调用了setState,只会生效最后一次state

import { useState, useEffect } from "react";
const App = function () {
  const [num, setNum] = useState(0);
  return (
    <div>
      <h1>{num}</h1>
      <button
        onClick={() => {
          setNum(num + 1);
          setNum(num + 1);
          setNum(num + 1);
          setNum(num + 1);//此时页面呈现的是1
        }}
      >
        点我更新
      </button>
    </div>
  );
};
export default App;
  • setState合并时进行累加: 给setState传递函数, 使用前一次state中的值

    import { useState, useEffect } from "react"; const App = function () { const [num, setNum] = useState(0); return (

    {num}

    <button onClick={() => { setNum((num) => num + 1); setNum((num) => num + 1); setNum((num) => num + 1); setNum((num) => num + 1);//此时打印的是4 }} > 点我更新
    ); }; export default App;

React 的更新流程

  • Reactpropsstate 发生改变时,会调用 Reactrender 方法,会创建一颗不同的树

  • React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI

    • 同层节点之间相互比较不会跨节点比较
  • 不同类型的节点,产生不同的树结构

  • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定

  • 比如下面的代码更改:

React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对Counter进行复用

  • 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点仅对比更新有改变的属性

memo

为什么需要memo?

shouldComponentUpdate

React给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值;主要作用是:**控制当前类组件对象是否调用render**方法

  • 该方法有两个参数:

  • 参数一: nextProps修改之后, 最新的 porps属性

  • 参数二: nextState 修改之后, 最新的 state 属性

  • 该方法返回值是一个 booolan 类型

  • 返回值为true, 那么就需要调用 render 方法

  • 返回值为false, 那么不需要调用 render 方法

  • 比如我们在App中增加一个message属性:

  • JSX中并没有依赖这个message, 那么它的改变不应该引起重新渲染

  • 但是通过setState修改 state 中的值, 所以最后 render 方法还是被重新调用了

PureComponent

  • 如果所有的类, 我们都需要手动来实现 shouldComponentUpdate, 那么会给我们开发者增加非常多的工作量

    • 我们设想一下在shouldComponentUpdate中的各种判断目的是什么?
  • props 或者 state 中数据是否发生了改变, 来决定shouldComponentUpdate返回 truefalse

  • 事实上 React 已经考虑到了这一点, 所以 React 已经默认帮我们实现好了, 如何实现呢?

    • 将 class 继承自 PureComponent
  • 内部会进行浅层对比最新的 stateporps , 如果组件内没有依赖 porpsstate 将不会调用render

  • 解决的问题: 比如某些子组件没有依赖父组件的stateprops, 但却调用了render函数

那么函数式组件如何解决这些问题呢?

  • 函数式组件如何解决render: 在没有依赖 stateprops 但却重新渲染 render 问题
    • 我们需要使用一个高阶组件memo

使用memo函数

*

使用方式很简单,在 Function Component 之外,在声明一个 areEqual 方法来判断两次 props 有什么不同,如果第二个参数不传递,则默认只会进行 props 的浅比较。

上面 React.memo() 的使用我们可以发现,最终都是在最外层包装了整个组件,并且需要手动写一个方法比较那些具体的 props 不相同才进行 re-render。

而在某些场景下,我们只是希望 component 的部分不要进行 re-render,而不是整个 component 不要 re-render,也就是要实现 局部 Pure 功能,这时需要使用useMemo();

使用 useMemo() 进行细粒度性能优化

useMemo() 基本用法如下:

*

useMemo() 返回的是一个 memoized 值,只有当依赖项(比如上面的 a,b 发生变化的时候,才会重新计算这个 memoized 值)memoized 值不变的情况下,不会重新触发渲染逻辑。

说起渲染逻辑,需要记住的是 useMemo() 是在 render 期间执行的,所以不能进行一些额外的副操作,比如网络请求等。

useMemo与useCallback

useMemo和useCallback都是reactHook提供的两个API,用于缓存数据,优化性能;两者接收的参数都是一样的,第一个参数表示一个回调函数,第二个表示依赖的数据。

共同作用

在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用

两者的区别

useMemo 缓存的结果是回调函数中return回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态

useCallback 缓存的结果是函数,主要用于缓存函数,应用场景如需要缓存的函数,因为函数式组件每次任何一个state发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费;另外还需要注意的是,useCallback应该和React.memo配套使用,缺了一个都可能导致性能不升反而下降。