React Hooks诞生动机是什么?

前言

之前写了一篇重新认识React组件的文章,里面提到了关于类组件函数式组件 的优劣势,类组件提供了强大的API能力和状态管理,但在一些情况下显得太重 ,而函数式组件轻量并且灵活,但由于能力有限又处于尴尬地步,由此,React Hooks应用而生,给函数式组件插上的翅膀,有了更广阔的用武之地,本文就来梳理关于React Hooks的前世今生。

React Hook解决了什么问题

万事万物存在因果轮回,Hooks诞生的动机是什么?它又解决了什么问题呢?

逻辑拆分

在类组件中,我们平时最常写的代码风格是业务逻辑生命周期函数混合在一起,尤其是在大型的业务中,它会存在以下问题:

  • 一个功能逻辑可能出现在多个生命周期函数中,如定时器的创建与销毁、窗口事件的创建与销毁
  • 一个生命周期函数中存在多种业务逻辑,如获取页面数据、获取路由信息、监听DOM的事件

来看看下面的例子:

js 复制代码
class ListComponent extends Component {
  state = {
    list: [],
  };
  fetchData = () => {
    // 获取数据
	// ...
  };
  resetData = () => {
    this.setState({ list: [] });
  }
  componentDidMount() {
	// 获取路由参数并进行格式化
	// ...
	
	// 获取数据  
    this.fetchData();
	
	// 创建 监听窗口 事件
    window.addEventListener('resize', () => {})
	
  }
  
  componentDidUpdate() {
	// 获取某个props参数进行更新DOM
  }
  
  componentWillUnmount() {
	// 销毁事件
    window.removeEventListener('resize', () => {})
  }
  render() {
    return (
      <div>
        <button onClick={this.resetData}>重置数据</button>
      </div>
    );
  }
}

写过Vue2的同学看上面代码也深有同感,在一个生命周期里面处理众多零碎的业务逻辑,造成函数体积过大,给阅读和维护者带来很多麻烦。重要的是,这些看似毫无关联的逻辑,它们被打碎之后放到不同的生命周期函数中去处理,比如监听销毁窗口事件以及示例中没有提到的定时器的创建与销毁,它们本质上做的是同一件事情,但却被分配到不同的钩子中去处理,显然在设计上并不属于最佳实践吧。

既然这样,那使用Hooks可以怎么写呢?

js 复制代码
// 自定义 Hook 1:用于数据获取
function useDataFetching(url) {
	// ...
}

// 自定义 Hook 2:用于数据筛选
function useDataFilter(data, filter) {
	// ...
}

// 自定义 Hook 3: 用于处理页面副作用
function usePageEffect() {
	// ...
	
	useEffect( () => {
		// ...
		// 创建 监听窗口 事件
		window.addEventListener('resize', () => {})
		
		return () => {
			// 销毁事件
			window.removeEventListener('resize', () => {})
		}
	})
}

// 组件:使用数据获取和筛选 Hook
function MyComponent() {
	const { data, isLoading } = useDataFetching("https://api.example.com/data");
	const filteredData = useDataFilter(data, "filterCriteria");
	
	// ...
	usePageEffect()
	
	return (
		<div/>
	)
}

Hooks的加持下,我们有能力把这些复杂的业务逻辑进行分治,通过不同的函数组件或者自定义Hooks进行维护,形成有专门处理数据获取的函数组件、有专门处理数据转换或过滤的函数组件、有专门处理副作用的函数组件等,这就是;通过合理的方式把这些函数组件聚合起来,形成搭积木过程,这就是

逻辑复用

在类组件中,我们在复杂的业务逻辑中,复杂的逻辑会让组件变的难以维护,通常借助诸如HOC高阶组件Render Props渲染属性进行封装公共逻辑,但它们容易形成嵌套地狱等问题

为什么这么说呢?

假设你有一个需要进行身份验证的组件 AuthenticatedComponent,它需要检查用户是否已登录,如果未登录,则重定向到登录页面。你需要使用一个HOC来处理身份验证逻辑,如下所示:

jsx 复制代码
function withAuth(Component) {
  class WithAuth extends React.Component {
    componentDidMount() {
      if (!userIsLoggedIn()) {
        this.props.history.push('/login');
      }
    }

    render() {
      return <Component {...this.props} />;
    }
  }

  return withRouter(WithAuth);
}

现在,你需要有另一个HOC,用于处理主题切换的逻辑,如下所示:

jsx 复制代码
function withTheme(Component) {
  class WithTheme extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        theme: 'light',
      };
    }

    toggleTheme() {
      this.setState((prevState) => ({
        theme: prevState.theme === 'light' ? 'dark' : 'light',
      }));
    }

    render() {
      return (
        <Component
          {...this.props}
          theme={this.state.theme}
          toggleTheme={this.toggleTheme}
        />
      );
    }
  }

  return WithTheme;
}

