React之state、hooks性能分析

目录

 

一、state

1、为什么使用setState

2、setState异步更新

3、如何获取异步的结果

4、setState一定是异步吗?

5、源码分析

6、数据的合并

7、多个state的合并

二、为什么需要Hook?

三、Class组件存在的问题

四、Hook的出现

五、案例对比

六、useState

1、案例分析

2、认识useState

七、Effect Hook

1、认识Effect Hook

2、需要清除Effect

3、使用多个Effect

4、Effect性能优化

八、useContext

九、useReducer

十、useCallback

十一、useMemo

十二、useRef

十三、useImperativeHandle

十四、useLayoutEffect

十五、自定义Hook

1、Context的共享

2、获取鼠标滚动位置

3、localStorage数据存储

十六、useState源码分析

十七、redux hooks


一、state

1、为什么使用setState

  1. 开发中我们并不能直接通过修改state的值来让界面发生更新:

    因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变 化;
     
    import React, { Component } from 'react'
    
    export default class App extends Component {
      constructor(props){
        super(props)
        this.state={
          counter:0
        }
      }
      increament = ()=> {
        this.state.counter += 1;
        console.log(this.state.counter)
      }
      render() {
        return (
          <div>
            <h2>当前计数:{this.state.counter}</h2>
            <button onClick={this.increament}>+1</button>
          </div>
        )
      }
    }
    
    点击按钮后,界面并有刷新,但是counter的值确实已经改变了


    因此,我们必须通过setState来告知React数据已经发生了变化;
    this.setState({
          counter : this.state.counter + 1
    })

     
  2. 在组件中并没有实现setState的方法,setState方法是从Component中继承过来的。



     

2、setState异步更新

  1. setState的更新是异步的?
     
    import React, { Component } from 'react';
    
    export default class App extends Component {
      constructor(props) {
        super(props);
        this.state = {
          message: 'init message'
        };
      }
      changeMessage = () => {
        this.setState({
          message: 'changeMessage',
        });
        console.log(this.state.message);
      };
      render() {
        return (
          <div>
            <h2>当前计数:{this.state.message}</h2>
            <button onClick={this.changeMessage}>按钮</button>
          </div>
        );
      }
    }
    




    最终打印结果是init message

    可见setState是异步的操作,我们并不能在执行完setState之后立马拿到最新的state的结果
  2. 为什么setState设计为异步呢?

    setState设计为异步其实之前在GitHub上也有很多的讨论;

    React核心成员(Redux的作者)Dan Abramov也有对应的回复(传送门);

    总结如下:

    1)setState设计为异步,可以显著的提升性能;

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

    最好的办法应该是获取到多个更新,之后进行批量更新;

    2)如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;

    state和props不能保持一致性,会在开发中产生很多的问题;
     
    import React, { Component } from 'react';
    
    function Home(props) {
      // 在父组件的render还未执行时,此处值仍然是init message 
      return <h2>{props.message}</h2>;
    }
    
    export default class App extends Component {
      constructor(props) {
        super(props);
        this.state = {
          message: 'init message',
        };
      }
      changeMessage = () => {
        this.setState({
          message: 'changeMessage',
        });
        //如果是同步,则此处state中的message已经是changeMessage,而由于render执行滞后,
        //会导致在render还未执行时,Home组建props中的message还是init message,在这个滞后期间,App中的message和Home中的message值不同步
        console.log(this.state.message);
      };
      render() {
        return (
          <div>
            <h2>当前计数:{this.state.message}</h2>
            <Home message={this.state.message} />
            <button onClick={this.changeMessage}>按钮</button>
          </div>
        );
      }
    }
    

3、如何获取异步的结果

  1. 方式一:setState的回调

    setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;

    格式如下:setState(partialState, callback)
     
      changeMessage = () => {
        //方式1:获取异步更新后的数据
        // setState(更新state,回调函数)
        this.setState({
          message: 'changeMessage',
        } ,()=> {
          console.log(this.state.message) //changeMessage
        });
      };
  2. 当然,我们也可以在生命周期函数:
     
    componentDidUpdate
    完整代码:
    import React, { Component } from 'react';
    
    export default class App extends Component {
      constructor(props) {
        super(props);
        this.state = {
          message: 'init message',
        };
      }
    
      componentDidUpdate(){
        console.log(this.state.message) //changeMessage
      }
      
      changeMessage = () => {
        //方式1:获取异步更新后的数据
        // setState(更新state,回调函数)
        this.setState({
          message: 'changeMessage',
        } ,()=> {
          console.log(this.state.message) //changeMessage
        });
      };
      render() {
        return (
          <div>
            <h2>当前计数:{this.state.message}</h2>
            <button onClick={this.changeMessage}>按钮</button>
          </div>
        );
      }
    }

     

