HooKs的产生给我们带了什么,和类组件有什么区别?
受益于 Hooks
和 JSX
的影响,我一直热衷于 React
开发。尽管在找工作的过程中,因为我不会使用 Vue,可能错失了一些面试机会,但我坚定地认为目前并不需要学习 Vue
。我选择了 React
,因为我认为在学习和掌握它之前,坚持专注于这个技术会更有意义。
在开始学习源码之前,我们首先要以一个开发者的身份去了解一下 HOOKS
的设计动机。
HOOKS 设计动机
引入 hooks
的动机是为了在函数组件中实现状态管理和副作用处理的一种灵活且易于使用的方式。在 React
中,之前主要通过类组件来实现这些功能。但是类组件的代码结构较为繁琐,需要继承并重写生命周期方法,难以复用逻辑。而 hooks
的出现解决了这些问题,它可以让我们在函数组件中使用状态和其他 React
特性,使组件逻辑更加清晰、简洁和可维护。
在组件之间雇佣状态逻辑很难
如果你使用过 React
一段时间,你也许会熟悉一些解决此类问题的方案,例如 render props
和 高阶组件
。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。
它们往往会存在以下问题:
- 嵌套层级增加: 使用
render props
或高阶组件时,往往需要多层嵌套组件来传递props
或包装组件。这可能导致组件结构复杂化,增加了代码的阅读和维护难度; - 组件耦合性增加: 使用
render props
或高阶组件时,组件与逻辑之间的耦合性会增加。这是因为它们通过props
传递逻辑,导致组件和逻辑紧密关联在一起。当需要修改或者替换逻辑时,可能需要同时修改多个组件,增加了代码的脆弱性; - 代码复杂性增加: 当组件关系变得复杂或需要传递多个逻辑时,可能需要编写大量的嵌套组件或高阶组件,使代码变得混乱和难以理解;
你可以使用 Hook
从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook
使你在无需修改组件结构的情况下并在多个组件中进行重用。这使得在组件间或社区内共享 Hook
变得更便捷。
例如,我们可以使用自定义 HOOKS
的方式来复用 localStorage
,这样使得我可以在每个页面轻松使用这些方法和属性,如下代码所示:
import React, { useState, useCallback } from "react";
function useLocalStorage(key, initialValue) {
// 获取本地存储的值,若不存在则使用初始值
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// 如果发生错误,则返回初始值
console.error("Error retrieving data from localStorage:", error);
return initialValue;
}
});
// 更新本地存储的值
const setValue = (value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error("Error storing data in localStorage:", error);
}
};
const clearValue = useCallback(() => {
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.error("Error clearing data from localStorage:", error);
}
}, [key, initialValue]);
return [storedValue, setValue, clearValue];
}
const App = () => {
const [name, setName, clearValue] = useLocalStorage("moment", "");
console.log(name);
return (
<div>
<div onClick={() => setName("index")}>设置</div>
<div onClick={() => clearValue()}>清除</div>
</div>
);
};
export default App;
在上面的代码中我们封装了一个 useLocalStorage
自定义 HOOKS
,这样使得我们可以在任意一个页面轻松使用这个自定义 HOOKS
向外暴露出来的方法和属性。
复杂组件变得难以理解
我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥,随着组件的复杂性增加,其代码可能会变得难以理解和维护。这主要有以下原因:
- 嵌套层级: 类组件中,为了处理各种逻辑和状态,往往需要进行多层嵌套。这样的嵌套结构会使代码的阅读和理解变得困难,尤其是当嵌套层级很深时;
- 生命周期: 类组件使用声明周期方法来管理组件的状态和副作用。然后这些生命周期方法的调用顺序和副作用之间的关系困难会导致代码的流程变得复杂,难以跟踪和调试。例如,组件常常在
componentDidMount
和componentDidUpdate
中获取数据。但是,同一个componentDidMount
中可能也包含很多其它的逻辑,如设置事件监听,而之后需在componentWillUnmount
中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生bug
,并且导致逻辑不一致; this
绑定: 在类组件中,需要使用this
关键字来引用组件实例。这可能导致一些问题,如忘记绑定事件处理程序、错误地使用箭头函数等,增加了代码的脆弱性和错误的发生率;- 类方法的复杂性: 类组件中,各种逻辑和功能往往以类方法的形式存在。这可能导致一个类方法变得很长,包含多个功能,难以分离和复用;
难以理解的 class
除了代码复用和代码管理会遇到困难外,我们还发现 class
是学习 React
的一大屏障。
在使用 JavaScript
的时候,我们一般请看下都是使用的函数式编成方式。在 React 中你必须理解 JavaScript
中的 this
的工作方式,这与其他语言存在巨大差异。
React
的类组件可能会更难以理解,以下是一些造成难以理解的常见因素:
- 生命周期方法的复杂性: 类组件使用生命周期方法来管理组件的渲染、更新和卸载过程。这些生命周期方法,如
componentDidMount
、shouldComponentUpdate
等的调用顺序和使用方式可能会令人困惑,特别是对于初学者来说。此外,存在多个生命周期方法时,它们之间的交互关系也可能导致代码的复杂性增加。 在React
的类组件中,监听DOM
事件并在适当的时候清除监听通常是在生命周期方法中完成的,如下代码所示:
在上面的示例中,class App extends React.Component { componentDidMount() { // 在组件挂载后,添加事件监听 document.addEventListener("click", this.handleClick); } componentWillUnmount() { // 在组件卸载前,清除事件监听 document.removeEventListener("click", this.handleClick); } handleClick(event) { // 处理点击事件 console.log("点击了:", event.target); } render() { return <div>My Component</div>; } }
componentDidMount
方法在组件挂载完成后被调用,此时添加了一个全局的点击事件监听器。而componentWillUnmount
方法在组件即将卸载前被调用,这里我们移除了之前添加的点击事件监听器; this
关键字的使用: 在类组件中,需要使用this
关键字来引用组件实例以及访问其属性和方法。然而,this
的指向在不同的上下文中可能会发生变化,容易引发错误。尤其是当需要在事件处理程序中使用this
时,需要进行额外的绑定操作或使用箭头函数,增加了代码的复杂性;- 继承和组合关系: 类组件使用继承来扩展
React
的基础组件类,并通过覆写父类方法来实现自定义行为。这种继承模式可能使组件之间的关系变得复杂,难以理解和维护。此外,类组件在实现组合时也需要显式地调用子组件的方法,增加了耦合度; - 内部状态和副作用: 类组件通过
state
属性来管理内部状态,并可以通过setState
方法进行更新。然而,由于类组件的复杂性,状态的变化和副作用的产生可能分布在多个方法中,使得它们之间的关系变得难以追踪和理解;
为了解决这些问题,Hook
使你在非 class
的情况下可以使用更多的 React
特性。从概念上讲,React
组件一直更像是函数。而 Hook
则拥抱了函数,同时也没有牺牲 React
的精神原则。Hook
提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。
Hook 会因为在渲染时创建函数而变慢吗?
不会!!!
读过前一篇文章都知道,React
采用 Fiber
的架构,当组件发生更新时,React
会从根节点开始遍历整个 Fiber
树,根据组件的依赖干洗和状态变化,决定哪些组件需要重新渲染,并以优先级顺序执行渲染操作。
具体到 Hook
的实现,React
内部维护了一个叫做 Hooks
的链表结构,用于存储每个组件使用的 Hook
相关信息。每个 Hook
在组件中的使用位置是有意义的,React
使用这个位置来确定每个 Hook
的顺序和状态。
当组件函数被调用时,React
会根据函数内部的 Hook
调用顺序,将对应的 Hook
数据添加到 Hooks
链表中。这样,在组件的每次渲染过程中,React
知道如何找到正确的状态和变量,并与之前的渲染进行关联。
在组件的多次渲染之间,React
会根据 Hooks
的调用顺序保持数据的一致性。如果 Hooks
的调用顺序发生变化,React
会正确地将状态与对应的 Hooks
实例关联起来。
通过这种机制,React
实现了在渲染时创建函数的能力,并确保了 Hook
在多次渲染之间正确地工作。这使得我们可以方便地在函数组件中使用状态和副作用,而不需要使用类组件或编写独立的函数。
尽管 Hooks
的实现依赖于闭包,但 React 在内部处理了这个问题,确保多次渲染之间正确地工作,而不会引发常见的闭包陷阱。
但是,在 React 的渲染过程中,每次渲染都会创建一个全新的闭包环境,这样每个 HOOK 都会捕获其对应的状态和变量。这意味着每个渲染都有自己独立的闭包环境,并且在多次渲染之间不会相互干扰。
况且在现代的浏览器当中,闭包和类的原始性能只有在极端场景下才会有明显的差别。Hooks
避免了 class
需要的额外开支,像是创建类实例和在构造函数中绑定时间处理器的成本。
HOOKS 的劣势
尽管 React Hooks
提供了许多优势,但它们也有一些劣势需要考虑:
- 性能问题: 使用不当的
Hooks
可能导致性能问题。例如,在渲染过程中过度使用useState
或useEffect
可能会导致不必要的重新渲染,从而降低性能。优化Hooks
的使用需要对其工作原理和React
的渲染过程有深入的了解; - 过度使用
Hooks
: 由于Hooks
提供了很多不同的特性,有时候开发者可能会过度使用它们,使组件过于复杂。在设计组件时,应该谨慎地选择使用Hooks
,并避免不必要的复杂性; - 使用规则: 使用
Hooks
时,需要遵循一些规则,例如只能在函数组件或自定义Hooks
中使用,不能在普通的JavaScript
函数中使用,而且 Hook 调用必须在组件的顶层。如果不遵循这些规则,可能会导致意想不到的错误; - 调试复杂性: 相比于类组件,在使用
Hooks
的组件中进行调试可能会更加复杂。当组件中使用多个状态和副作用时,可能需要更多的断点来追踪程序流程。 - 可读性: 有时,在组件中使用多个不同的
Hooks
,可能会导致组件代码看起来比较冗长,尤其是与使用类组件时相比。虽然Hooks
有助于更好地组织代码,但可能会导致一些人认为代码的可读性降低; - 闭包陷阱:
Hooks
使用闭包来缓存每次渲染的状态,这可能会导致一些意外的问题,如闭包陷阱和状态共享等;
在适当的情况下,合理使用 Hooks
可以提高代码的可读性、简洁性和可维护性,从而带来更好的开发体验和性能优势。
参考资料
总结
React Hooks
的产生动机是为了更好地支持函数式组件,使得状态管理和其他特性的复用更加便捷,同时也是为了使 React
组件的 API
更简单、灵活,为未来 React 的发展打造更好的基础。Hooks
带来的优势使得 React
应用的开发变得更加现代、简洁和高效。
最后分享两个我的两个开源项目,它们分别是:
这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰
转载自:https://juejin.cn/post/7259189576009760824