React Hooks浅学

可能是秋招之后第一篇关于技术的文章,虽然我的博客域名是.life,但是还是要水水技术性的文章。要不然有失我的技术人设,别搞得天天都在那里娱乐了。

说回来,工作上虽然干了半年的react native,但是一直用的是经过公司魔改过的react框架,即便用的是react的class写法,也基本上无生命周期的东西,基本上就是纯业务的东西弄来弄去。也没用过全局的状态管理,状态管理全靠通知事件去监听其他模块发来的变更通知。所以经过这半年的打磨,我对react的知识学习几乎为0,和我刚进来的一模一样,还是一窍不通。但是最近我们部门的业务要整个进行重构解耦,单独分离出来做了一个新的仓库。H5/APP/PC三端的通用逻辑部分只做一份,有差异的部分比如UI层,事件处理等再单独做。所以逻辑这块就可以纯用react来实现,而不需要用公司各种魔改的框架来实现。新仓库也用上了react18,所有的新特性也都可以用上了,在观摩代码的过程中,我也发现了很多🪝函数,我都没用过(在此之前我只知道useState和useEffect),因此在这浅学一波hooks(非常浅,就稍微了解一下为啥用和怎么用)

React Hooks

官方中文文档:https://zh-hans.react.dev/reference/react/hooks

先看了一遍阮一峰的文章,清除了自己之前混乱的想法。

首先class类存在什么样子的问题,写过的可能都有些体会,就是他是把数据和逻辑都融合在了一块,每一个方法都可以改这个数据,导致你这个数据有异常的时候,你不确定是哪个方法改的,所以找起来就很麻烦。方法顺序也都是随意的,只要你在这个类里面,你随便写到哪里都行,类里哪里调用你都行,排查起来比较乱。

函数的话就是按顺序执行,你调用顺序都比较直观。且好的函数只应该做一件事,就是纯函数,用起来和测起来令人神清气爽。react的函数式组件,应该就返回组件的HTML代码,做一个纯函数。但是这是不可能的,基本上都会引入了与计算无关的操作——副效应(side effect),这就使这个函数不纯了。纯函数内部只有通过间接的手段(即通过其他函数调用),才能包含副效应。

那hook是干什么的?就是为了解决React函数组件副效应的方案,所以用了各种hook。

最常用的useEffect,他的名字我之前一直不理解,怎么取得这么抽象,完全不明所以,没想到他就是最常用的,解决副效应(side effect)的钩子。突然恍然大悟,我悟了,怪不得网络请求啥的都放在这个方法里,因为他是最通用的方法!

useEffect

useEffect(setup, dependencies?)

setup就是你要处理effect的函数,比如请求网络获取数据。

第二个可选的dependencies就是你的依赖的数据,是以数组形式来的。不写这个参数就会每次渲染都执行这个方法。

如果是个空数组,那么他就不依赖于任何数据,所以他只会执行一次,所以这个时候有点像生命周期的componentDidMount(我只是说有点像这种效果,并不等同于就是这样子的啊,我没查证过)所以像刚进来页面获取数据,就可以只传空数组,那么就获取一次即可了。

如果是个非空的数组,比如传入了useState创建出来的参数,那么就会当只有这个依赖项发生变化的时候,才会重新执行,给我的感觉就有点像vue的watch函数(我之前一直以为是类同这个函数,所以我才会觉得useEffect这个函数名字太怪了,为啥不叫useWatch)比如我们乘机人变了,需要执行一些校验方法,那么一旦变更就自动执行,如果没变,就不会执行这个里面的方法(因为校验的代价还是比较大的,尽可能没变化的情况别执行)

还有一些清除的方法,具体可以看官方文档来学习。

关于生命周期,细节来了嗷,还有两个变种体,他们的执行时机不同。

useEffect 有两个很少使用的变换形式,它们在执行时机有所不同:

  • useLayoutEffect 在浏览器重新绘制屏幕前执行,可以在此处测量布局。
  • useInsertionEffect 在 React 对 DOM 进行更改之前触发,库可以在此处插入动态 CSS。

那么问题来了,useEffect他的执行时机是什么时候呢?

对于 useEffect 执行, React 处理逻辑是采用异步调用 ,对于每一个 effect 的 callback, React 会向 setTimeout回调函数一样,放入任务队列,等到主线程任务完成,DOM 更新,js 执行完成,视图绘制完毕,才执行。所以 effect 回调函数不会阻塞浏览器绘制视图。

他是等视图绘制完才执行,所以需要注意,他会先拿旧的依赖值进行渲染,然后更新DOM绘制完再执行effect,内部改了新的值的话,会再重新进行渲染。

还有一些关于useEffect的方案,都在这个应急方案的列表下,建议都看看

使用 Effect 同步 – React 中文文档

经验总结:

  • 总是检查是否可以通过添加 key 来重置所有 state,或者在渲染期间计算所需内容
  • 当你决定将某些逻辑放入事件处理函数还是 Effect 中时,你需要回答的主要问题是:从用户的角度来看它是 怎样的逻辑。如果这个逻辑是由某个特定的交互引起的,请将它保留在相应的事件处理函数中。如果是由用户在屏幕上 看到 组件时引起的,请将它保留在 Effect 中。

