找回密码
 会员注册
查看: 36|回复: 0

换个角度思考ReactHooks

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64454
发表于 2024-9-21 02:16:30 | 显示全部楼层 |阅读模式
从Vue迁移到React,不太习惯ReactHooks的使用?也许换个角度思考Hooks出现的意义会对你有所帮助。1什么是Hooks简而言之,Hooks是个函数,通过使用Hooks可以让函数组件功能更加丰富。在某些场景下,使用Hooks是一个比使用类组件更好的主意。1.1Hooks出现的背景在Hooks出现之前,函数组件对比类组件(class)形式有很多局限,例如:不能使用state、ref等属性,只能通过函数传参的方式使用props没有生命周期钩子同时在类组件的使用中,也存在着不少难以解决的问题:在复杂组件中,耦合的逻辑代码很难分离组件化讲究的是分离逻辑与UI,但是对于平常所写的业务代码,较难做到分离和组合。尤其是在生命周期钩子中,多个不相关的业务代码被迫放在一个生命周期钩子中,需要把相互关联的部分拆封更小的函数。监听清理和资源释放问题当组件要销毁时,很多情况下都需要清除注册的监听事件、释放申请的资源。事件监听、资源申请需要在Mount钩子中申请,当组件销毁时还必须在Unmount勾子中进行清理,这样写使得同一资源的生成和销毁逻辑不在一起,因为生命周期被迫划分成两个部分。组件间逻辑复用困难在React中实现逻辑复用是比较困难的。虽然有例如renderprops、高阶组件等方案,但仍然需要重新组织组件结构,不算真正意义上的复用。抽象复用一个复杂组件更是不小的挑战,大量抽象层代码带来的嵌套地狱会给开发者带来巨大的维护成本。class学习成本与Vue的易于上手不同,开发React的类组件需要比较扎实的JavaScript基础,尤其是关于this、闭包、绑定事件处理器等相关概念的理解。Hooks的出现,使得上述问题得到了不同程度的解决。我认为了解Hooks出现的背景十分重要。只有知道了为什么要使用Hooks,知道其所能解决而class不能解决的问题时,才能真正理解Hooks的思想,真正享受Hooks带来的便利,真正优雅地使用Hooks。2Hooks基础让我们从最简单的Hooks使用开始。2.1useState这里贴上React文档中的示例:import React, { useState } from 'react';function Example() {  // 声明一个 "count" 的 state 变量  const [count, setCount] = useState(0);  return (           You clicked {count} times       setCount(count + 1)}>        Click me            );}useState就是一个Hooks,以前的函数组件是无状态的,但是有了Hooks后我们可以在函数中通过useState来获取state属性(count)以及修改state属性的方法(setCount)。整个Hooks运作过程:函数组件Example第一次执行函数时useState进行初始化,其传入的参数0就是count的初始值;返回的VDOM中使用到了count属性,其值为0;通过点击按钮,触发setCount函数,传入修改count的值,然后重新执行函数(就像类组件中重新执行render函数一样);第二次及以后执行函数时,依旧通过useState来获取count及修改count的方法setCount,只不过不会执行count的初始化,而是使用其上一次setCount传入的值。从使用最简单的Hooks我们可以知道。存储“状态”不再使用一个state属性。以往都是把所有状态全部放到state属性中,而现在有了Hooks我们可以按照需求通过调用多个useState来创建多个state,这更有助于分离和修改变量。const [count, setCount] = useState(0);const [visible, setVisible] = useState(false);const [dataList,setDataList] =useState([]);setCount传入的参数是直接覆盖,而setState执行的是对象的合并处理。总之useState使用简单,它为函数组件带来了使用state的能力。2.2useEffect在Hooks出现之前函数组件是不能访问生命周期钩子的,所以提供了useEffectHooks来解决钩子问题,以往的所有生命周期钩子都被合并成了useEffect,并且其解决了之前所提的关于生命周期钩子的问题。2.2.1实现生命周期钩子组合先举一个关于class生命周期钩子问题的例子,这里贴上React文档的示例:// Count 计数组件class Example extends React.Component {  constructor(props) {    super(props);    this.state = {      count: 0    };  }  componentDidMount() {    document.title = `你点击了 ${this.state.count} 次`;  }  componentDidUpdate() {    document.title = `你点击了 ${this.state.count} 次`;  }  render() {    return (               You clicked {this.state.count} times         this.setState({ count: this.state.count + 1 })}>          Click me                  );  }}可以看到当我们在第一次组件挂载(初始化)后以及之后每次更新都需要该操作,一个是初始化一个是更新后,这种情况在平时经常会遇到,有时候遇到初始化问题,就避免不了会写两次,哪怕是抽离成单独的函数,也必须要在两个地方调用,当这种写法多了起来后将会变得冗余且容易出bug。useEffect是怎么解决的?一个简单示例:import React, { useState, useEffect } from 'react';function Example() {  const [count, setCount] = useState(0);  // 效果如同 componentDidMount 和 componentDidUpdate:  useEffect(() => {    // 更新 title    document.title = `你点击了 ${count} 次`;  });  return (           You clicked {count} times       setCount(count + 1)}>        Click me            );}它把两个生命周期钩子合并在了一起。整个Hooks过程:Example组件第一次执行时,返回VDOM,渲染;渲染后从上至下按顺序执行useEffect;Example组件更新后,返回VDOM,渲染;渲染后从上至下按顺序执行useEffect。可以看到无论是初始化渲染还是更新渲染,useEffect总是会确保在组件渲染完毕后再执行,这就相当于组合了初始化和更新渲染时的生命周期钩子。并且由于闭包的特性,useEffect可以访问到函数组件中的各种属性和方法。useEffect里面可以进行“副作用”操作,例如:更变DOM(调用setCount)发送网络请求挂载监听不应该把“副作用”操作放到函数组件主体中,就像不应该把“副作用”操作放到render函数中一样,否则很可能会导致函数执行死循环或资源浪费等问题。2.2.2实现销毁钩子这就完了吗?没有,对于组件来说,有些其内部是有订阅外部数据源的,这些订阅的“副作用”如果在组件卸载时没有进行清除,将会容易导致内存泄漏。React类组件中还有个非常重要的生命周期钩子componentWillUnmount,其在组件将要销毁时执行。下面演示类组件是如何清除订阅的:// 一个订阅好友的在线状态的组件class FriendStatus extends React.Component {  constructor(props) {    super(props);    this.state = { isOnline: null };    this.handleStatusChange = this.handleStatusChange.bind(this);  }   // 初始化:订阅好友在线状态  componentDidMount() {    ChatAPI.subscribeToFriendStatus(      this.props.friend.id,      this.handleStatusChange,    );  }  // 更新:好友订阅更改  componentDidUpdate(prevProps) {    // 如果 id 相同则忽略    if (prevProps.friend.id === this.props.friend.id) {      return;    }    // 否则清除订阅并添加新的订阅    ChatAPI.unsubscribeFromFriendStatus(      prevProps.friend.id,      this.handleStatusChange,    );    ChatAPI.subscribeToFriendStatus(      this.props.friend.id,      this.handleStatusChange,    );  }  // 销毁:清除好友订阅  componentWillUnmount() {    ChatAPI.unsubscribeFromFriendStatus(      this.props.friend.id,      this.handleStatusChange,    );  }    // 订阅方法  handleStatusChange(status) {    this.setState({      isOnline: status.isOnline,    });  }  render() {    if (this.state.isOnline === null) {      return 'Loading...';    }    return this.state.isOnline ? 'Online' : 'Offline';  }}可以看到,一个好友状态订阅使用了三个生命周期钩子。那么使用useEffect该如何实现?function FriendStatus(props) {  const [isOnline, setIsOnline] = useState(null);  useEffect(() => {    function handleStatusChange(status) {      setIsOnline(status.isOnline);    }    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);        // 清除好友订阅    return function cleanup() {      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    };  });  if (isOnline === null) {    return 'Loading...';  }  return isOnline ? 'Online' : 'Offline';}useEffect把好友订阅相关的逻辑代码组合到了一起,而不像类组件那样把同一类型的逻辑代码按照生命周期来划分。其中return的函数是在useEffect再次执行前或是组件要销毁时执行,由于闭包,useEffect中的返回函数可以很容易地获取对象并清除订阅。整个Hooks过程:初始化函数组件FriendStatus,挂载VDOM;按顺序执行useEffect中传入的函数;更新:函数FriendStatus重新执行,重新挂载VDOM;执行上一次useEffect传入函数的返回值:清除好友订阅的函数;执行本次useEffect中传入的函数。2.2.3实现不同逻辑分离刚才讲的都是在一个场景下使用Hooks。现在将计数组件和好友在线状态组件结合并作对比。class FriendStatusWithCounter extends React.Component {  constructor(props) {    super(props);    this.state = { count: 0, isOnline: null };    this.handleStatusChange = this.handleStatusChange.bind(this);  }  componentDidMount() {    document.title = `你点击了 ${count} 次`;    ChatAPI.subscribeToFriendStatus(      this.props.friend.id,      this.handleStatusChange    );  }  componentDidUpdate() {    document.title = `你点击了 ${count} 次`;  }  componentWillUnmount() {    ChatAPI.unsubscribeFromFriendStatus(      this.props.friend.id,      this.handleStatusChange    );  }    componentDidUpdate(prevProps) {    // 如果 id 相同则忽略    if (prevProps.friend.id === this.props.friend.id) {      return;    }    // 否则清除订阅并添加新的订阅    ChatAPI.unsubscribeFromFriendStatus(      prevProps.friend.id,      this.handleStatusChange,    );    ChatAPI.subscribeToFriendStatus(      this.props.friend.id,      this.handleStatusChange,    );  }  handleStatusChange(status) {    this.setState({      isOnline: status.isOnline    });  }可以很明显地感受到,在多个生命周期钩子中,计数和好友订阅等逻辑代码都混合在了同一个函数中。接下来看看useEffect是怎么做的:function FriendStatusWithCounter(props) {  // 计数相关代码  const [count, setCount] = useState(0);  useEffect(() => {    document.title = `你点击了 ${count} 次`;  });  // 好友订阅相关代码  const [isOnline, setIsOnline] = useState(null);  useEffect(() => {    function handleStatusChange(status) {      setIsOnline(status.isOnline);    }    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    return () => {      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    };  });  // ...}useEffect可以像使用多个useState那样,把组件的逻辑代码进行分离和组合,更有利于组件的开发和维护。2.2.4跳过useEffect有些时候并没有必要每次在函数组件重新执行时执行useEffect,这个时候就需要用到useEffect的第二个参数了。第二个参数传入一个数组,数组元素是要监听的变量,当函数再次执行时,数组中只要有一个元素与上次函数执行时传入的数组元素不同,那么则执行useEffect传入的函数,否则不执行。给个示例会更好理解:function FriendStatus(props) {  const [isOnline, setIsOnline] = useState(null);  useEffect(() => {    function handleStatusChange(status) {      setIsOnline(status.isOnline);    }    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);        // 清除好友订阅    return function cleanup() {      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    };        // 加入 props.friend.id 作为依赖,当 id 改变时才会执行该次 useEffect  }, [props.friend.id]);  if (isOnline === null) {    return 'Loading...';  }  return isOnline ? 'Online' : 'Offline';}给useEffect加入id的依赖,只有当id改变时,才会再次清除、添加订阅,而不必每次函数重新执行时都会清除并添加订阅。需要注意的是,对于传入的对象类型,React只是比较引用是否改变,而不会判断对象的属性是否改变,所以建议依赖数组中传入的变量都采用基本类型。3真正的Hooks刚才只是Hooks的简单使用,但是会使用并不能代表着真正理解到了Hooks的思想。从类组件到函数组件不仅仅是使用Hooks的区别,更重要的是开发时根本上思维模式的变化。让我们换个角度思考。3.1useEffect——远不止生命周期很多人认为useEffect只是生命周期钩子的更好替代品,这是不完全正确的。试想一下这样的场景:一个图表组件Chart需要接收大量的数据然后对其进行大量计算处理(getDataWithinRange())并做展示。类组件:// 大量计算处理function getDataWithinRange() {  //...}class Chart extends Component {  state = {    data: null,  }  componentDidMount() {    const newData = getDataWithinRange(this.props.dateRange)    this.setState({data: newData})  }  componentDidUpdate(prevProps) {    if (prevProps.dateRange != this.props.dateRange) {      const newData = getDataWithinRange(this.props.dateRange)      this.setState({data: newData})    }  }  render() {    return (          )  }}当使用生命周期钩子时,我们需要手动去判断哪些数据(dataRange)发生了变化,然后更新到对应的数据(data)。而在Hooks的使用中,我们只需关注哪些值(dataRange)需要进行同步。使用useEffect的函数组件:const Chart = ({ dateRange }) => {  const [data, setData] = useState()    useEffect(() => {    const newData = getDataWithinRange(dateRange)    setData(newData)  }, [dateRange])    return (      )}useEffect可以让你有更简单的想法实现保持变量同步。不过这还不够简单,我们可以再看下一个例子。3.2强大的useMemo事实上,刚才Hooks中的例子还是有些类组件的思维模式,显得有些复杂了。使用useEffect进行数据的处理;存储变量到state;在JSX中引用state。有没有发现中间多了个state的环节?我们不需要使用state,那是类组件的开发模式,因为在类组件中,render函数和生命周期钩子并不是在同一个函数作用域下执行,所以需要state进行中间的存储,同时执行的setState让render函数再次执行,借此获取最新的state。而在函数式组件中我们有时根本不会需要用到state这样的状态存储,我们仅仅是想使用。所以我们可以把刚才的图表例子写成这样:const Chart = ({ dateRange }) => {    const data = useMemo(() => (    getDataWithinRange(dateRange)  ), [dateRange])    return (      )}useMemo会返回一个“记忆化”的结果,执行当前传入的函数并返回结果值给声明的变量,且当依赖没变化时返回上一次计算的值。为什么可以这样写?因为函数组件中render和生命周期钩子在同一个函数作用域中,这也就意味着不再需要state作中间数据桥梁,我们可以直接在函数执行时获取到处理的数据,然后在return的JSX中使用,不必需要每次使用属性都要在state中声明和创建了,不再需要重新渲染执行一次函数(setData)了,所以我们去除掉了useState。这样,我就减少了一个state的声明以及一次重新渲染。我们把变量定义在函数里面,而不是定义在state中,这是类组件由于其结构和作用域上与函数组件相比的不足,是函数组件的优越性。当然,如果getDataWithinRange函数开销不大的话,这样写也是可以的:const Chart = ({ dateRange }) => {  const newData = getDataWithinRange(dateRange)  return (      )}在函数上下文中进行数据的处理和使用,是类结构组件所难以实现的。如果还没有体会到Hooks所带来的变化,那么下面的例子可能会令你有所领悟。3.3多个数据依赖上一个例子我们只要处理一个数据就可以了,这次我们尝试处理多条数据,并且数据间有依赖关系。需求如下:需要对传入的dataRange进行处理得到data当margins改变后需要更新dimensions当data改变后需要更新scales类组件:class Chart extends Component {  state = {    data: null,    dimensions: null,    xScale: null,    yScale: null,  }  componentDidMount() {    const newData = getDataWithinRange(this.props.dateRange)    this.setState({data: newData})    this.setState({dimensions: getDimensions()})    this.setState({xScale: getXScale()})    this.setState({yScale: getYScale()})  }  componentDidUpdate(prevProps, prevState) {    if (prevProps.dateRange != this.props.dateRange) {      const newData = getDataWithinRange(this.props.dateRange)      this.setState({data: newData})    }    if (prevProps.margins != this.props.margins) {      this.setState({dimensions: getDimensions()})    }    if (prevState.data != this.state.data) {      this.setState({xScale: getXScale()})      this.setState({yScale: getYScale()})    }  }  render() {    return (          )  }}函数组件:const Chart = ({ dateRange, margins }) => {  const data = useMemo(() => (    getDataWithinRange(dateRange)  ), [dateRange])  const dimensions = useMemo(getDimensions, [margins])  const xScale = useMemo(getXScale, [data])  const yScale = useMemo(getYScale, [data])  return (      )}为什么代码那么少?因为在Hooks中我们依旧只需关注哪些值(data、dimensions、xScale、yScale)需要同步即可。而观察类组件的代码,我们可以发现其使用了大量的陈述性代码,例如判断是否相等,同时还使用了state作为数据的存储和使用,所以产生了很多setState代码以及增加了多次重新渲染。3.4解放State还是刚才3.3的例子,不过把需求稍微改了一下:让scales依赖于dimensions。看看类组件是如何做到的:class Chart extends Component {  state = {    data: null,    dimensions: null,    xScale: null,    yScale: null,  }  componentDidMount() {    const newData = getDataWithinRange(this.props.dateRange)    this.setState({data: newData})    this.setState({dimensions: getDimensions()})    this.setState({xScale: getXScale()})    this.setState({yScale: getYScale()})  }  componentDidUpdate(prevProps, prevState) {    if (prevProps.dateRange != this.props.dateRange) {      const newData = getDataWithinRange(this.props.dateRange)      this.setState({data: newData})    }    if (prevProps.margins != this.props.margins) {      this.setState({dimensions: getDimensions()})    }    if (      prevState.data != this.state.data      || prevState.dimensions != this.state.dimensions    ) {      this.setState({xScale: getXScale()})      this.setState({yScale: getYScale()})    }  }  render() {    return (          )  }}由于依赖关系发生了变化,所以需要重新进行判断,并且由于多个依赖关系,判断的条件也变得更加复杂了,代码的可读性也大幅降低。接着看Hooks是如何做到的:const Chart = ({ dateRange, margins }) => {  const data = useMemo(() => (    getDataWithinRange(dateRange)  ), [dateRange])  const dimensions = useMemo(getDimensions, [margins])  const xScale = useMemo(getXScale, [data, dimensions])  const yScale = useMemo(getYScale, [data, dimensions])  return (      )}使用Hooks所以不用再去关心谁是props谁是state,不用关心该如何存储变量,存储什么变量等问题,也不必去关心如何进行判断的依赖关系。在Hooks开发中,我们把这些琐碎的负担都清除了,只需关注要同步的变量。所以当数据关系复杂起来的时候,类组件的这种写法显得比较笨重,使用Hooks的优势也就体现出来了。再回顾一下之前一步步走过来的示例,可以看到Hooks帮我们精简了非常多的代码。代码越短并不意味着可读性越好,但是更加精简、轻巧的组件,更容易让我们把关注点放在更有用的逻辑上,而不是把精力消耗在判断依赖的冗余编码中。4参考文章React官方文档ThinkinginReactHooks紧追技术前沿,深挖专业领域扫码关注我们吧!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-28 08:25 , Processed in 0.915043 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表