React从Class方式转Hooks

Posted Vicky沛沛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React从Class方式转Hooks相关的知识,希望对你有一定的参考价值。

React Hooks

前言

之前工作三年中一直在用class方式去写前端功能页面这些,其实接触hooks也是有一定时间了。在第一次接触的时候应该是看了一门关于electron+react的项目的课程的时候。当时主要是去看electron,所以对hooks没有特别的关注。也有可能是长期以来对class的熟悉,当时对hooks这种函数式的写法也有一些抵触。但是由于业内对hooks的好评,曾经也想去用hooks去起一个项目,但是由于当时项目周期以及已有的技术栈等原因,一直想实践也没机会去实践使用。

由于,最近接手新的项目一直使用的是react hooks+ts这一套。于是,就得开始使用hooks了。其实就功能开发而言,照猫画虎,其实工作也没有什么问题的。但是由于一直没有系统的对hooks进行深入一步的了解,导致在很多时候,其实并不是很清楚为什么要这样去使用,于是最近去找了《React Hooks核心原理与实战》去进行了学习。

在使用层面以及原因层面上,重新审视hooks的使用以及为什么要使用hooks。函数式写法究竟有哪些好处进行了更进一步的思考。其实,在一定程度而言,也还是浅尝辄止,这里仅仅将我这段时间学习记录下来。

Why Hooks ?

Hooks很大的一个亮点是可以进行业务逻辑的重用。这一点在hooks中体现的尤为明显。比如,往常的class中如果要去监听窗口大小的变化的时候,就得在组件中在挂载后去添加监听事件,但是如果当另外一个地方需要用到这种监听窗口大小功能的话,这种逻辑代码并不可以复用,只能在那个组件中重新写一遍。但是在hooks中,我们可以将这部分监听的逻辑代码进行hooks方式封装,完全可以做到逻辑上的复用。

For Class

使用Class作为React的载体的时候:

  • 组件之间不会相互继承,没有利用到class的继承的特性
  • UI是状态驱动的,所有的方法都是内部调用或者作为生命周期的方法内部自动调用。没有使用到类的实例方法可以调用的特性

For Function

React中的一个核心就是要实现从State数据到View试图层面的一个绑定。使用函数,其实更好的去解决State到View的一个映射问题。但是,用函数作为React的载体就会出现两个问题,函数中状态的保存以及生命周期的方法

  • Hooks怎样去解决上面两个问题:把一个外部的数据绑定到函数的执行。当数据变化时,让函数能够自动重新执行。这样的话,任何会影响 UI 展现的外部数据,都可以通过这个机制绑定到 React 的函数组件。
  • Hooks钩子的理解:把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果

图解:一个执行过程(Execution),例如是函数组件本身,可以绑定在(钩在)传统意义的 State,或者 URL,甚至可以是窗口的大小。这样当 State、URL、窗口大小发生变化时,都会重新执行某个函数,产生更新后的结果。

Class & Hooks 对比

  • 比起 Class 组件,函数组件是更适合去表达 React 组件的执行的,因为它更符合 State => View 这样的一个逻辑关系。但是因为缺少状态、生命周期等机制,让它一直功能受限。Hooks解决了函数组件作为React载体的状态生命周期等受限问题,让其功能充分发挥出来

  • Hooks 中被钩的对象,可以是某个独立的数据源,也可以是另一个 Hook 执行的结果,这就带来了 Hooks 的最大好处:逻辑的复用

  • 简化了逻辑复用

    • Class方式中:使用高阶组件的设计模式进行逻辑复用。比如:我们要去复用一个窗口resize的功能,我们需要去定义一个没有UI的外层组件,去写相关resize的逻辑定义,然后将数据结果用属性的方式传给子组件。组件要复用这个逻辑的话,必须外层用这个组件包裹并返回。针对整个而言,**为了传递一个外部的状态,我们不得不定义一个没有 UI 的外层组件,而这个组件只是为了封装一段可重用的逻辑。**频繁使用,每一个高阶组件的使用都会多一层节点,会给调试等带来很大的负担。
    //Class中高阶组件实现resize方法复用
    
    //1、高阶组件的声明
    const withWindowSize = Component => 
      // 产生一个高阶组件 WrappedComponent,只包含监听窗口大小的逻辑
      class WrappedComponent extends React.PureComponent 
        constructor(props) 
          super(props);
          this.state = 
            size: this.getSize()
          ;
        
        componentDidMount() 
          window.addEventListener("resize", this.handleResize); 
        
        componentWillUnmount() 
          window.removeEventListener("resize", this.handleResize);
        
        getSize() 
          return window.innerWidth > 1000 ? "large""small";
        
        handleResize = ()=> 
          const currentSize = this.getSize();
          this.setState(
            size: this.getSize()
          );
        
        render() 
          // 将窗口大小传递给真正的业务逻辑组件
          return <Component size=this.state.size />;
        
      
      return WrappedComponent;
    ;
    
    //2、组件MyComponent使用高阶组件中的resize功能
    class MyComponent extends React.Component
      render() 
        const  size  = this.props;
        if (size === "small") return <SmallComponent />;
        else return <LargeComponent />;
      
    
    // 使用 withWindowSize 产生高阶组件,用于产生 size 属性传递给真正的业务组件
    export default withWindowSize(MyComponent); 
    
    • Hooks方式中:实现resize的话,窗口大小只是外部的一个数据状态。我们使用hooks方式对其封装,只是将其变成了一个可以绑定的数据源,当窗口大小发生变化的时候,这个组件也会重新渲染代码会更加简洁直观,并且不会产生额外的组件节点。
    //Hooks中使用hooks方法进行resize逻辑复用
    
    //定义useWindowSize这个hook
    const getSize = () => 
      return window.innerWidth > 1000 ? "large" : "small";
    
    const useWindowSize = () => 
      const [size, setSize] = useState(getSize());
      useEffect(() => 
      const handler = () => 
          setSize(getSize())
        ;
        window.addEventListener('resize', handler);
        return () => 
          window.removeEventListener('resize', handler);
        ;
      , []);
      
      return size;
    ;
    
    //函数组件中使用这个hook
    const Demo = () => 
      const size = useWindowSize();
      if (size === "small") return <SmallComponent />;
      else return <LargeComponent />;
    ;
    
  • 有助于关注分离