现在,你可能想要将这两个HOC应用于你的组件 MyComponent,以进行身份验证和主题切换。你的代码可能如下所示:

jsx 复制代码
const xxxComponent = withAuth(withTheme(MyComponent));

这柯里化看起挺优雅的呀,没错!虽然看起来很方便,但如果你有更多的HOC需要应用,代码可能会变得混乱,难以阅读和维护,这在React设计者看来并不是最佳的。

自从有了 Hooks 以后,我们可以通过自定义 Hooks ,达到既不破坏组件结构、又能够实现逻辑复用的效果,同时,函数一等公民的身份也让单元测试变的简单。还是上面的例子:

jsx 复制代码
// 自定义 Hook 1:用于数据获取
function useDataFetching(url) {
	// ...
}

// 自定义 Hook 2:用于数据筛选
function useDataFilter(data, filter) {
	// ...
}

// 自定义 Hook 3: 用于处理页面副作用
function usePageEffect() {
	// ...
	
	useEffect( () => {
		// ...
		// 创建 监听窗口 事件
		window.addEventListener('resize', () => {})
		
		return () => {
			// 销毁事件
			window.removeEventListener('resize', () => {})
		}
	})
}

this指向问题

this的指向问题一直在JavaScript中都是谜一样的存在,也让众多的初级开发者头疼,来看看下面类组件代码:

jsx 复制代码
class TransactionTable extends Component {
  state = {
    list: [],
  };
  fetchData = () => {
    transactionList().then((response) => {
      const list = response.data.data.items.slice(0, 13);
      if (this._isMounted) { 
        this.setState({ list });
      }
    });
  };
  resetData() {
    this.setState({ list: [] });
  }
  componentDidMount() {
    this.fetchData();
	
	// 创建 监听窗口 事件
    window.addEventListener('resize', () => {})
  }
  
  componentWillUnmount() {
	// 销毁事件
    window.removeEventListener('resize', () => {})
  }
  render() {
    return (
      <div>
        <button onClick={this.resetData}>重置数据</button>
      </div>
    );
  }
}

export default TransactionTable;

执行上面的代码,点击重置数据调用resetData会报错

是因为resetData为普通函数,它存在自己的函数作用域,我们需要在编码借助bind来修正this指向类组件实例或者通过箭头函数定义resetData,让它成为词法作用域。不管什么招数,这些都是从实践的层面约束来解决设计层面的问题。

明明我是按照生命周期函数的写法来写的呀,在这个层面上开发者存在一定的心智负担,告诉自己必须要留意this指向问题。

好,现在有Hooks了,我们可以将上面的类组件改为函数组件,就不用关心this问题。

React Hooks的注意点

既然你说Hooks有这么多好处,那使用Hooks就意味着可以高枕无忧了吗?并不是,凡事有利有弊,我们需要清楚它的局限性:

  • Hooks 暂时还不能完全代替类组件:比如getSnapshotBeforeUpdatecomponentDidCatch 这些生命周期,目前都还是强依赖类组件的。
  • Hooks 在使用层面有着严格的规则:只能在 React 函数中调用 Hook;不要在循环、条件或嵌套函数中调用 Hook

好家伙,你说类组件有心智负担,这玩意不也一样有吗?那不是自相矛盾?

这是一个成本与收益之前的博弈问题,而针对这个问题,React也给我们提供了eslint-plugin-react-hooks来帮助开发者减少这类问题引发的困难或bug,在项目配置中引入即可。

bash 复制代码
npm install eslint-plugin-react-hooks --save-dev
json 复制代码
// 你的 ESLint 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}

总结

React为了增强函数组件的能力, React Hooks 应运而生,为我们解决了从前开发中的一些痛点,如类组件的实例 this 指向、业务逻辑拆分和复用等,但也对开发者的水平提出了更高的要求,在使用Hooks的过程中需要注意遵守相关规则,好在React提供了工具帮助开发者减少这类负担。

最后,关于为什么React会提出上面的规则进行约束呢?下一篇从源码的角度来分析。

相关推荐
10年前端老司机2 小时前
10道js经典面试题助你找到好工作
前端·javascript
小小小小宇7 小时前
TS泛型笔记
前端
小小小小宇8 小时前
前端canvas手动实现复杂动画示例
前端
codingandsleeping8 小时前
重读《你不知道的JavaScript》(上)- 作用域和闭包
前端·javascript
小小小小宇8 小时前
前端PerformanceObserver使用
前端
zhangxingchao9 小时前
Flutter中的页面跳转
前端
前端风云志9 小时前
TypeScript实用类型之Omit
javascript
烛阴10 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝10 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇11 小时前
前端模拟一个setTimeout
前端