React-Hooks

useState

基本使用

js 复制代码
// 使用方法一:初始值为基础数据类型或Object
const [state, setState] = useState(initialState);

// 使用方法二:初始值为函数
const [state, setState] = useState(() => initialState);

@param useState 接受一个参数,作为 state 的初始值,这个参数可以是任何数据类型 ,也可以是函数

@return useState 返回一个数组,数组中包括 someState 数据源和更新这个 state 的方法 setSomeState

使用

js 复制代码
const [name, setName] = useState('React'); // 参数是String

const [age, setAge] = useState(9); // 参数是Number

const [features, setFeatures] = useState([{ text: 'JSX' }]); // 参数是Object

const [count, setCount] = useState(() => {
  // 先执行一定的逻辑,后再返回初始值
  const initialState = computedBaseCount(props);
  return initialState;
}); // 参数是Function

注意事项

  • 状态更新可能是异步的:React可能会延迟执行状态更新,以提高性能。因此,如果状态更新依赖于当前状态,最好使用函数形式的更新:

    ini 复制代码
    JavaScript
     代码解读
    复制代码
    setCount(currentCount => currentCount + 1);
  • 状态更新是替换而不是合并 :与类组件中的this.setState方法不同,更新状态变量总是替换它,而不是合并。因此,如果状态是对象或数组,需要注意手动合并或构造新的状态

当你调用useState时,React会在内部为当前组件保持一个状态,并在组件的每次渲染之间保持这个状态。当你通过调用状态更新函数(如上例中的setCount)更新状态时,React会重新渲染组件,并使用最新的状态值

原理分析

1. Hooks 是如何存储状态的

React为每个组件维护了一个状态链表。每次组件渲染时,React都会按照Hooks被调用的顺序,从这个链表中读取或更新状态。这就是为什么Hooks必须在组件的最顶层调用,且不能在循环、条件语句或嵌套函数中调用的原因------这确保了每次组件渲染时Hooks的调用顺序是一致的。

2. 组件渲染和更新

当你使用useState定义了一个状态变量和一个更新这个变量的函数后,每当状态更新函数被调用时,React会将新的状态值入队,然后触发组件的重新渲染。在组件的下一个渲染周期中,useState会返回最新的状态值。

3. 批量更新和异步更新

React对状态更新采用了批处理和异步更新的策略来优化性能。当多个状态更新函数在React事件处理函数或生命周期方法中被同步调用时,React会将这些更新批量处理,延迟更新直到事件处理完成,然后一次性应用所有更新并重新渲染组件。这减少了不必要的渲染次数和计算,提高了应用的性能。

4. 函数式更新

如果新的状态依赖于旧的状态,推荐使用函数式更新。这是因为状态更新可能是异步的,直接使用旧状态可能会导致不一致的结果。函数式更新确保了每次更新都使用最新的状态值

源码分析

juejin.cn/post/707645...

useEffect

可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

useEffect需要传递两个参数,第一个参数是逻辑处理函数,第二个参数是一个数组

js 复制代码
useEffect(() => {
/** 执行逻辑 */
},[])

1、第二个参数存放变量,当数组存放变量发生改变时,第一个参数,逻辑处理函数将会被执行

2、第二个参数可以不传,不会报错,但浏览器会无限循环执行逻辑处理函数。

js 复制代码
useEffect(() => {
/** 执行逻辑 */
})

3、第二个参数如果只传一个空数组,逻辑处理函数里面的逻辑会在组件挂载时执行一次 ,会在组件被销毁前执行一次,不就是相当于 componentDidMount 和 componentWillUnmount。

js 复制代码
useEffect(() => {
/** 执行逻辑 */
},[])

4、第二个参数如果不为空数组,如下

js 复制代码
const [a, setA] = useState(1);
const [b, setB] = useState(2);
useEffect(() => {
/** 执行逻辑 */
},[a,b])

逻辑处理函数会在组件挂载时执行一次和(a或者b变量在栈中的值发生改变时执行一次) 这是不是相当于componentDidMount 和 componentDidUpdate 的结合

5、useEffect第一个参数可以返回一个回调函数,这个回调函数将会在组件被摧毁之前和再一次触发更新时,将之前的副作用清除掉。这就相当于componentWillUnmount。