useState

最基本的一个hook,稍微有些注意事项,列举一下:

  • useState(initialState),initialState可以初始化值,或者用一个纯函数直接返回一个值,这个函数只会在初始化的时候执行一次

  • set函数仅更新下一次渲染的状态变量。如果在调用 set 函数后读取状态变量,则仍会得到在调用之前显示在屏幕上的旧值。

  • 提供的新值与当前state相同,则会跳过重新渲染该组件及其子代码

  • 对象和数组应该是替换它而不是改变现有对象,{…form,fistName:”Taylor”}

  • React会批量处理状态更新,他会在所有事件处理函数运行并调用set函数后更新屏幕

    // 因为set不会更新已经运行代码中的age,所以每次都是42+1
    function handleClick() {
      setAge(age + 1); // setAge(42 + 1)
      setAge(age + 1); // setAge(42 + 1)
      setAge(age + 1); // setAge(42 + 1)
    }
    
    // 用下面这种函数,从待定状态中计算下一个状态
    function handleClick() {
      setAge(a => a + 1); // setAge(42 => 43)
      setAge(a => a + 1); // setAge(43 => 44)
      setAge(a => a + 1); // setAge(44 => 45)
    }
    

useMemo

这个钩子我也经常看见,一开始不知道什么意思,后来才知道有点东西,就是能够缓存计算的结果,memo,记忆化(memoization),是性能优化最常见的手段。(我老是想成meme梗图那个,总觉得很怪)

const cachedValue = useMemo(calculateValue, dependencies)

calculateValue当然也是一个没有任何参数的纯函数,会把计算结果返回出来。

dependencies,响应式变量包括 props、state 和所有你直接在组件中定义的变量和函数。

注意事项不是很多,但还是有一些:

如果计算函数依赖于直接在组件主体创建的对象,那么依赖这样子的对象会破坏记忆性,因为重新渲染的时候,创建对象的代码行也会重新渲染时运行,导致你依赖的对象并不是你之前依赖的那个对象,会重新计算函数。所以最好是改成依赖某个基本数据类型而非对象。

// 不合适的代码,会多次重新渲染运行
function Dropdown({ allItems, text }) {
  const searchOptions = { matchMode: 'whole-word', text };

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); // 🚩 提醒:依赖于在组件主体中创建的对象
  
