React 常用 Hooks笔记 | 青训营笔记 第 12 天

184 阅读9分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 12 天

React 常用 Hooks笔记

前言

官方明确推荐函数式组件 react.docschina.org/docs/hooks-…

Hook 这个单词的意思是"钩子"。 React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。 你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用 use 前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 useXxx。hook是v16.8.0后,才新增的特性

生命周期在class组件中非常重要。但是太多的太多的生命周期难记,有些也不知道具体的场景麻烦。还有就是this指向的问题比如我们要写大量的bind函数来改变this的指向,当然也可以通过装饰器等其他方法改变,但是本质上来说还是要跟this打交道。

Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) ,而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

内置钩子:

  • useState【维护状态】
  • useEffect【完成副作用操作】
  • useContext【使用共享状态】
  • useReducer【类似redux】
  • useCallback【缓存函数】
  • useMemo【缓存值】
  • useRef【访问DOM】
  • useImperativeHandle【使用子组件暴露的值/方法】
  • useLayoutEffect【完成副作用操作,会阻塞浏览器绘制】

www.jianshu.com/p/ceac435da…

useState

概念

返回一个 state,以及更新 state 的函数。

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的依次重新渲染加入队列。

可以简单理解为,如果要改变数据联动视图就要使用useState

注意:如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。

简单使用

import { useState } from "react";
export default function UseState() {
  // 这两个参数我们可以自定义名字,用的是数组解构的方法
  const [num, setNum] = useState(1);
  function add() {
    setNum(num + 1, () => console.log(num));
  }
  return (
    <div>
      <h2>Counter:{num}</h2>
      <button onClick={() => add()}>++</button>
    </div>
  );
}

进阶写法

初始值比较比较复杂,则可以直接传入一个函数进行计算,该函数只会在初始化的时候被调用一次,后续在使用setState更新的时候,不会在改变值,也就是说useState是惰性的,可以对比一个函数中变量的使用。

import { useState } from "react";
export default function UseState() {
  const initNum = (i) => {
    return i + 1;
  };
  const [num, setNum] = useState(() => {
    console.log("只会在初始化的时候触发了");
    const initialState = initNum(1);
    return initialState;
  });
  const add = () => {
    // 改变数据不会再触发initNum
    setNum(num + 1);
  };

  return (
    <div>
      <button onClick={() => add()}>+1</button>
      <div>你好,react hook{num}</div>
    </div>
  );
}

useEffect

useEffect 可以让你在函数组件中执行副作用操作,接收两个参数,第一个参数是要执行的函数 callback,第二个参数是可选的依赖项数组 dependencies。其中依赖项是可选的,如果不指定,那么 callback 就会在每次函数组件执行完后都执行;如果指定了,那么只有依赖项中的值发生变化的时候,它才会执行。 简单来说就是当我们的依赖项发生发生变化的时候,可以异步的执行里面的回调。

注意:

useEffect是在render之后执行

import { useEffect, useState } from "react";
export default function UseEffect() {
  /**
   * 第一个参数是回调函数
   * 第二个参数是依赖项
   * 每次num变化时都会变化
   * 
   * 注意初始化的时候,也会调用一次
   */
  const [num, setNum] = useState(1);
  useEffect(() => {
    console.log("每次num,改变我才会触发");
    return () => {
      /**
       * 这是卸载的回调可以执行某些操作
       * 如果不想执行,则可以什么都不写
       */
      console.log("卸载当前监听");
    };
  }, [num]);
  useEffect(() => {
    console.log("每次render页面我就会触发");
    return () => {
      console.log("卸载当前监听");
    };
  });
  return (
    <div>
      <button onClick={() => setNum(num + 1)}>+1</button>
      <div>你好,react hook{num}</div>
    </div>
  );
}

Immutable Data 不可变数据

www.yuque.com/zhuba_ahhh/…

概念

www.yuque.com/wangkai-jjg…

Immutable 意为「不可变的」。在编程领域,Immutable Data 是指一种一旦创建就不能更改的数据结构。它的理念是:在赋值时,产生一个与原对象完全一样的新对象,指向不同的内存地址,互不影响

作用

避免副作用

当我们需要对一个对象进行修改时,直接在原对象上进行变更很方便,也很节省内存。但是在 js 中,对象都是引用类型,在按引用传递数据的场景中,会存在多个变量指向同一个内存地址的情况,这样会引发不可控的副作用。

板子

import produce from "immer"; 