4、setState一定是异步吗?

  1.  验证一:在setTimeout中的更新:
      changeMessage = () => {
        // 情况1:将setState放入计时器中
        setTimeout(() => {
          this.setState({ message: 'changeMessage' });
          console.log(this.state.message) //changeMessage
        }, 0);
      };
  2. 验证二:原生DOM事件:
     document.getElementById('btn').addEventListener('click',()=>{
          console.log('btn被点击')
          this.setState({ message: 'changeMessage' });
          console.log(this.state.message) //changeMessage
     })

    完整代码:
     

    import React, { Component } from 'react';
    
    export default class App extends Component {
      constructor(props) {
        super(props);
        this.state = {
          message: 'init message',
        };
      }
      
      changeMessage = () => {
        // 情况1:将setState放入计时器中
        setTimeout(() => {
          this.setState({ message: 'changeMessage' });
          console.log(this.state.message) //changeMessage
        }, 0);
      };
    
      componentDidMount(){
        document.getElementById('btn').addEventListener('click',()=>{
          console.log('btn被点击')
          this.setState({ message: 'changeMessage' });
          console.log(this.state.message) //changeMessage
        })
      }
    
      render() {
        return (
          <div>
            <h2>当前计数:{this.state.message}</h2>
            <button onClick={this.changeMessage}>按钮</button>
            <button id='btn'>按钮2</button>
          </div>
        );
      }
    }

     

  3. 其实分成两种情况:

     在组件生命周期或React合成事件中,setState是异步;

    在setTimeout或者原生dom事件中,setState是同步

5、源码分析

6、数据的合并

我通过setState去修改message,是不会对name产生影响的;

源码中其实是有对 原对象 和 新对象进行合并的:

 

7、多个state的合并

比如我们还是有一个counter属性,记录当前的数字:

 

二、为什么需要Hook?

Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。

class组件相对于函数式组件有什么优势?比较常见的是下面的优势:
 

  1. class组件可以定义自己的state,用来保存组件自己内部的状态;

    函数式组件不可以,因为函数每次调用都会产生新的临时变量;
     
    function Home() {
      // 局部变量 ,函数调用 变量会重新被定义
      let a = 0;
      return (
        <>
          <h2>aaaa</h2>
          <button>+1</button>
        </>
      );
    }
  2. class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;

    比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;

    函数式组件在没有hooks情况下,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
  3. class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;

    函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
  4. 所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件

三、Class组件存在的问题

  1. 复杂组件变得难以理解:

    我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;

    比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在 componentWillUnmount中移除);

    而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
  2. 难以理解的class:

    理解ES6的class是学习React的一个障碍。

    比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
     
  3. 组件复用状态很难

    为了一些状态的复用我们需要通过高阶组件或render props;

    像redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;

    或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;

    这些代码让我们不管是编写和设计上来说,都变得非常困难;

四、Hook的出现

  1. Hook的出现,可以解决上面提到的这些问题;
  2. 简单总结一下hooks:

    它可以让我们在不编写class的情况下使用state以及其他的React特性

    但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
  3. Hook的使用场景:

    Hook的出现基本可以代替我们之前所有使用class组件的地方(除了一些非常不常用的场景);

    但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;

    Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;

五、案例对比

  我们通过一个计数器案例,来对比一下class组件和函数式组件结合hooks的对比:
class
import React, { Component } from 'react';

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      message: 'init message',
    };
  }

  changeMessage = () => {
    this.setState({ message: 'changeMessage' });
  };

  render() {
    return (
      <div>
        <h2>当前计数:{this.state.message}</h2>
        <button onClick={this.changeMessage}>按钮</button>
      </div>
    );
  }
}
函数式
import React from 'react';

