react学习笔记

state的时效性

statereact中的含义是“记忆”,或可以理解为“状态”,但是不是即时状态
在这里插入图片描述

在这里插入图片描述
触发click,执行的步骤是

  1. setIndexreact内部的index改为新值
  2. 打印原本的index
  3. setIndex触发react计算,重新渲染List组件
  4. 渲染时重新调用,打印index

所以一般情况下,可以想象成react在函数内部跑了一个tempIndex = indexhandleClickconsole.log(index)实际上是conosle.log(tempIndex)

触发页面渲染需要调用setState

Vue不同,react并不是响应式的,在下面的例子中,如果不调用setObj,则不会触发list的重新渲染
在这里插入图片描述

react称,不做成响应式

useState修改对象

react不是响应式,所以他不会监听直接对对象、数组的修改,通常都需要整个替换来实现,官方建议使用useImmer来简化代码,如:

import { useImmer } from 'use-immer';
export default function List() {
  const [obj, setObj] = useImmer({ num: 1 });
  const [arr, setArr] = useImmer([]);

  return (
    <article>
      <h2
        onClick={() => {
          setArr((draft: number[]) => {
            draft.push(3);
          });
          setObj(draft => {
			draft.num = 5
		  })
        }}
      >
        {arr.length}
      </h2>
    </article>
  );
}

它的原理是,直接修改draft草稿对象之后,immer内部再帮我们进行全量替换,draftimmer创建出来的一个对象 / 数组 copy

state设置原则

类比vue的话,state相当于data,但是需要手动修改(调用setXXX);computed则直接写对应的表达式即可,因为调用setXX时会触发组件重新渲染,即组件内部的表达式会重新计算

组件渲染替换的特殊性

react渲染出来的东西,一般被称作UI树,如果前后两次更新某个特定位置的组件没有变化,那么这个组件内部的state会保留下来:

例一

渲染两个计数器,第二个计数器的State每次渲染都会重置

在这里插入图片描述
官方的解释是:
在这里插入图片描述
但是当我这么一改,他就不会清空那个位置的State,这是因为一个相同的组件被渲染在了相同的位置
在这里插入图片描述
只有当我加上keyreact才会把他们当成两个不同的组件
在这里插入图片描述

例二
export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  if (isFancy) {
    return (
      <div>
        <Counter isFancy={true} />
        <label>
          <input
            type="checkbox"
            checked={isFancy}
            onChange={e => {
              setIsFancy(e.target.checked)
            }}
          />
          使用好看的样式
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        使用好看的样式
      </label>
    </div>
  );
}

对于上面这样的代码,点击后不会重置Counter组件里的State,即点击的前后,UI树结构没有发生变化,相同的位置还是渲染了相同的组件
在这里插入图片描述

在这里插入图片描述