const newGoods = produce(goods, (draft) => {
  draft[id].select = !draft[id].select;
});
setGoods(newGoods);
const a = { x: 1 };
const b = a;
b.x = 6;
a.x // 6
import React, { useState } from "react";
export default function App() {
  const [list, setList] = useState([1, 2, 3]);
  const addMutable = () => {
    list.push("新数据");
    setList(list); // 并不能添加 并未改变引用类型数据的地址
  };
  const addImmutable = () => {
    setList([...list, "新数据"]); // 可以添加
  };
  return (
    <div className="App">
      <button onClick={addMutable}>已可变的方式添加</button>
      <button onClick={addImmutable}>已不可变的方式添加</button>
      {list.map((item, index) => (<li key={index}>{item}</li>))}
    </div>
  );
}
import produce from "immer";

  const [goods, setGoods] = useState([
    { name: "hxs", price: 12, num: 1 },
    { name: "hsa", price: 29, num: 2 },
    { name: "ass", price: 32, num: 3 },
    { name: "ads", price: 12, num: 3 },
  ]);
  const [selectAll, setSelectAll] = useState(false);
  const update = (id, flag) => {
    if (goods[id].num || flag) {
      const newGoods = produce(goods, (draft) => {  // 不可变数据更新视图才会重新渲染
        draft[id].num += flag;
      });
      setGoods(newGoods);
    }
  };

路由的一些hooks www.yuque.com/zhuba_ahhh/…

react hooks组件传值

父子组件之间的传值

react hook 父子组件之间通过props进行传值

父传子

父组件:在子组件标签上定义属性

子组件:函数组件接收一个props是一个对象,父组件传的属性名就是props对象的key,属性的值就是对应的value

const Child = (props) => {
  // 父组件穿过来一个name
  return (
    <div>
    <div>{props.name}</div>
  </div>
)
}
const Parent = () => {
  // 组件标签上传递属性
  return (
    <Child name='张三'></Child>
  )
}

子传父

概括:在props上定义一个方法,调用方法的时候传入参数,达到传值的效果

父组件:在子组件标签上定义一个属性,属性值为一个方法

import { useState } from "react";

const Child = (props) => {
  const toParent = () => {
    // 调用props上面的getChildData方法
    props.getChildData && props.getChildData("传给父组件");
  };

  return (
    <div>
    <button onClick={toParent}>往父组件传值</button>
    </div>
  );
};

const Parent = () => {

  // 点击子组件 就会触发这个函数
  const getChild = (msg) => {
    console.log(msg)
  }

  // 组件标签上传递属性,属性的值是一个函数
  return <Child getChildData={getChild}></Child>;
};

React.memon

概念

高阶组件是参数为组件,返回值为新组件的函数。

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

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

// 如果num不改变当前组件不会重新渲染
const MyComponent =  React.memo((props) => {
  /* 使用 props 渲染 */
  return (
    <div>{props.num}</div>
                               )
})
export default function hook() {

  const [num, setNum] = useState(1)

  return (
    <div>
    <button onClick={() => setNum(num + 1)}>+1</button>
  <MyComponent num={num}></MyComponent>
  </div>
)
}

特点

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现,第二个参数是一个函数,返回true不渲染,false渲染,可以做渲染劫持 自定义逻辑做劫持

import React, { useState, useEffect, useContext } from 'react';
const MyComponent =  React.memo((props) => {
  /* 使用 props 渲染 */
  return (
    <div>{props.num}</div>)
/**
   * prevProps 上次的值
   * nextProps 最新的值
   * 
   * 如果传来的值是偶数的话则不更新组件
   * 可以做渲染劫持
   * 自定义逻辑做劫持
   */
}, (prevProps, nextProps) => {
  console.log(nextProps, nextProps.num % 2)
  return nextProps.num % 2 === 0
})
export default function hook() {
  const [num, setNum] = useState(1)
  useEffect(() => {
    /**
     * 当它是一个空数组时,回调只会被触发一次,类似于 componentDidMount
     */
    console.log("componentDidmount")
  }, [])

  return (
    <div>
    <button onClick={() => setNum(num + 1)}>+1</button>
  <MyComponent num={num}></MyComponent>
  </div>
)
}

劫持渲染

import { memo } from "react";
export default memo(
  ({ lists }) => {
    return lists.map((list, index) => (
      <div key={index}>
        {list.name}---{index + 1}
      </div>
    ));
  },
// 劫持渲染 两个参数分别是前后的props
  (pre, cur) => {
    console.log(cur);
    return pre.lists.length >= 4 && pre.lists.length <= 8;
  }
);

useCallback