useEffect去除副作用。我们可能会在组件即将被挂载的时候创建一些不断循环的订阅(计时器,或者递归循环)。在组件被摧毁之前,或者依赖数组的元素更新后,应该将这些订阅也给摧毁掉。

比如以下的情况(没有去除计时器,增大不必要的开销和代码风险)

js 复制代码
const [time, setTime] = useState(0)

useEffect(() => {
	const InterVal = setInterval(() => {
		setTime(time + 1)
	},1000)
},[])

利用第五点,在组件被摧毁前去除计时器。

const [time, setTime] = useState(0)

js 复制代码
useEffect(() => {
	const InterVal = setInterval(() => {
		setTime(time + 1)
	},1000)
	return () => {
   		clearInterval(InterVal )
   	}
},[])

useLayoutEffect

useLayoutEffect 与 useEffect 使用方式是完全一致的,区别的是 两者的执行时机

useEffect()是在DOM更新后(浏览器重绘完成)执行的,是异步的;useLayoutEffect()是在渲染之后但在屏幕更新之前执行的,是同步的。二者均是等待jsx执行完毕后再执行,但useLayoutEffect()在useEffect()之前触发。 大部分情况下采用useEffect(),因为它的性能更好。但当你的useEffect中需要操作dom,并且会改变页面的样式,就需要使用useLayoutEffect(),否则可能会出现闪屏问题

  1. react 在 diff 后,会进入到 commit 阶段,准备把虚拟 DOM 发生的变化映射到真实 DOM 上
  2. 在 commit 阶段的前期,会调用一些生命周期方法,对于类组件来说,需要触发组件的 getSnapshotBeforeUpdate 生命周期,对于函数组件,此时会调度 useEffect 的 create destroy 函数,注意是调度,不是执行。在这个阶段,会把使用了 useEffect 组件产生的生命周期函数入列到 React 自己维护的调度队列中,给予一个普通的优先级,让这些生命周期函数异步执行
  3. 随后,就到了 React 把虚拟 DOM 设置到真实 DOM 上的阶段,这个阶段主要调用的函数是 commitWork,commitWork 函数会针对不同的 fiber 节点调用不同的 DOM 的修改方法,比如文本节点和元素节点的修改方法是不一样的。
  4. commitWork 如果遇到了类组件的 fiber 节点,不会做任何操作,会直接 return,进行收尾工作,然后去处理下一个节点,这点很容易理解,类组件的 fiber 节点没有对应的真实 DOM 结构,所以就没有相关操作,但在有了 hooks 以后,函数组件在这个阶段,会同步调用上一次渲染时 useLayoutEffect(create, deps) create 函数返回的 destroy 函数
  5. 注意一个节点在 commitWokr 后,这个时候,我们已经把发生的变化映射到真实 DOM 上了
  6. 但由于 JS 线程和浏览器渲染线程是互斥的,因为 JS 虚拟机还在运行,即使内存中的真实 DOM 已经变化,浏览器也没有立刻渲染到屏幕上
  7. 此时会进行收尾工作,同步执行对应的生命周期方法,我们说的componentDidMount,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函数都是在这个阶段被同步执行。
  8. 对于 react 来说,commit 阶段是不可打断的,会一次性把所有需要 commit 的节点全部 commit 完,至此 react 更新完毕,JS 停止执行
  9. 浏览器把发生变化的 DOM 渲染到屏幕上,到此为止 react 仅用一次回流、重绘的代价,就把所有需要更新的 DOM 节点全部更新完成
  10. 浏览器渲染完成后,浏览器通知 react 自己处于空闲阶段,react 开始执行自己调度队列中的任务,此时才开始执行 useEffect(create, deps) 的产生的函数

useRef

useRef Hook 的作用主要有两个:

  • 多次渲染之间保证唯一值的纽带。

useRef 会在所有的 render 中保持对返回值的唯一引用。因为所有对ref的赋值和取值拿到的都是最终的状态,并不会因为不同的 render 中存在不同的隔离。

这点我们在开头的 useEffect Hook 中就已经展示了它的示例,判断是否是由于页面更新而非首次渲染:

ts 复制代码
import { useRef } from 'react';
export function useFirstMountState(): boolean {
  const isFirst = useRef(true);
  if (isFirst.current) {
    isFirst.current = false;
    return true;
  }
  return isFirst.current;
}
  • 获取 Dom 元素,在 Function Component 中我们可以通过 useRef 来获取对应的 Dom 元素。

useContext

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

熟悉 React 中Context Api 和 Vue 中的 provide/inject Api 的同学可能会对这个钩子的作用深有体会。

假设这样一种场景:

在根级别组件上我们需要向下传递一个用户名 username 的属性给每一个子组件进行使用。

此时,如果使用 props 的方法进行层层传递那么无疑是一种噩梦。而且如果我们的 G 组件需要使用 username 但是 B、E 并不需要,如果使用 props 的方法难免在 B、E 组件内部也要显式声明 username。

React 中正是为了解决这样的场景提出来 Context Api。

可以通过 React.createContext 创建 context 对象,在跟组件中通过 Context.Provider 的 value 属性进行传递 username ,从而在 Function Component 中使用 useContext(Context) 获取对应的值。

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer 接受三个参数分别是 reducer 函数、初始值 initialArg 以及一个可选的惰性初始化的 init 函数。

它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

让我们通过一个简单的计数器例子来了解一下它的基础用法:

ts 复制代码
import { useReducer } from 'react';

interface IState {
  count: number;
}

interface IAction {
  type: 'add' | 'subtract';
  payload: number;
}

const initialState: IState = { count: 0 };

const reducer = (state: IState, action: IAction) => {
  switch (action.type) {
    case 'add':
      return { count: state.count + action.payload };
    case 'subtract':
      return { count: state.count - action.payload };
    default:
      throw new Error('Illegal operation in reducer.');
  }
};

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <h1>Hello , My name is 19Qingfeng.</h1>
      <p>Counter: {state.count}</p>
      <p>
        <button onClick={() => dispatch({ type: 'add', payload: 1 })}>
          add 1!
        </button>
      </p>
      <p>
        <button onClick={() => dispatch({ type: 'subtract', payload: 1 })}>
          subtract 1!
        </button>
      </p>
    </>
  );
}

export default Counter;

这里我们创建了一个简单的 Counter 计数器组件,内部通过 useReducer 管理 couter 的状态

useMemo和useCallBack

useMemo

解决数是复杂数据类型,子组件仍会渲染的问题

使用场景:

假设以下场景,父组件在调用子组件时传递 info 对象属性,点击父组件的点击增加按钮时,发现控制台会打印出子组件被渲染的信息。

javascript 复制代码
import React, { memo, useState } from 'react';

// 子组件
const ChildComp = (props:{info:{name, age}}) => {
  console.log('ChildComp...',name,age);
  return (<div>ChildComp...</div>);

};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = { name, age };

  return (
    <div className="App">
      <div>hello world {count}</div>
      <button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
      <MemoChildComp info={info}/>
    </div>
  );
};

export default Parent;

分析原因:

点击父组件按钮,触发父组件重新渲染;父组件渲染,const info = { name, age } 一行会重新生成一个新对象,导致传递给子组件的 props 变化,进而导致子组件重新渲染。

解决方法:

使用 useMemo 将对象属性包一层。useMemo 有两个参数:

  • 第一个参数是个函数,返回的对象指向同一个引用,不会创建新对象;
  • 第二个参数是个数组,只有数组中的变量改变时,第一个参数的函数才会返回一个新的对象。   下面请看改进后的代码:
typescript 复制代码
import React, { memo, useMemo, useState } from 'react';

// 子组件
const ChildComp = (info:{info:{name: string, age: number}}) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  
  // 使用 useMemo 将对象属性包一层
  const info = useMemo(() => ({ name, age }), [name, age]);

  return (
    <div className="App">
      <div>hello world {count}</div>
      <button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
      <MemoChildComp info={info}/>
    </div>
  );
};

export default Parent;

useMemo

scss 复制代码
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo返回一个被记忆的值 !!!。

注意传入 useMemo的依赖项,这样的话useMemo它仅会在某个依赖项改变时才重新计算 memoized 值,这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useCallback