export default () => {
  const [message, setMessage] = useState('init message');
  const changeMessage = () => {
    setMessage('changeMessage');
  };
  return (
    <div>
      <h2>当前计数:{message}</h2>
      <button onClick={changeMessage}>按钮</button>
    </div>
  );
}
你会发现上面的代码差异非常大:函数式组件结 合hooks 让整个代码变得非常简洁,并且再也不 用考虑this 相关的问题;

 

 

六、useState

1、案例分析

  1. 那么我们来研究一下核心的一段代码代表什么意思:

    useState来自react,需要从react中导入,它是一个hook;

    参数:初始化值,如果不设置为undefined;

    返回值:数组,包含两个元素; 元素一:当前状态的值(第一调用为初始化值); 元素二:设置状态值的函数;

    点击button按钮后,会完成两件事情: 调用setCount,设置一个新的值; 组件重新渲染,并且根据新的值返回DOM结构;
  2. Hook 就是 JavaScript 函数,这个函数可以帮助你 钩入(hook into) React State以及生命周期等特性;
  3. 但是使用它们会有两个额外的规则:

    只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。

    只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
  4. Hook指的类似于useState、 useEffect这样的函数 ,Hooks是对这类函数的统称

2、认识useState

  1. State Hook的API就是 useState:

    useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一

    般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

    useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为
    undefined)。

    useState是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。 (传送门)
  2. 为什么叫 useState 而不叫 createState?

    “Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。

    在下一次重新渲染时,useState 返回给我们当前的 state。

    如果每次都创建新的变量,它就不是 “state”了。

    这也是 Hook 的名字总是以 use 开头的一个原因。
  3. 当然,我们也可以在一个组件中定义多个变量和复杂变量(数组、对象)
  4. 多状态:
    import React, { useState } from 'react';
    
    export default function MultiHookState() {
    
      const [count, setCount] = useState(0);
      const [age, setAge] = useState(18);
      const [friends, setFriends] = useState(["kobe", "lilei"]);
    
      return (
        <div>
          <h2>当前计数: {count}</h2>
          <h2>我的年龄: {age}</h2>
          <ul>
            {
              friends.map((item, index) => {
                return <li key={item}>{item}</li>
              })
            }
          </ul>
        </div>
      )
    }
  5. 复杂状态修改
    import React, { useState } from 'react';
    
    export default function ComplexHookState() {
      const [friends, setFrineds] = useState(['kobe', 'lilei']);
      const [students, setStudents] = useState([
        { id: 110, name: 'why', age: 18 },
        { id: 111, name: 'kobe', age: 30 },
        { id: 112, name: 'lilei', age: 25 },
      ]);
    
      function addFriend() {
        friends.push('hmm');
        setFrineds(friends);
      }
    
      function incrementAgeWithIndex(index) {
        const newStudents = [...students];
        newStudents[index].age += 1;
        setStudents(newStudents);
      }
    
      return (
        <div>
          <h2>好友列表:</h2>
          <ul>
            {friends.map((item, index) => {
              return <li key={index}>{item}</li>;
            })}
          </ul>
          <button onClick={(e) => setFrineds([...friends, 'tom'])}>添加朋友</button>
          {/* 错误的做法 */}
          <button onClick={addFriend}>添加朋友</button>
    
          <h2>学生列表</h2>
          <ul>
            {students.map((item, index) => {
              return (
                <li key={item.id}>
                  <span>
                    名字: {item.name} 年龄: {item.age}
                  </span>
                  <button onClick={(e) => incrementAgeWithIndex(index)}>
                    age+1
                  </button>
                </li>
              );
            })}
          </ul>
        </div>
      );
    }
    

     

 

七、Effect Hook

 