接收两个参数,第一个参数是个函数,第二个是依赖项。返回一个 memoized函数,依赖项不变的情况下,memoizedCallback的引用不变。即:useCallback会被缓存,从而达到渲染性能优化的目的。

使用场景:

  • 函数定义时需要进行大量运算, 这种场景极少
  • 需要比较引用的场景,如上文提到的useEffect,又或者是配合React.Memo使用:
import React, { useState, useCallback } from "react";

// react.memo会做一层浅比较

/**
 * 因为我们每次触发render 都会重新执行一遍当前函数
 * 所以说,我们的方法会重新赋值,react.memo时进行浅比较
 * 重新赋值的方法和之前的方法,引用不一样,所以react.memo
 * 会认为是一个新的对象,所以会重新渲染
 */
const ChildComponent = React.memo((props) => {
  console.log("每次render都会触发吗?", props);
  return (
    <div>
      <div>你好我是子组件</div>
    </div>
  );
});
export default function LearnUseCallBack() {
  const [num, setNum] = useState(1);
  const [count, setCount] = useState(1);
  /**
   * useCallback 第一个参数是一个函数
   * 第二个参数是依赖项
   * 依赖项不变的情况下,函数的引用不变
   * 依赖项传空数组,那么函数会一直不变
   * 如果什么都不穿,那么会失效
   *
   * 引用地址变了后,函数不会调用,他只负责引用地址
   */
  const add = useCallback(() => {
    console.log("你好");
    setNum(num + 1);
  }, [count]);
  return (
    <div>
      <div>缓存函数</div>
      <button onClick={add}>num + 1</button>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      num ==> {num}
      count ==> {count}
      <ChildComponent add={add}></ChildComponent>
    </div>
  );
}

import React, { useState, useCallback } from "react";
// react.memo会做一层浅比较
/**
 * 因为我们每次触发render 都会重新执行一遍当前函数
 * 所以说,我们的方法会重新赋值,react.memo时进行浅比较
 * 重新赋值的方法和之前的方法,引用不一样,所以react.memo
 * 会认为是一个新的对象,所以会重新渲染
 */
const ChildComponent = React.memo((props) => {
  console.log("每次render都会触发", props);
  return (
    <div>
      <div>你好我是子组件</div>
    </div>
  );
});
export default function LearnUseCallBack() {
  const [num, setNum] = useState(1);
  const [count, setCount] = useState(1);
  /**
   * useCallback 第一个参数是一个函数
   * 第二个参数是依赖项
   * 依赖项不变的情况下,函数的引用不变
   * 依赖项传空数组,那么函数会一直不变
   * 如果什么都不穿,那么会失效
   *
   * 引用地址变了后,函数不会调用,他只负责引用地址
   */
  const add = useCallback(() => {
    console.log("你好");
    setNum(num + 1);
  }, [count]);
  return (
    <div>
      <div>缓存函数</div>
      <button onClick={add}>num + 1</button>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      num ==> {num}
      count ==> {count}
      <ChildComponent add={add}></ChildComponent>
    </div>
  );
}

模仿生命周期函数

componentDidMount-useEffect

useEffect(() => {
  /**
     * 当它是一个空数组时,回调只会被触发一次,类似于 componentDidMount
     */
  console.log("componentDidmount")
}, [])

shouldComponentUpdate-React.memo

React.memo可以模仿shouldComponentUpdate的部分功能,拦截

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


const MyComponent =  React.memo((props) => {
  /* 使用 props 渲染 */
  return (
    <div>{props.num}</div>)
/**
   * prevProps 上次的值
   * nextProps 最新的值
   * 
   * 如果传来的值是偶数的话则不更新组件
   */
}, (prevProps, nextProps) => {
  console.log(nextProps, nextProps.num % 2)
  return nextProps.num % 2 === 0
})
export default function hook() {
  const [num, setNum] = useState(1)
  useEffect(() => {
    /**
     * 当它是一个空数组时,回调只会被触发一次,类似于 componentDidMount
     */
    console.log("componentDidmount")
  }, [])
  return (
    <div>
    <button onClick={() => setNum(num + 1)}>+1</button>
  <MyComponent num={num}></MyComponent>
  </div>)
}

componentWillUnmount

  useEffect(() => {
    return () => { // return是结束时可以调用
      console.log('componentWillUnmount')
    }
  }, [])

封装一个自定义hook useDidUpdate

// 引入我们封装的自定义hooks
import useDidUpdate from './hooks/use-didupdate'

export default () => {
  useDidupdate(() => {
    // 每次初始化的时候不触发,每次render都会触发
    console.log("模仿componentDidUpdate")
  })
}