Hooks能够让针对同一个业务逻辑的代码尽可能聚合在一块,在Class组件中不得不吧同一个业务逻辑代码分散在类组件的不同的生命周期方法中

图解:左侧是 Class 组件,右侧是函数组件结合 Hooks。蓝色和黄色代表不同的业务功能

Hooks如何保存组件状态和使用生命周期?

React一共提供了10个Hooks,useStateuseEffectuseCallbackuseMemouseRefuseContext等等

1、useState:让函数具有维持状态的能力

我们要遵循的一个原则就是:state 中永远不要保存可以通过计算得到的值,例如:

  • 从 props 传递过来的值。有时候 props 传递过来的值无法直接使用,而是要通过一定的计算后再在 UI 上展示,比如说排序。那么我们要做的就是每次用的时候,都重新排序一下,或者利用某些 cache 机制,而不是将结果直接放到 state 里。
  • 从 URL 中读到的值。比如有时需要读取 URL 中的参数,把它作为组件的一部分状态。那么我们可以在每次需要用的时候从 URL 中读取,而不是读出来直接放到 state 里。
  • 从 cookie、localStorage 中读取的值。通常来说,也是每次要用的时候直接去读取,而不是读出来后放到 state 里。

2、useEffect:执行副作用

副作用是指一段和当前执行结果无关的代码。比如说要修改函数外部的某个变量,要发起一个请求。形式:useEffect(callback, dependencies)。涵盖了componentDidMountcomponentDidUpdatecomponentWillUnmount三个生命周期方法。简而言之,useEffect 是每次组件 render 完后判断依赖并执行。

使用useEffect应该注意的点:
  • 没有依赖项,则每次 render 后都会重新执行
useEffect(()=>
	console.log('re-render')		//每次render完成一次后就执行
)
  • 空数组作为依赖项,则只在首次执行时触发,对应到 Class 组件就是 componentDidMount
useEffect(()=>
  console.log('did mount')		//相当于componentDidMount
,[])
  • 可以返回一个函数,用在组件销毁的时候做一些清理的操作
const [size,setResize] = useState()
useEffect(()=>
	const handler = () => 
    setResize()
	
	window.addEventListener('resize',handler)
	return ()=>
		window.removeEventListener('resize',handler)
	
,[])
总结useEffect使用的四个场景
  • 每次 render 后执行:不提供第二个依赖项参数。比如useEffect(() => )。
  • 仅第一次 render 后执行:提供一个空数组作为依赖项。比如useEffect(() => , [])。
  • 第一次以及依赖项发生变化后执行:提供依赖项数组。比如useEffect(() => , [deps])。
  • 组件 unmount 后执行:返回一个回调函数。比如useEffect() => return () => , [])。

3、关于Hooks的依赖

在hooks定义中我们已经可以看到依赖项的一个概念,那么定义这个依赖项的时候,究竟应该去注意一些什么呢?

  • 依赖项一定会在回调函数中使用到,否则声明依赖项的意义失去了
  • 依赖项一般是一个常量数组,而不是一个变量
  • react会使用浅比较来对比依赖项是否发生了变化,所以特别要注意数组或者对象这种类型。如果每次都是创建一个新的对象,即使和之前的是等价的也会被认为是依赖项发生了变化(这个地方十分容易产生bug),例如:
function Sample()
	const todos = [text:'vicky demo']
	useEffect(()=>
		console.log('todos change')
	,[todos])

解释:这里的 todos 变量是在函数内创建的,实际上每次都产生了一个新数组。所以在作为依赖项的时候进行引用的比较,实际上被认为是发生了变化的。

4、Hooks的使用规则

  • 只能在函数组件的顶级作用域使用;只能在函数组件或者其他 Hooks 中使用。

  • 顶层作用域,就是 Hooks 不能在循环、条件判断或者嵌套函数内执行,而必须是在顶层。同时 Hooks 在组件的多次渲染之间,必须按顺序被执行。

5、useCallback:缓存回调函数

React函数组件中,每一次UI的变化都是通过重新执行整个函数来完成的,函数组件中并没有一个直接的方式在多次渲染之间维持一个状态。例如,通过demo理解:

function Counter()
	const [count,setCount] = useState(0)
	const handleAdd = () => setCount(count+1)
	
	//...other code 
	return <button onClick=handleAdd>+</button>

  • 每次组件状态发生变化的时候,函数组件都会重新执行一边
  • 每次执行的时候,函数内部都会重新创建一个handleAdd的事件处理函数(这个事件处理函数包含了count这个变量的一个闭包用于保证每次执行的拿到正确的结果),实际上并不是每次的函数组件重新渲染的时候都需要去重新创建一个事件处理函数。虽然结果不影响,但是增加了系统开销、并且每次创建新的函数的方式会让接收事件处理函数的组件重新渲染(因为,比如这个例子中的button,接收了handleAdd,如果handleAdd发生改变就会认为这个组件的props发生了变化从而去重新渲染)
  • 实际上,我们需要做到的是只有count发生变化的时候才会去重新定义这个组件。保证了组件不会创建重复的回调函数。而接收这个回调函数作为属性的组件,也不会频繁地需要重新渲染

6、useMemo:缓存计算结果

  • 使用场景:

    • 某个数据是通过其他数据计算得到的。只有当用到的数据(依赖)发生变化的时候才需要重新计算
  • 使用useMemo和不使用区别:

    • 不使用:每次重新渲染都需要重新计算
    • 使用:只有当依赖数据变化的时候才需要重新计算
    • demo说明:
      • 场景:显示用户信息列表,需要对用户进行搜索筛选
      • 功能需要的两个状态:筛选字段、用户列表数据本身
      • 不使用useMemo:不论什么原因组件刷新,一定会再做一次筛选计算
      • 使用useMemo:只在依赖项改变时候去筛选计算
  • 优点:

    • 避免重复计算
    • 避免子组件重复渲染

7、useRef:在多次渲染之间共享数据

可以把useRef看作函数组件之外创建的一个容器空间,这个容器空间上我们可以通过唯一的current属性设置一个值,从而可以在多次渲染之间共用这个值。

  • 使用场景:

    • window.setInterval计时组件提供计时功能,需要保存这个计数器的引用方便停止和开始,这时候采用useRef去保存十分合适
    export default const Timer () 
    	const [time,setTime] = useState(0)
    	const timer = useRef(null)
    	const handleStart = useCallback(()=>
    		timer.current = window.setInterval(()=>
    			setTime(time=>time+1)
    		,100)
    	,[])
    	const handlePause = useCallback(()=>
    		window.clearInterval(timer.current)
    	,[])
    	return (
        <div>
        	time/10seconds
        	<br/>
        	<button onClick=handleStart>Start</button>
        	<button onClick=handlePause>End</button>
        </div>
    	)
    
    
    • 保存dom节点的引用
    function TextFocusBtn()
      const inputEl = useRef(null)
      const onBtnClick = () => 
      	inputEl.current.focus()
      
      return (
        <>
        	<input ref=inputEl type="text"/>
        	<button onClick=onBtnClick></button>
        </>
      )
    
    
    
  • useRef保存的数据一般是和UI渲染无关的,因此ref的值发生变化的时候是不会触发组件的重新渲染的

8、useContext:定义全局状态

  • 目的:跨层次或者同层的组件之间进行数据的共享
const themes =  light:  foreground: "#000000", background: "#eeeeee" , dark:  foreground: "#ffffff", background: "#222222" ;
const ThemeContext = React.createContext(theme)
function App()
	return <ThemeContext.Provider>
		//...
		<ToolBar/>
	</ThemeContext.Provider>


function ToolBar()
	return <div>
		<ThemeBtn/>
	</div>


function  ThemeBtn()
	const theme = useContext(ThemeContext)
	return <button style=theme.dark>Style Theme Btn</button>

  • why?为什么不使用全局变量而要去使用这种复杂机制进行共享?能够进行数据的绑定,也就是Context数据变化的时候需要自动刷新