// 更好的解决方案,只依赖text这种基本数据类型
function Dropdown({ allItems, text }) {
const visibleItems = useMemo(() => {
  const searchOptions = { matchMode: 'whole-word', text };
  return searchItems(allItems, searchOptions);
}, [allItems, text]); // ✅ 只有当 allItems 或者 text 改变的时候才会重新计算

memo

穿插一下memo,这个并非钩子,但是经常出现,而且可能与useMemo有点傻傻分不清,因此学习一下。

memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。

import { memo } from 'react';
const SomeComponent = memo(function SomeComponent(props) {
  // ...
});

Attention:但 React 仍可能会重新渲染它:记忆化是一种性能优化,而非保证

这意味着memo里面的函数应该具备纯粹的渲染逻辑(纯函数?),意味着相同的输入,始终是相同的输出,那么只要memo的props没有改变,React就不需要重新渲染。

  • 即使一个组件被记忆化了,当它自身的状态发生变化时,它仍然会重新渲染(即组件自身里面的state状态变了,那么也会重新渲染)。memo 只与从父组件传递给组件的 props 有关。
  • 即使组件已被记忆化,当其使用的 context 发生变化时,它仍将重新渲染。

memo的默认比较是用Object.is(),也就是判断那些基本类型。对象、数组或函数这种无法判断相同的,返回都是false。所以我们用useMemo来避免每次都重新创建对象,从而减少memo的重新渲染。

function Page() {
  const [name, setName] = useState('Taylor');
  const [age, setAge] = useState(42);
	// 使用useMemo来记忆对象
  const person = useMemo(
    () => ({ name, age }),
    [name, age]
  );
  return <Profile person={person} />;
}

const Profile = memo(function Profile({ person }) {
  // ...
});

memo还有第二个参数,就是自定义比较函数,但是这个比较麻烦,还是尽量减少使用。

useCallback

那么既然都讲到了useMemo,就可以讲useCallback,两者的功能都是类似的。只不过一个是记忆你的计算结果,另一个记忆函数本身。

useCallback(fn, dependencies)

fn:缓存的函数,此函数可以接受任何参数并且返回任何值(和useMemo的区别,参数不一样,一个是无参,一个是可以有参)

dependencies:和useMemo一样


function ProductPage({ productId, referrer, theme }) {
  //与字面量对象 {} 总是会创建新对象类似,在 JavaScript 中,function () {} 或者 () => {} 总是会生成不同的函数。
  // 在多次渲染中缓存函数
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // 只要这些依赖没有改变

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

useCallback可以利用useMemo来实现

// 在 React 内部的简化实现
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}

**注意:**useCallback仅在少数情况下有意义:

  • 将其作为 props 传递给包装在 [memo] 中的组件。如果 props 未更改,则希望跳过重新渲染。缓存允许组件仅在依赖项更改时重新渲染(如之前例子一样)。
  • 传递的函数可能作为某些 Hook 的依赖。比如,另一个包裹在 useCallback 中的函数依赖于它,或者依赖于 [useEffect]中的函数。

useContext

那讲到useContext之前,得先讲createContext

const SomeContext = createContext(defaultValue)

defaultValue:给一个默认值,该默认值是用于作为最后手段的后备方案,因为这是一成不变的,上下文之所以有用,是能够提供来自其他组件的动态变化的值。

createContext 返回一个上下文对象,该上下文对象本身不包含任何信息(这很重要,因为我总以为是他给了一个值,但他一点有效的值都没有!)它只表示其他组件读取或提供的那个上下文。一般来说,在组件上方使用 SomeContext.Provider指定上下文的值,并在被包裹的下方组件内调用 useContext(SomeContext)读取它。上下文对象有一些属性:

function App() {
  const [theme, setTheme] = useState('light');
  // ……
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

value:想要传的值在这里面赋值,可以为任意类型,名字定死一定是value,不能是其他的!

还可以覆盖部分的context,有点意思,一部分是dark,再一部分是light

<ThemeContext.Provider value="dark">
  ...
  <ThemeContext.Provider value="light">
    <Footer />
  </ThemeContext.Provider>
  ...
</ThemeContext.Provider>

还能用export导出createContext,供其他组件使用,怪好用的。

useContext(SomeContext)就是读取值,useContext() 总是在调用它的组件 上面 寻找最近的 provider。它向上搜索,不考虑调用 useContext() 的组件中的 provider。

useRef

这个🪝的使用频率也挺高的,它能帮助引用一个不需要渲染的值。

const ref = useRef(initialValue)

initialValue:ref 对象的 current 属性的初始值。可以是任意类型的值。这个参数在首次渲染后被忽略。

useRef 返回一个只有一个属性的对象:

  • current:初始值为传递的 initialValue。之后可以将其设置为其他值。如果将 ref 对象作为一个 JSX 节点的 ref 属性传递给 React,React 将为它设置 current 属性。可以修改ref.current,但不要轻易修改,因为他是一个普通的js对象,改变他不会出发重新渲染。如果你的目的是这样子,那么可以修改,相当于固定住这个值。

react 的useRef可以用useState来实现近似版本的

// React 内部
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}

使用 ref 可以确保:

  • 可以在重新渲染之间 存储信息(普通对象存储的值每次渲染都会重置)。
  • 改变它 不会触发重新渲染(状态变量会触发重新渲染)。
  • 对于组件的每个副本而言,这些信息都是本地的(外部变量则是共享的)。

注意:

  • 不要在渲染期间写入或者读取 ref.current
  • 可以在 事件处理程序或者 Effect 中读取和写入 ref

ref最常用的一个功能就是操作DOM,current属性可以拿到DOM节点

forwardRef

但是如果你尝试直接传递ref给自定义组件,那么会报错,自定义组件不会暴露它们内部的DOM节点的ref,所以你可以用forwardRef包裹一层组件,他会把他收到的ref转发给子组件。

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

// ...
<MyInput ref={inputRef} />

useImperativeHandle

这个hook虽然感觉用的频率很低,但是在项目中见过了,还是讲一讲。正如之前所讲,我们的模块单独抽出来做了一个仓库,虽然解耦了,但还是需要在主仓库中调度使用,那么有些事件需要操作子组件里的东西,那么该如何操作呢?其中一个就是useRef,你指定ref的东西,它就能操作,而useImperativeHandle就是让你能够自定义ref向外暴露的东西。

import { forwardRef, useImperativeHandle } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
  useImperativeHandle(ref, () => {
    return {
      // ... 你想暴露的一些方法 ...
    };
  }, []);
  // ...

useSyncExternalStore

最后来一个在项目中没用到过,但是感觉用的还是挺重要的。useSyncExternalStore 是一个让你订阅外部 store 的 React Hook。

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
  • subscribe 函数应当订阅该 store 并返回一个取消订阅的函数。
  • getSnapshot 函数应当从该 store 读取数据的快照。

感觉挺抽象的,来点🌰

比如订阅浏览器的API,这就是在react外部的东西,navigator.onLine

import { useSyncExternalStore } from 'react';

export default function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
// 从浏览器 API 读取当前值,来实现 getSnapshot 函数:
function getSnapshot() {
  return navigator.onLine;
}
// 接下来,你需要实现 subscribe 函数。例如,当 navigator.onLine 改变,浏览器触发 window 对象的 online 和 offline 事件,然后返回一个清除订阅的函数:
// 注意,这里的subscribe被移到了ChatIndicator外面,这是因为放在ChatIndicator里面的话,每次重新渲染都是新的subscribe函数,react会重新订阅,每次都会调用,为了避免重新订阅,可以把subscribe移到外面,或者用useCallback
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