1、认识Effect Hook

  1. Effect Hook 可以让你来完成一些类似于class中生命周期的功能;

    事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);

    所以对于完成这些功能的Hook被称之为 Effect Hook;
  2. 假如我们现在有一个需求:页面的title总是显示counter的数字,分别使用class组件和Hook实现:
     
    import React, { PureComponent } from 'react';
    
    export default class ClassCounterTitleChange extends PureComponent {
      constructor(props) {
        super(props);
    
        this.state = {
          counter: 0,
        };
      }
    
      componentDidMount() {
        // 1.修改DOM
        document.title = this.state.counter;
    
        // 2.订阅事件
        console.log('订阅一些事件');
    
        // 3.网络请求
      }
    
      componentWillUnmount() {
        console.log('取消事件订阅');
      }
    
      componentDidUpdate() {
        document.title = this.state.counter;
      }
    
      render() {
        return (
          <div>
            <h2>当前计数: {this.state.counter}</h2>
            <button
              onClick={(e) => this.setState({ counter: this.state.counter + 1 })}
            >
              +1
            </button>
          </div>
        );
      }
    }
    
    import React, { useState, useEffect } from 'react';
    
    export default function HookCounterChangeTitle() {
      const [counter, setCounter] = useState(0);
    
      useEffect(() => {
        document.title = counter;
      });
    
      return (
        <div>
          <h2>当前计数: {counter}</h2>
          <button onClick={(e) => setCounter(counter + 1)}>+1</button>
        </div>
      );
    }

     

  3. useEffect的解析:

    通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;

    useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;

    默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;

2、需要清除Effect

import React, { useEffect, useState } from 'react';

export default function EffectHookCancelDemo() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('订阅一些事件');

    return () => {
      console.log('取消订阅事件');
    };
  }, []);

  return (
    <div>
      <h2>EffectHookCancelDemo</h2>
      <h2>{count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
    </div>
  );
}
  1. 在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除:

    比如我们之前的事件总线或Redux中手动调用subscribe;

    都需要在componentWillUnmount有对应的取消订阅;

    Effect Hook通过什么方式来模拟componentWillUnmount呢?
  2. useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B
  3. type EffectCallback = () => (void | (() => void | undefined));
  4. 为什么要在 effect 中返回一个函数?

    这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;

    如此可以将添加和移除订阅的逻辑放在一起;

    它们都属于 effect 的一部分;
  5. React 何时清除 effect?

     React 会在组件更新和卸载的时候执行清除操作;

    effect 在每次渲染的时候都会执行;

3、使用多个Effect

  1. 使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:

    比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;
  2. 使用Effect Hook,我们可以将它们分离到不同的useEffect中:
  3. Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:

    React 将按照 effect 声明的顺序依次调用组件中的每一个 effect;

4、Effect性能优化

  1. 默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

    某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅);

    另外,多次执行也会导致一定的性能问题;
  2. 我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?

    useEffect实际上有两个参数:

    参数一:执行的回调函数;

    参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
  3. 但是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:

    那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了;
  4. 案例
    import React, { useState, useEffect } from 'react'
    
    export default function MultiEffectHookDemo() {
      const [count, setCount] = useState(0);
      const [isLogin, setIsLogin] = useState(true);
    
      useEffect(() => {
        console.log("修改DOM", count);
      }, [count]);
    
      useEffect(() => {
        console.log("订阅事件");
      }, []);
    
      useEffect(() => {
        console.log("网络请求");
      }, []);
    
      return (
        <div>
          <h2>MultiEffectHookDemo</h2>
          <h2>{count}</h2>
          <button onClick={e => setCount(count + 1)}>+1</button>
          <h2>{isLogin ? "message": "未登录"}</h2>
          <button onClick={e => setIsLogin(!isLogin)}>登录/注销</button>
        </div>
      )
    }

八、useContext

  1. 在之前的开发中,我们要在组件中使用共享的Context有两种方式:

    类组件可以通过 类名.contextType = MyContext方式,在类中获取context;

    多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;
  2. 但是多个Context共享时的方式会存在大量的嵌套:

    Context Hook允许我们通过Hook来直接获取某个Context的值;
  3. 注意事项:

    当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值
  4. 案例:
    export const UserContext = createContext();
    
    export const ThemeContext = createContext();
    <UserContext.Provider value={{ name: 'why', age: 18 }}>
            <ThemeContext.Provider value={{ fontSize: '30px', color: 'red' }}>
              <ContextHookDemo />
            </ThemeContext.Provider>
    </UserContext.Provider>
     
    import React, { useContext } from 'react';
    
    import { UserContext, ThemeContext } from "../App";
    
    export default function ContextHookDemo() {
    
      const user = useContext(UserContext);
      const theme = useContext(ThemeContext);
      console.log(user);
      console.log(theme);
    
      return (
        <div>
          <h2>ContextHookDemo</h2>
        </div>
      )
    }