例三
export default function App() {
  const [showB, setShowB] = useState(true);
  console.log('render')
  return (
    <div>
      {showB && <Counter />}
      {!showB ? <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        渲染第二个计数器
      </label>
    </div>
  );
}

如果这样子写,在react UI树中,初始时他是
在这里插入图片描述
点击后,变成
在这里插入图片描述
这样子属于在不同的位置渲染不同的组件,所以此时不会保留State

想硬要他们保留的话,那可以给他们设置一个相同的key来实现

{show && <ListItem key="1" />}
{!show && <ListItem key="1" />}

useReducer

useReducer相当于语法糖,与reduce方法类似,但是要求必定返回一个state,并且不能在其中执行异步或其他操作

比如官方的示例中,点击发送到按钮,会触发sent_messagedispatch,实现的逻辑是“点击按钮后清空输入框”,所以返回一个{...state, message: ''}的对象来重新设置state
在这里插入图片描述
在这里插入图片描述
示例代码中在点击按钮时还会alert出对应的消息
在这里插入图片描述
需要注意的是,alert不能移动到sent_messagedispatch中执行,否则会出现死循环的情况
在这里插入图片描述

useContext

这是一种快捷实现组件透传的hooks,使用上有三步

1.创建context,官方示例是单个文件中导出
在这里插入图片描述
2. 在要使用透传的组件中用useContext实现
在这里插入图片描述
3. 提供Context,如果有逐层传递的情况,则会使用距离最近的context提供的值作为context的值
在这里插入图片描述
比如这里,传到PlaceImage组件时就应该是100 +500 + 500 = 1100

react中,兄弟组件间要进行通信,通常需要进行状态提升,即把要互通的变量、操作等提到两者共同的父组件或更顶层的父组件来实现,比如下面这个todoList

在这里插入图片描述
使用useContext的话就无需每个组件都向下传递了,直接在子组件内通过useContext调用即可
在这里插入图片描述

在这里插入图片描述

useRef

useRef通常用于设定一个不会出发页面渲染的值,他的值永远都是最新的,通常用于获取dom元素,与vueref调用DOM元素类似

由于没有响应式,所以思考这样的场景:输入一段文字,点击“发送”,3s后才发送,但是期间修改输入框的值,要求发送的值为最新值

在这里插入图片描述
如果是在vue中,直接setTimeout(() => { alert(this.message) })即可

但是在react中,因为state是渲染时快照,点击“发送”,message绑定的是此时输入框中的值
在这里插入图片描述

所以这里要额外引入一个useRef来实现:

在这里插入图片描述

或者声明一个文件内的变量

跨组件调用ref

主要通过forwardRef来实现,它会向外部暴露ref属性

参考下面这个例子,封装了一个SearchInput组件,使用forwardRef实现了将ref“代理”到真实的input元素上
在这里插入图片描述
在这里插入图片描述

useImperativeHandle

如果想要向外暴露某些方法或变量,就要用到useImperativeHandle + forwardRef来实现

vue中可以直接通过ref.xxx来访问子组件,但是在react里则需要先进行一层套用

在这里插入图片描述

在这里插入图片描述
不过实际使用中不应该滥用ref

useEffect

useEffect通常用于实现在组件渲染后执行一些特定的操作,但他并不和渲染挂钩,只取决于组件函数的执行

这里的关键在于:依赖变化是否触发了组件函数重新执行

useEffectvue - watch很像,但是不能等同,这里举个例子说明:

const TodoList: React.FC = () => {
  const [state, setState] = useState(0);
  const ref = useRef(0);
  const handleClick = () => {
    console.log("点击!");
  };
  useEffect(() => {
    console.log("state 修改", state);
  }, [state]);
  return (
    <div className={styles.container}>
      <Button type="primary" onClick={handleClick}>
        点击
      </Button>
    </div>
  );
};

export default TodoList;

在这里插入图片描述

一个可以确定的特性是,首次渲染时,无论是否指定依赖数组,都会触发一次useEffect绑定的函数

现在为按钮绑定点击的回调,让按钮点击时修改state,则实际的执行步骤为:

  1. 触发点击事件,调用setState
  2. setState引起组件重新渲染(或者说组件函数重新调用)
  3. 走到useEffect,内部判断state前后状态下是否相等
  4. state不相等,执行useEffect绑定的函数

代码:

const TodoList: React.FC = () => {
  const [state, setState] = useState(0);
  const handleClick = () => {
    console.log("点击! oldState:", state);
    setState((state) => state + 1);
  };
  useEffect(() => {
    console.log("触发effect");
  }, [state]);
  return (
    <div className={styles.container}>
      <Button type="primary" onClick={handleClick}>
        点击
      </Button>
    </div>
  );
};

效果:
在这里插入图片描述
这里要注意,一定是要跑了组件函数才会触发useEffect里的函数执行,假如绑定的依赖是useRef声明的变量,那么每次更新如果不跑组件函数,useEffect绑定的函数一样是不会调用的,比如下面这个例子,代码:

const TodoList: React.FC = () => {
  const [state, setState] = useState(0);
  const ref = useRef(0);
  const handleClick = () => {
    console.log("点击! oldState:", state);
    setState((state) => state + 1);
  };
  const handleSecondClick = () => {
    console.log("点击! oldRef:", ref.current);
    ref.current += 1;
  };
  useEffect(() => {
    console.log("触发effect");
  }, [ref.current]);
  return (
    <div className={styles.container}>
      <Button type="primary" onClick={handleClick}>
        点击
      </Button>
      <Button type="primary" onClick={handleSecondClick}>
        点击
      </Button>
    </div>
  );
};

点击第二个按钮,触发ref增加,但是没有触发组件重新渲染,因此不会跑useEffect
在这里插入图片描述

此时点击第一个按钮,调用了setState触发页面重绘,所以跑了useEffectuseEffect去判断ref.current前后两次不同,所以要执行一次
在这里插入图片描述

但是需要注意,他是放在某次组件渲染后才执行的,比如下面这个场景:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这里没有渲染两次,是因为使用框架跑的,框架中把这个特性给关了

react严格模式下,组件会默认渲染两次,对此官方的解释是

以此可以让开发者发现一些错误并修复

假如我们在useEffect中创建一个到服务器的连接,每个useEffect需要在下一次组件渲染时才会进行cleanups,如果用户此时切换页面,而我们没有设置useEffectcleanup回调,就会导致这条连接一直保持着存活性
在这里插入图片描述

然后以此你会发现到“上一个连接没有终止”的问题,但我个人感觉有点牵强了,以前用vue的时候本身就会在beforeDestroy钩子里写各种销毁操作了

解决方法就是设置一个返回函数,在里面给他断开

在这里插入图片描述

用法一:仅在组件初次渲染时调用

看下图,value在父组件中用useState声明,onChange会触发setState导致组件重新渲染
在这里插入图片描述

这意味着每次输入或勾选 / 反选,都会导致输入框重复聚焦

如果不用useEffect会读不到ref.current

所以解决方案就是,第二个参数,它是一个需要忽略的依赖数组,将其设为空数组则表示只需要在该组件初次渲染时调用
在这里插入图片描述

传空数组应该这么理解,空数组可以当作是[‘’],每次useEffect的调用会与上一次useEffect时指定的值作对比

重复渲染触发对比,发现两次的值相同 '' === '',所以第二次渲染时就不会执行useEffect里的代码

如果上面改成下面这样,那就会疯狂调用回调,因为每次渲染时value都变了

  useEffect(() => {
    console.log(123)
  }, [value]);
用法二:销毁effect

useEffect的回调可以设置一个返回的函数,在其中执行一些操作;每个useEffect回调执行前都会清理上一个Effect(如果有的话)

常用的场景有两种,一个是清空计时器,一个是解决请求覆盖

清空计时器:
在这里插入图片描述
解决请求覆盖:
在这里插入图片描述
不过一般请求都是在handleXXX回调里发起的,在那边解决请求覆盖应该是结合AbortController来实现,或者用singletonPromise实现;就看是否需要连请求一起中断了

useEffect生命周期

每个useEffect都是一个独立的同步过程,他的第二个参数接收一个依赖数组,表示当数组中任一依赖发生了变化时,执行指定的计算逻辑

在这里插入图片描述

useEffectEvent(实验性的,未上正式版)

主要是用于提取useEffect中非响应式的逻辑

对于下面这段代码,应该只有切换roomId时才重新连接聊天室,不希望切换theme的时候也触发effect,但同时又需要在展示"Connected"的时候显示此时的主题,所以这里就产生了矛盾
在这里插入图片描述
也许你会想到可以把他提取出去,但是这本身已经违背业务逻辑了(连接到聊天室是异步的,不应该放在外面提示LOL)
在这里插入图片描述
所以为了解决这个问题,react提供了useEffectEvent来实现
在这里插入图片描述
这段代码让useEffect仅依赖于roomId的变化,而在connected的回调中又能确保showNotification提示的是最新的theme

useLayoutEffect

useEffect是放在组件渲染完成后执行的,可以简单对照到vue - nextTick后执行的代码,即下一次tick才清空useEffect的队列

useLayoutEffect是在浏览器绘制页面前执行,但在layoutEffect的内部依然能获取到DOM元素的最新的状态,这是因为本身react就是先算出要怎么渲染,最后再提交到浏览器中绘制出来的

假设某个操作修改了页面DOM元素的状态,在useLayoutEffect中依然获取到的是元素最新的值

对于下面这段代码:

const TodoList: React.FC = () => {
  const [state, setState] = useState(400);
  const divRef = useRef<HTMLDivElement>(null);
  const handleClick = () => {
    setState(600);
  };

  useEffect(() => {
    console.log(
      "useEffect div height:",
      divRef.current?.getBoundingClientRect().height
    );
  }, [state]);

  useLayoutEffect(() => {
    console.log(
      "useLayoutEffect div height:",
      divRef.current?.getBoundingClientRect().height
    );
  }, [state]);

  return (
    <div ref={divRef} style={{ height: state }} className={styles.container}>
      <Button type="primary" onClick={handleClick}>
        点击
      </Button>
    </div>
  );
};

点击按钮后的执行结果:
在这里插入图片描述
在官方文档中给出的一个实际的使用案例是在计算tooltip实际应该展示的位置时,避免一闪而过

这是useLayoutEffect的效果:

在这里插入图片描述
这是useEffect的效果:
在这里插入图片描述

区别参考另一个博主的描述,我觉得挺OK的:
在这里插入图片描述
出现页面闪烁时可以考虑用useLayoutEffect优化

useCallback

hook主要用于实现对函数进行缓存,是性能优化时常用的一种手段

因为react会在setState调用时重新渲染所有使用该state的组件,所以组件内的各类声明,按理说有一些函数或状态是不需要重新声明的(比如某些业务方法,提交表单,二次确认信息等),此时就可以使用useCallback来实现

useCallback接收两个参数,fn与依赖项数组,如果依赖项没有变化,那么useCallback会返回原本声明的函数,否则才重新创建函数

一个性能优化的最佳实践是结合memo使用,memo可以创建一个组件,并且该组件仅在传入的props发生修改了的情况下重新渲染,如

import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
  // ...
});