包裹函数,依赖改变时返回新的函

用法

useCallback的第一个参数是函数体,第二个参数是依赖项,只有依赖项中的变量改变时,才会返回一个新的函数

ini 复制代码
 const myFunction=( ()=>{
           函数体...
 }, [])

demo案例:

紧接着上面useMemo的例子,假设需要传给子组件一个函数,如下所示,当点击父组件按钮时,发现控制台会打印出子组件被渲染的信息,说明子组件又被重新渲染了。

javascript 复制代码
import React, { memo, useMemo, useState } from 'react';

// 子组件
const ChildComp = (props) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = useMemo(() => ({ name, age }), [name, age]);
  const changeName = () => {
    console.log('输出名称...');
  };

  return (
    <div className="App">
      <div>hello world {count}</div>
      <button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
      <MemoChildComp info={info} changeName={changeName}/>
    </div>
  );
};

export default Parent;

分析原因:

点击父组件按钮,改变了父组件中 count 变量值(父组件的 state 值 ),进而导致父组件重新渲染;父组件重新渲染时,会重新创建 changeName 函数,即传给子组件的 changeName 属性发生了变化,子组件props发生变化从而导致子组件渲染;

解决方法:

修改父组件的 changeName 方法,用 useCallback 钩子函数包裹一层, useCallback 参数与 useMemo 类似

使用 useCallback 将函数包一层,useCallback 有两个参数:

  • 第一个参数是个函数,返回的函数指向同一个引用,不会创建新函数;
  • 第二个参数是个数组,第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,就会重新返回一个新的记忆函数提供给后面进行渲染。如果是一个空数组则是无论什么情况下该函数都不会发生改变
javascript 复制代码
import React, { memo, useCallback, useMemo, useState } from 'react';

// 子组件
const ChildComp = (props) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = useMemo(() => ({ name, age }), [name, age]);
  const changeName = useCallback(() => {
    console.log('输出名称...');
  }, []);

  return (
    <div className="App">
      <div>hello world {count}</div>
      <button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
      <MemoChildComp info={info} changeName={changeName}/>
    </div>
  );
};

export default Parent;

优化后分析: 用useCallback包裹后,父组件render时,包裹后的函数因为依赖项不变所以还是用记忆函数,则MemoChildComp组件中changeName并没有发生改变,那个子组件props也没有改变,也就不会进行rerender。

补充: useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。

scss 复制代码
useCallback(fn, inputs)    就相当于   useMemo(() => fn, inputs).

前面使用 useCallback 的例子可以使用 useMemo 进行改写:

javascript 复制代码
这是通过useMemo进行改写上面useCallback这个例子。
  const changeName = useMemo(() => () => {
    console.log('输出名称...');
  }, []); // 空数组代表无论什么情况下该函数都不会发生改变
 

唯一的区别是:useCallback不会执行第一个参数函数,而是将它返回给你,而useMemo会执行第一个函数并且将函数执行结果返回给你

总结: useCallback 常用记忆事件函数 ,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值

相关推荐
幼儿园老大*5 分钟前
【Echarts】折线图和柱状图如何从后端动态获取数据?
前端·javascript·vue.js·经验分享·后端·echarts·数据可视化
黄宏哲14 分钟前
自定义 CSS 和 t-att-class 的使用
前端·css·odoo
地球空间-技术小鱼26 分钟前
SQL常用语法
java·开发语言·前端
林中白虎30 分钟前
CSS实现服务卡片
前端·css
盼兮*39 分钟前
栏目一:使用echarts绘制简单图形
前端·信息可视化·echarts
BIGSHU09231 小时前
Spring Web是个什么东西
java·前端·spring
qbbmnnnnnn1 小时前
【前端开发入门】css快速入门
前端·css·css基础·css教程·css入门
xgq2 小时前
使用Credential Management API实现更安全的用户身份验证
前端·javascript·面试
bobostudio19952 小时前
TypeScript 算法手册【快速排序】
前端·javascript·算法·typescript
想要打 Acm 的小周同学呀3 小时前
前端组件化开发
前端·javascript·vue.js·组件化开发