九、useReducer

  1. 很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是。
  2. useReducer仅仅是useState的一种替代方案:

    在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;

    或者这次修改的state需要依赖之前的state时,也可以使用;
  3. 数据是不会共享的,它们只是使用了相同的counterReducer的函数而已。
  4. 所以,useReducer只是useState的一种替代品,并不能替代Redux。
  5. 案例:
    home.js
    import React, { useReducer } from 'react';
    import reducer from './reducer';
    
    export default function Home() {
      // const [count, setCount] = useState(0);
    
      const [state, dispatch] = useReducer(reducer, { counter: 0 });
      return (
        <div>
          <h2>Home当前计数: {state.counter}</h2>
          <button onClick={(e) => dispatch({ type: 'increment' })}>+1</button>
          <button onClick={(e) => dispatch({ type: 'decrement' })}>-1</button>
        </div>
      );
    }
    profile.js
    import React, { useReducer } from 'react';
    
    import reducer from './reducer';
    
    export default function Profile() {
      // const [count, setCount] = useState(0);
      const [state, dispatch] = useReducer(reducer, { counter: 0 });
    
      return (
        <div>
          <h2>Profile当前计数: {state.counter}</h2>
          <button onClick={(e) => dispatch({ type: 'increment' })}>+1</button>
          <button onClick={(e) => dispatch({ type: 'decrement' })}>-1</button>
        </div>
      );
    }

    reducer.js
     

    export default function reducer(state, action) {
      switch (action.type) {
        case 'increment':
          return { ...state, counter: state.counter + 1 };
        case 'decrement':
          return { ...state, counter: state.counter - 1 };
        default:
          return state;
      }
    }

     

十、useCallback

  1. useCallback实际的目的是为了进行性能的优化。
  2. 如何进行性能的优化呢?

    useCallback会返回一个函数的 memoized(记忆的) 值;

    在依赖不变的情况下,多次定义的时候,返回的值是相同的;
  3. 案例

    案例一:使用useCallback和不使用useCallback定义一个函数是否会带来性能的优化;
    import React, { useState, useCallback, useMemo } from 'react';
    
    export default function CallbackHookDemo01() {
      const [count, setCount] = useState(0);
    
      const increment1 = () => {
        console.log('执行increment1函数');
        setCount(count + 1);
      };
    
      const increment2 = useCallback(() => {
        console.log('执行increment2函数');
        setCount(count + 1);
      }, [count]);
    
      return (
        <div>
          <h2>CallbackHookDemo01: {count}</h2>
          <button onClick={increment1}>+1</button>
          <button onClick={increment2}>+1</button>
        </div>
      );
    }
    案例二:使用useCallback和不使用useCallback定义一个函数传递给子组件是否会带来性能的优化;
    import React, {useState, useCallback, memo} from 'react';
    
    /**
     * useCallback在什么时候使用?
     * 场景: 在将一个组件中的函数, 传递给子元素进行回调使用时, 使用useCallback对函数进行处理.
     */
    
    const HYButton = memo((props) => {
      console.log("HYButton重新渲染: " + props.title);
      return <button onClick={props.increment}>HYButton +1</button>
    });
    
    export default function CallbackHookDemo02() {
      console.log("CallbackHookDemo02重新渲染");
    
      const [count, setCount] = useState(0);
      const [show, setShow] = useState(true);
    
      const increment1 = () => {
        console.log("执行increment1函数");
        setCount(count + 1);
      }
    
      const increment2 = useCallback(() => {
        console.log("执行increment2函数");
        setCount(count + 1);
      }, [count]);
    
      return (
        <div>
          <h2>CallbackHookDemo01: {count}</h2>
          {/* <button onClick={increment1}>+1</button>
          <button onClick={increment2}>+1</button> */}
          <HYButton title="btn1" increment={increment1}/>
          <HYButton title="btn2" increment={increment2}/>
    
          <button onClick={e => setShow(!show)}>show切换</button>
        </div>
      )
    }
  4. 通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存;

