内置hooks(1):如何保存组件状态和使用生命周期?

一、基本hooks的用法

  如果你用过基于类的组件,那对组件的生命周期函数一定不会陌生,例如componentDidMount,componentDidUpdate,等等。如果没有使用过,也没关系,基于hooks去考虑组件的实现,这会是一个非常不同的思路,你完全不用去关心一个组件的生命周期是怎样的。遇到需求直接考虑hooks中去如何实现。

  react提过的hooks其实非常少,一共只有10个,比如useState、useEffect、useCallBack、useMemo、useRef、useContext等等,接下来我们先学习useState和useEffect这两个最核心的hooks。掌握了hooks,那90%的react都可以开发。

  只写hooks的功能其实非常简单,多看看官网文档就可以了,我们要学习的是如何用hooks的思路去实现功能

二、实践

  1.useState:让函数组件具有持续状态的能力

  state是react组件的一个核心机制,那么useState这个hooks就是用来管理state的,他可以让函数组件具有维持状态的能力。也就是说,在一个函数组件的多次渲染之间,这个state是共享的。下面的例子时显示useState的用法:

import React,{useState} from 'react';

function Example(){
   //创建一个保存count的state,并给初始0
   const [count,setCount]=useState(0);
   return(
       <div>
           <p>{count}</p>
            <button onClick={()=>setCount(count+1)}>
      +
</button>
</div>
)  
}

  在这个例子中,我们生命了一个名为count的state,并得到了设置这个count的值的函数setCount.当调用setCount时,count这个state就会被更新,并且触发组件的刷新,那末useState这个hook的用法总结出来就是这样的:

1.useState(initialState)的参数initialState时创建state的初始值,他可以时任意类型,比如数字、对象、数组等等。

2.useState()的返回值时一个有着两个元素的数组。第一个数组杨素是用来读取state的值,第二个则是用来设置这个state的值。这里要注意的时,state的变量(例子中的count)时只读的所以我们必须通过第二个数组元素setCount来设置他的值。

3.如果要创建多个state,那么我们就需要多次调用useState.比如要创建多个state,使用的代码如下:

//定义一个年龄的state,初始值时42
const [age,setAge]=useState(42);
//定义一个水果的state,初始值是banana
const [fruit,setFruit]=useState('banbana');
//定义一个一个数组state,初始值是包含一个todo的数组
const [todos,setTodos]=useState([{text:'Learn Hooks'}])

从这段代码中可以看到,useState是一个非常简单的hook,他让你很方便地区创建一个状态,并且提供一个特定的方法(比如setAge)来设置这个状态。

  如果你之前用过类组件,那么这里的useState就和组件的setState非常类似。不过两者最大的区别就在于,类组件中的state只能有一个。以我们一般把对象作为一个state,然后再通过不同的属性来表示不同的状态。而函数组件中用useState则可以很容易的创建多个state,所以他更加语义化。

  可以说,state是react组件非常重要的一个机制,那么什么样的值应该保存再state中呢?通常来说,我们要遵循的一个原则就是:state中永远不要保存课可以通过计算得到的值。比如说:

1.从props传递过来的值。有时候props传递过来的值无法直接使用,而是要通过一定的计算后再在ui展示,比如说排序。那么我们要做的事每次用的时候,都重新排序一下,或者利用某些cache机制,而不是直接放到state中。

2.从url中读取到的值。比如有些需要读取url中的参数,把他作为组件的一部分状态。那么我们可以在每次需要用的时候从url中读取,而不是读取出来直接放到state中。

3.从cookie、localStorage中读取的值。通常来说,也是每次要用的时候直接去读取,而不是读出来后放到state里。

  2.useEffect:执行副作用

useEffect,顾名思义,用于执行一段副作用

  什么是副作用?通常来说,副作用是指一段和当前执行结果无关的代码。比如说要修改函数外部的某个变量,要发起一个请求,等等。也就是说在函数组件的当此执行过程中,

