受益于 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,这样使得我可以在每个页面轻松使用这些方法和属性,如下代码所示:
jsx复制代码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 事件并在适当的时候清除监听通常是在生命周期方法中完成的,如下代码所示: jsx复制代码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 旧官方文档
总结
React Hooks 的产生动机是为了更好地支持函数式组件,使得状态管理和其他特性的复用更加便捷,同时也是为了使 React 组件的 API 更简单、灵活,为未来 React 的发展打造更好的基础。Hooks 带来的优势使得 React 应用的开发变得更加现代、简洁和高效。