十一、useMemo

  1. useMemo实际的目的也是为了进行性能的优化。
  2.  如何进行性能的优化呢?

    useMemo返回的也是一个 memoized(记忆的) 值;

    在依赖不变的情况下,多次定义的时候,返回的值是相同的;
  3. 案例:

    案例一:进行大量的计算操作,是否有必须要每次渲染时都重新计算;
    import React, { useState, useMemo } from 'react';
    
    function calcNumber(count) {
      console.log('calcNumber重新计算');
      let total = 0;
      for (let i = 1; i <= count; i++) {
        total += i;
      }
      return total;
    }
    
    export default function MemoHookDemo01() {
      const [count, setCount] = useState(10);
      const [show, setShow] = useState(true);
    
      const total = calcNumber(count);
      // const total = useMemo(() => {
      //   return calcNumber(count);
      // }, [count]);
    
      return (
        <div>
          <h2>计算数字的和: {total}</h2>
          <button onClick={(e) => setCount(count + 1)}>+1</button>
          <button onClick={(e) => setShow(!show)}>show切换</button>
        </div>
      );
    }


    案例二:对子组件传递相同内容的对象时,使用useMemo进行性能的优化
     
    import React, { useState, memo, useMemo } from 'react';
    
    const HYInfo = memo((props) => {
      console.log("HYInfo重新渲染");
      return <h2>名字: {props.info.name} 年龄: {props.info.age}</h2>
    });
    
    export default function MemoHookDemo02() {
      console.log("MemoHookDemo02重新渲染");
      const [show, setShow] = useState(true);
    
      // const info = { name: "why", age: 18 };
      const info = useMemo(() => {
        return { name: "why", age: 18 };
      }, []);
    
      return (
        <div>
          <HYInfo info={info} />
          <button onClick={e => setShow(!show)}>show切换</button>
        </div>
      )
    }
 

十二、useRef

  1. useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变。
  2. 最常用的ref是两种用法:

    用法一:引入DOM(或者组件,但是需要是class组件)元素;

    用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;
  3. 案例:

    案例一:引用DOM
    import React, { useEffect, useRef } from 'react';
    
    class TestCpn extends React.Component {
      render() {
        return <h2>TestCpn</h2>
      }
    }
    
    function TestCpn2(props) {
      return <h2>TestCpn2</h2>
    }
    
    export default function RefHookDemo01() {
    
      const titleRef = useRef();
      const inputRef = useRef();
      const testRef = useRef();
      const testRef2 = useRef();
    
      function changeDOM() {
        titleRef.current.innerHTML = "Hello World";
        inputRef.current.focus();
        console.log(testRef.current);
        console.log(testRef2.current);
      }
    
      return (
        <div>
          <h2 ref={titleRef}>RefHookDemo01</h2>
          <input ref={inputRef} type="text"/>
          <TestCpn ref={testRef}/>
          <TestCpn2 ref={testRef2}/>
    
          <button onClick={e => changeDOM()}>修改DOM</button>
        </div>
      )
    }
    


    案例二:使用ref保存上一次的某一个值
     
    import React, { useRef, useState, useEffect } from 'react'
    
    export default function RefHookDemo02() {
      const [count, setCount] = useState(0);
    
      const numRef = useRef(count);
    
      useEffect(() => {
        numRef.current = count;
      }, [count])
    
      return (
        <div>
          {/* <h2>numRef中的值: {numRef.current}</h2>
          <h2>count中的值: {count}</h2> */}
          <h2>count上一次的值: {numRef.current}</h2>
          <h2>count这一次的值: {count}</h2>
          <button onClick={e => setCount(count + 10)}>+10</button>
        </div>
      )
    }

十三、useImperativeHandle

  1. ref和forwardRef结合使用:
    import React, { useRef, forwardRef } from 'react';
    
    const HYInput = forwardRef((props, ref) => {
      return <input ref={ref} type="text"/>
    })
    
    export default function ForwardRefDemo() {
      const inputRef = useRef();
    
      return (
        <div>
          <HYInput ref={inputRef}/>
          <button onClick={e => inputRef.current.focus()}>聚焦</button>
        </div>
      )
    }


    通过forwardRef可以将ref转发到子组件;

    子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;
  2. forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:

    直接暴露给父组件带来的问题是某些情况的不可控;

    父组件可以拿到DOM后进行任意的操作;

    但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作;
  3. 通过useImperativeHandle可以只暴露固定的操作:
    import React, { useRef, forwardRef, useImperativeHandle } from 'react';
    
    const HYInput = forwardRef((props, ref) => {
      const inputRef = useRef();
    
      useImperativeHandle(ref, () => ({
        focus: () => {
          inputRef.current.focus();
        }
      }), [inputRef])
    
      return <input ref={inputRef} type="text"/>
    })
    
    export default function UseImperativeHandleHookDemo() {
      const inputRef = useRef();
    
      return (
        <div>
          <HYInput ref={inputRef}/>
          <button onClick={e => inputRef.current.focus()}>聚焦</button>
        </div>
      )
    }
    


    通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起;

    所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象;

    比如我调用了 focus函数,甚至可以调用 printHello函数;