如何正确理解函数的组件的生命周期?

  • 函数组件中思考的方式应该是:某个状态变化时候,我要做什么

  • Class中constructor作用:进行一次初始化工作。Hooks中怎样进行这一点?Hooks中其实是没有必要进行一次统一数据状态初始化的工作的(本质:某些代码只执行一次)。但是如果要实现在组件挂载之后进行一次初始化操作的工作我们要怎样去实现呢?

function useSingle(callback)
	const called = useRef(false)	//这里使用useRef不用useCall是因为使用useRef不会影响UI重新渲染,useState会导致多余的渲染
	if(called.current)return;
	callback();
	called.current = false;


const MyDemo = () => 
	useSingle(()=>
		console.log('这个函数只执行一次')
	)
	return <div>demo</div>

//注意:
//1、使用useEffect传[]也是可以实现只执行一次的工能,但是useEffect是在render完成之后执行的,自定义可以在函数题执行之前执行
//2、useRef是当前组件实例上的一个变量,所以组件重新mount或者多实例是互不相关的。也就是一个组件中多次使用这个useSingle自定义hooks是互不干扰的
  • hooks与Class生命周期如何对应?
useEffect(()=>
	//这里基本等价于componentDidMount和componentWillUpdate
	return () => 
		//这里相当于componentWillUnmount。这里用于清理上一次 Effect 的结果,并且只会在组件销毁前(componentWillUnmount)执行一次
	
,[deps])

Hooks四个典型使用场景

核心优点:

  • 方便逻辑复用
  • 关注分离

思考:

  • 功能开发时候思考:这个功能中哪些逻辑是可以抽出来成为独立Hooks的
  • Hooks 和普通函数在语义上是有什么区别?区别在于函数中有没有用到其它 Hooks
  • **拆分逻辑的目的不一定是为了重用,而可以是仅仅为了业务逻辑的隔离。**所以在这个场景下,我们不一定要把 Hooks 放到独立的文件中,而是可以和函数组件写在一个文件中。

四个使用场景

1、抽取业务逻辑(例如:之前的count demo)
2、封装通用逻辑(例如:useAsync)
3、监听浏览器状态(例如:useScroll)
4、拆分复杂组件(例如:)

useAsync

import  useState  from 'react';

const useAsync = (asyncFunction) => 
  // 设置三个异步逻辑相关的 state
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  // 定义一个 callback 用于执行异步逻辑
  const execute = useCallback(() => 
    // 请求开始时,设置 loading 为 true,清除已有数据和 error 状态
    setLoading(true);
    setData(null);
    setError(null);
    return asyncFunction()
      .then((response) => 
        // 请求成功时,将数据写进 state,设置 loading 为 false
        setData(response);
        setLoading(false);
      )
      .catch((error) => 
        // 请求失败时,设置 loading 为 false,并设置错误状态
        setError(error);
        setLoading(false);
      );
  , [asyncFunction]);

  return  execute, loading, data, error ;
;

useScroll

import  useState, useEffect  from 'react';

// 获取横向,纵向滚动条位置
const getPosition = () => 
  return 
    x: document.body.scrollLeft,
    y: document.body.scrollTop,
  ;
;
const useScroll = () => 
  // 定一个 position 这个 state 保存滚动条位置
  const [position, setPosition] = useState(getPosition());
  useEffect(() => 
    const handler = () => 
      setPosition(getPosition(document));
    ;
    // 监听 scroll 事件,更新滚动条位置
    document.addEventListener("scroll", handler);
    return () => 
      // 组件销毁时,取消事件监听
      document.removeEventListener("scroll", handler);
    ;
  , []);
  return position;
;

Reudx在函数组件中的使用

redux做了什么事情?

  • 用全局唯一的 Store 维护了整个应用程序的状态

图解:左图未使用redux,右图使用redux

state、action、reducer关系

  • reducer是一个纯函数。对store的修改都通过action描述动作去纯函数修改store而不是直接修改store数据
  • 优点:
    • 可预测性
    • 易于调试

redux的使用逻辑

redux处理异步逻辑

  • 引入中间件去进行处理。thunk、saga的中间件角色

  • 常用的thunk和saga其实只是中间件作用,在action和reducer中间添加一层去进行异步处理,action和reducer的逻辑不变

以上是关于React从Class方式转Hooks的主要内容,如果未能解决你的问题,请参考以下文章

React从Class方式转Hooks

React从Class方式转Hooks

React从Class方式转Hooks

React Hooks 快速入门:从一个数据请求开始

如何使用 React Hooks 实现复杂组件的状态管理

React Hooks 介绍及与传统 class 组件的生命周期函数对比