function ProductPage({ productId, referrer, theme }) {
  // 在多次渲染中缓存函数
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // 只要这些依赖没有改变

  return (
    <div className={theme}>
      {/* ShippingForm 就会收到同样的 props 并且跳过重新渲染 */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

思考另一个场景,假如现在有一个按钮可以往页面的列表中插入一条数据,比如todo

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos([...todos, newTodo]);
  }, [todos]);
  // ...

在这种写法中,我们的useCallback每次都会因为其他地方对todos修改而创建新的函数,所以常用的办法是使用updater函数来优化setTodos的调用,使函数的声明不依赖todos的修改

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: nextId++, text };
    setTodos(todos => [...todos, nextTodo]);
  }, []);
  // ...

另外还有一种场景,如果是在useEffect里使用了组件内声明的函数,需要把这个函数当作依赖项传入

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  function createOptions() {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🔴 问题:这个依赖在每一次渲染中都会发生改变
  // ...

但是当组件因为其他原因重新渲染时,createOption都是新的值,导致useEffect每次都会调用,所以也可以用useCallback来解决

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const createOptions = useCallback(() => {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }, [roomId]); // ✅ 仅当 roomId 更改时更改

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // ✅ 仅当 createOptions 更改时更改
  // ...

或者可以将这个方法提到组件外进行声明


const createOptions = (roomId = '') => {
   return {
     serverUrl: 'https://localhost:1234',
     roomId: roomId
   };
 };
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');


  useEffect(() => {
    const options = createOptions(roomId);
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ 仅当 roomId更改时更改
  // ...

useMemo

类似于vuecomputed,但是也在组件函数重新执行时才会去调用并计算出值

几乎所有的useXXX钩子,都要求是运行时才“计算”,并不会像vue那样由watcher去监听和自动计算,对于刚从vue转过来的我还需要一定的时间慢慢适应

useMemo一般用来记忆“值”,也可以记忆函数,避免每次组件函数调用时重复声明函数导致重新渲染。

对于函数的记忆,使用useCallback即可
在这里插入图片描述

自定义HOOK

核心在于下面这句话:
在这里插入图片描述

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值