十四、useLayoutEffect

  1. useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:

    useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;

    useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
  2. 如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。
  3. 案例: useEffect和useLayoutEffect的对比
     
    import React, { useState, useEffect } from 'react'
    
    export default function EffectCounterDemo() {
      const [count, setCount] = useState(10);
    
      useEffect(() => {
        if (count === 0) {
          setCount(Math.random() + 200)
        }
      }, [count]);
    
      return (
        <div>
          <h2>数字: {count}</h2>
          <button onClick={e => setCount(0)}>修改数字</button>
        </div>
      )
    }
    
    import React, { useState, useEffect, useLayoutEffect } from 'react'
    
    export default function LayoutEffectCounterDemo() {
      const [count, setCount] = useState(10);
    
      useLayoutEffect(() => {
        if (count === 0) {
          setCount(Math.random() + 200)
        }
      }, [count]);
    
      return (
        <div>
          <h2>数字: {count}</h2>
          <button onClick={e => setCount(0)}>修改数字</button>
        </div>
      )
    }

    可以看到使用useEffect,当点击按钮时,会先执行setCount(0),界面重新渲染,渲染完成后,执行useEffect,再次更改count,界面再次渲染,在界面上会看到数字闪烁一下,而使用useLayoutEffect,点击按钮时setCount只会执行一次,不会出现数字闪烁一下问题。

十五、自定义Hook

  1. 自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。
  2. 案例:所有的组件在创建和销毁时都进行打印
    function useLoggingLife(name) {
      useEffect(() => {
        console.log(`${name}组件被创建出来了`);
    
        return () => {
          console.log(`${name}组件被销毁掉了`);
        }
      }, []);
    }


    组件被创建:打印 组件被创建了;

    组件被销毁:打印 组件被销毁了;

1、Context的共享

export const UserContext = createContext();
export const TokenContext = createContext();
import { useContext } from "react";
import { UserContext, TokenContext } from "../App";

function useUserContext() {
  const user = useContext(UserContext);
  const token = useContext(TokenContext);

  return [user, token];
}

export default useUserContext;

2、获取鼠标滚动位置

import { useState, useEffect } from 'react';

function useScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPosition(window.scrollY);
    }
    document.addEventListener("scroll", handleScroll);

    return () => {
      document.removeEventListener("scroll", handleScroll)
    }
  }, []);

  return scrollPosition;
}

export default useScrollPosition;

3、localStorage数据存储

import {useState, useEffect} from 'react';

function useLocalStorage(key) {
  const [name, setName] = useState(() => {
    const name = JSON.parse(window.localStorage.getItem(key));
    return name;
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(name));
  }, [name]);

  return [name, setName];
}

export default useLocalStorage;

十六、useState源码分析

 

十七、redux hooks

  1. 在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect:

    但是这种方式必须使用高阶函数结合返回的高阶组件;

    并且必须编写:mapStateToProps和 mapDispatchToProps映射的函数;
  2. 在Redux7.1开始,提供了Hook的方式,我们再也不需要编写connect以及对应的映射函数了
  3. useSelector的作用是将state映射到组件中:

    参数一:将state映射到需要的数据中;

    参数二:可以进行比较来决定是否组件重新渲染;(后续讲解)
  4. useSelector默认会比较我们返回的两个对象是否相等;

    如何比较呢? const refEquality = (a, b) => a === b;

    也就是我们必须返回两个完全相等的对象才可以不引起重新渲染;
  5. useDispatch非常简单,就是直接获取dispatch函数,之后在组件中直接使用即可;
  6. 我们还可以通过useStore来获取当前的store对象;
 
 
 
 
 
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值