内置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开发了。