useEffect中代码的执行不是影响渲染出来的UI的。

我们先来看看他具体的用法。useEffect可以接收连个参数函数签名如下

useEffect(callback,dependencies)

  第一个参数为要执行的函数callback,第二个是可选的依赖项数组dependencies。其中依赖项是可选的,如果不指定,那么callback就会在每次函数组件执行完之后都执行;如果指定了,那么只有依赖项中的值发生变化的时候,他才会执行。

  对应到class组件,那么useEffect就涵盖了componentDidMount、componentDidUpdate和componentWillUnmount三个生命周期方法。不过你习惯了使用class组件,那千万不要按照把useEffect对应到某个或者某几个生命周期的用法,需要记住的是,useEffect是每次组件render后完成判断依赖并执行就可以了。

  举个例子,某个组件用于显示一篇Blog文章,那么这个组件会接收一个参数来表示Blog的id。当id发生变化的时候,组件需要发起请求来获取文章内容并展示:

import React,{useState,useEffect} from"react";

function BlogView({id}){
    //设置一个本地state用于保存blog内容
   const [blogeContent,setBlogContent]=useState(null);
  useEffect(()=>{
     //useEffect的callback要避免直接的async函数,需要封装一下
    const doAsync=async ()=>{
     //当id发生变化时,将当前内容清楚以保持一致性
     setBlogContent(null);
     //发起请求
    const res=await fecch(`/blog-content/${id}/`);
//将获取的数据放入state
    setBlogContent(await res.text());
};
doAsyc();

},[id]);//使用id作为依赖项,变化时则执行副作用
//如果没有blogContent则认为时在loading状态
const isloading=!blogContent;
return <div>
{isLoading?"Loading...."blogContent}
</div>

}

  这样,我们就利用useEffect完成了一个简单的数据请求的需求,在这段代码中,我们把ID作为依赖项参数,这样就很自然的在id发生变化时,利用useEffect执行副作用去获取数据。如果在之前的类组件中完成类似的需求,我们就需要在componentDidUpdate这个方法里,自己去判断两次id是否发生变化。如果变了,才去发请求。这样的话,逻辑上就不如useEffect来的直观。

useEffect还有两个特殊的用法:没有依赖项,以及依赖项作为空数组。代码如下

1.没有依赖项,则每次render后都会重新执行。例如:

useEffect(()=>{
   //每次render完一定执行
console.log(‘re-enderred’)

})

2.空数组作为依赖项,则只在首次执行时触发,对应到class组件就是componentDidMount.例如:

useEffect(()=>{
  //组件首次渲染时执行,等价于class组件中的componentDidMount
 console.log('did mount');;
},[])

除了这些机制之外,useEffect还允许你返回一个函数,用于在组件销毁的时候做一些清丽的操作。比如移除事件的监听。这个机制就几乎等价于类组件中componentWillUnnoint.举个例子,在组件中,我们需要监听窗口的大小编变化,以便做一些布局上的调整:

//设置一个size的用于保存当前窗口尺寸
const [size,setSize]=useState({});

useEffect(()=>{
  //窗口大小变化事件处理函数
  const handler=()=>{
setSize(getSize());
};
//监听resize事件
window.addEventListener('resize',handler)
//返回一个callback在组件销毁时调用
return ()=>{
  //移除resize事件
  window.removueEventLisTener('resize',hander)
}

})

  总结一下,useEffect让我们能够在下面四种时机去执行一个回调函数产生副作用:

1.每次render后执行:不提供第二个依赖项参数。比如useEffect(()=>{})。

2.仅第一次render后执行:提过一个空数组作为依赖项。比如useEffect(()=>{},[])

3.第一次以及依赖项发生那个变化后执行:提供依赖项数组。比如useEffect(()=>{},[deps]).

4.组件unMount后执行:返回一个回调函数。比如useEffect(()=>{return ()=>{}},[])

  定义依赖项时,我们需要注意一下三点:

1.依赖项中定义的变量一定会在回调函数中用到,否则声明依赖项其实时没有意义的,

2.依赖项一般为一个常量数组,而不是一个变量。因为一般在创建callBack的时候,你其实是非常清楚其中要用到那些依赖项了

3.react会使用浅比较来对比依赖项是否发生了变化,所以要特别注意数组或者对象类型。如果你是每次创建一个新对象,即使和之前是等价的,也会被认为是依赖项发生了变化,这是一个开始使用hooks是很容易导致bug的地方。例如下代码

function sample(){
  //这里在每次组件执行时创建了一个新数组
   const todos=[{text:'learn hooks'}];
  useEffect(()=>{
   console.log('todos changed')

},[todos]);  
}

代码的愿意可能是todos变化的时候产生一些副作用,但是这里的todos变量是在函数内部创建的,实际上每次产生了一个新数组。所以在作为依赖项的时候进行引用的比较,实际上被认为是发生了变化的。

  掌握hooks的使用规则

HOOKS本身作为纯粹的js函数,不是通过特殊的API去创建的,而是直接定义一个函数。他需要在降低学习成本和使用成本的同时,还要遵循一定的规则才能正常工作。因而hooks的使用规则包括以下两个:只能在函数组件的顶级作用域使用:只能在函数组件或者hooks中使用。

Hooks只能在函数组件的顶级作用域使用

  所谓顶级作用域,就是hooks不能再循环、条件判断或者嵌套函数内执行,而必须是再顶层。同时Hooks在组件的多次渲染之间,必须按照顺序被执行。因为在react组件内部,其实是维护了一个对应组件的固定hooks执行列表的,以便在多次渲染之间保持hooks的状态。

  例如:下面代码是可行的,因为hooks一定会被执行到:

function MyComp(){
    const [count,setCount]=userState(0);
    return <div>
{count}
</div>
}

  而下面代码是错误的,因为在某些条件下hooks是不会被执行到的:

function MyComp(){
   const [count,setCount]=useState(0);
   if(count>10){
    //错误:不能将hooks用在条件判断里
   useEffect(()=>{
  //...
},[count])
//这里可能提前放回组件渲染结果,后面就不能在用hooks了
if(count==0){
    return 'No content';
}
//错误:不能将hooks放在可能的return之后
const [loding,setLoading]=useState(false);
//...
return <div>
{count}
</div>
}

}

  所以hooks的这个规则可以总结为两点:第一,所有hooks必须要被执行到。第二,必须按顺序执行。

Hooks只能在函数组件或者其他hooks中使用

  hooks作为专门为函数设计的机制,使用的情况只有两种,一种是在函数组件内,另一种是在自定义hooks里面。

这个规则在函数组件和类组件同时存在的项目中,可能造成一定和困扰,因为hooks简介、直观,我们可能都倾向于hooks来实现逻辑的重用,但是如果一定要在class组件中使用,有一个通用的机制,那就是利用高阶函数组件 模式,将hooks封装成高阶组件,从而让类组件使用。

例如:

import React from 'react';
import { useWindowSize } from '../hooks/useWindowSize';

export const withWindowSize = (Comp) => {
  return props => {
    const windowSize = useWindowSize();
    return <Comp windowSize={windowSize} {...props} />;
  };
};
//高阶组件写法

import React from 'react';
import { withWindowSize } from './withWindowSize';

class MyComp {
  render() {
    const { windowSize } = this.props;
    // ...
  }
}

// 通过 withWindowSize 高阶组件给 MyComp 添加 windowSize 属性
export default withWindowSize(MyComp);

三、总结

  在这里我们学习了useState和useEffect这两个核心hooks的用法,一个用于保存装填,一个用于执行副作用。可以说掌握了这俩个hooks,几乎就能完成大部分的react开发了。

posted @ 2022-02-18 12:01  前端乔  阅读(778)  评论(0编辑  收藏  举报