主要介绍useStateuseEffectuseReduceruseContext用法你还在为使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?——有了hooks,你不再需要写Class,你所有的组件都将是Function。你还在因为想不通使用哪个生命周期钩子函数而失眠吗?——有了Hooks,生命周期钩子函数可以先放一边。你还在为组件中的this指针感到困惑吗?——既然Class丢了,这是哪里?这是你生命中的第一次,你不再需要面对这个。最简单的Hooks之一先来看一个简单的有状态组件:image让我们看看使用hooks后的版本:是不是简单多了!可以看到Example变成了一个函数,但是这个函数有自己的状态(count),它也可以更新自己的状态(setCount)。这个函数之所以厉害,是因为它注入了一个hook——useState,让我们的函数成为一个有状态的函数。除了useStatehook,还有很多其他的hook。比如useEffect提供componentDidMount等类似生命周期钩子的功能,useContext提供context函数等。钩子本质上是一类特殊的函数,可以将一些特殊的功能注入到你的函数组件中。嗯?这听起来有点像被诟病的Mixins?Mixins会在React中复活吗?当然不是,后面我们会讲到两者的区别。总而言之,这些钩子的目标是让你不再写类,让函数称霸世界。为什么React有Hooks?复用一个有状态的组件太麻烦了!我们都知道react的核心思想是将一个页面拆分成一堆独立的、可重用的组件,并以自上而下的单向数据流的形式将这些组件连接在一起。但是如果你在一个大型的工作项目中使用react,你会发现你项目中的很多react组件其实很冗长,难以复用。尤其是那些写成类的组件,它们本身就包含状态(state),这样的组件复用就变得很麻烦。在此之前,官方推荐是如何解决这个问题的?答案是:渲染道具和高阶组件。我们可以简单地看一下这两种模式。rendering属性是指使用一个值为函数的prop来传递需要动态渲染的节点或组件。从下面的代码可以看出,我们的DataProvider组件包含了所有状态相关的代码,而Cat组件可以是一个纯展示组件,这样DataProvider就可以单独复用了。尽管这种模式称为RenderProps,但这并不意味着您必须使用名为render的prop。人们通常这样写:高层组件的概念更好理解。说白了,一个函数接受一个组件作为参数,经过一系列的处理,最后返回一个新的组件。看下面的代码示例,withUser函数是一个高阶组件,它返回一个新的组件,这个组件具有获取它提供的用户信息的功能。生命周期钩子函数里的逻辑太乱了!我们通常希望一个函数只做一件事情,但是我们的生命周期钩子函数通常同时做很多事情。比如我们需要在componentDidMount中发起ajax请求获取数据,绑定一些事件监听器等等。同时,有时我们需要在componentDidUpdate中做同样的事情。当项目变得复杂时,这个区域的代码就变得不那么直观了。课程真的很混乱!我们在使用class创建react组件的时候,还有一个很麻烦的事情,就是this的指向问题。为了保证this指向正确,我们经常会这样写代码:this.handleClick=this.handleClick.bind(this),或者这样的代码:this.handleClick(e)}>.一旦不小心忘记绑定this,各种bug就会随之而来,非常麻烦。还有一件事困扰着我。正如我在之前的React系列文章中所说,尽可能将你的组件写成无状态组件,因为它们更便于复用,并且可以独立测试。然而,很多时候,我们是用函数来写一个简单而完美的无状态组件。后来由于需求的变化,这个组件必须要有自己的状态,我们也很难把function改成class。在这样的背景下,Hooks诞生了!什么是状态钩子?回到我们一开始用的例子,我们分解一下状态钩子是干什么的:声明一个状态变量useState是react自带的一个钩子函数,它的作用就是声明状态变量。useState函数接收的参数是我们的状态初始值(initialstate),它返回一个数组,这个数组的第[0]项是当前当前状态值,第[1]项是可以改变状态值的方法功能。所以我们所做的其实就是声明一个状态变量count,将其初始值设置为0,并提供一个可以改变count的函数setCount。上面的表达式是借鉴了es6的数组解构(arraydestructuring),可以让我们的代码看起来更简洁。如果不知道这个用法,可以先看我的文章:30分钟掌握ES6/ES2015核心内容(上)。如果不需要解构数组,可以这样写。事实上,数组解构是一个非常昂贵的事情。使用下面的写法,或者改用对象解构,性能会有很大的提升。具体可以看这篇文章的分析:Arraydestructuringformulti-valuereturns(inlightofReacthooks),这里不做详述,按照官方推荐使用arraydestructuring即可。读取状态值是不是超级容易?因为我们的状态计数只是一个简单的变量,所以我们不需要再写{this.state.count}了。更新状态当用户点击按钮时,我们调用setCount函数,它接收修改后的新状态值。接下来就是react,react会重新渲染我们的Example组件,并使用更新后的新状态,即count=1。这里我们不得不停下来想一想,Example本质上就是一个普通的函数,为什么它能记住之前的状态呢?为什么钩子记住状态而不是每次初始化?到这里我们就找到了问题所在。一般来说,我们在函数中声明的变量会在函数运行结束时被销毁(这里不考虑闭包等),例如考虑下面的例子:我们重复调用了多少次add函数,result为1。因为每次我们调用add时,result变量的初始值都是0。那为什么上面的Example函数每次执行的时候都以上次执行的状态值作为初始值呢?答案是:React帮助我们记住了。至于react用什么机制来记忆,留个悬念吧。如果一个组件有多个状态值怎么办?首先,useState是可以调用多次的,所以我们可以这样写:其次,useState接收到的初始值并没有规定必须是string/number/boolean等简单数据类型,完全可以接收对象或数组作为参数。唯一需要注意的是,之前我们的this.setState做的是合并状态返回新状态,而useState直接替换旧状态返回新状态。最后,如果您更喜欢redux风格的状态管理解决方案,React还为我们提供了一个useReducer钩子。从ExampleWithManyStates函数中我们可以看出无论调用多少次useState都是相互独立的。这是至关重要的。你为什么这么说?其实我们看hooks的“形式”,有点类似于之前被官方否决的Mixins方案,它们都提供了“可插拔函数注入”的能力。之所以拒绝mixins,是因为mixins机制允许多个mixins共享一个对象的数据空间,所以很难保证不同mixins所依赖的状态不冲突。现在我们的钩子,一方面是直接在函数中使用,而不是在类中使用;另一方面,每个hook相互独立,不同的组件调用同一个hook也可以保证各自状态的独立性。这是两者的本质区别。React如何保证多个useState相互独立?还是看上面给出的ExampleWithManyStates例子,我们调用了3次useState,每次传递的参数都只是一个值(比如42,'banana'),并没有告诉react这些值对应哪个keyto,那么react是如何保证这三个useStates找到对应状态的呢?答案是react是根据useState出现的顺序来的。我们具体看一下:如果我们改一下代码:这样,鉴于此,React规定我们必须在函数的最外层写hooks,而不是在ifelse等条件语句中,以保证hooks的执行顺序是一致的。什么是效果钩子?让我们在上一节的示例中添加一个新函数:让我们比较一下。没有钩子我们怎么写呢?我们写的有状态组件通常会有很多副作用,比如发起ajax请求获取数据,添加一些监控注册和注销,手动修改dom等等。我们之前在生命周期函数钩子中写过这些副作用函数,比如componentDidMount、componentDidUpdate、componentWillUnmount。当前的useEffect就是这些生命周期函数钩子的集合。这是一个对三个。同时,由于上面提到的钩子可以重复使用,所以它们之间是相互独立的。所以我们合理的做法是给每个sideeffect一个单独的useEffecthook。这样,这些副作用就不再堆积在生命周期钩子中,代码也变得更加清晰。useEffect有什么作用?我们再梳理一下下面这段代码的逻辑:首先,我们声明了一个状态变量count,并将其初始值设置为0。然后我们告诉react,我们的组件有一个sideeffect。我们传递了一个匿名函数给useEffecthook,这个匿名函数就是我们的sideeffect。在这个例子中,我们的副作用是调用浏览器API来修改文档标题。当React渲染我们的组件时,它首先会记住我们使用的副作用。React更新DOM后,依次执行我们定义的副作用函数。这里有几点需要注意:首先,传递给useEffect的函数在react的第一次渲染和之后的每次渲染都会被调用一次。之前,我们用两个生命周期函数来表示第一次渲染(componentDidMount)和后续更新引起的重新渲染(componentDidUpdate)。其次,useEffect中定义的副作用函数的执行不会阻止浏览器更新视图,也就是说,这些函数是异步执行的,而前面的componentDidMount或componentDidUpdate中的代码是同步执行的。除了某些情况外,这种安排对于大多数副作用都是合理的。例如,有时我们需要在重新渲染之前根据DOM计算元素的大小。这时,我们希望重新渲染同步发生,这意味着它会在浏览器实际绘制页面之前发生。如何解绑useEffect的一些副作用这种场景很常见。当我们在componentDidMount中添加注册时,我们必须立即清除我们在componentWillUnmount中添加的注册,即在组件未注册之前,否则会出现内存泄漏的问题。.如何清除它?让我们将副作用函数传递给useEffect以返回一个新函数。这个新功能将在下一次重新渲染组件后执行。这种模式在pubsub模式的一些实现中很常见。看下面的例子:这里有一点需要注意!这种解绑方式不同于componentWillUnmount。componentWillUnmount只会在组件销毁前执行一次,每次组件渲染时都会执行useEffect中的函数,包括sideeffect函数返回的cleanup函数也会再次执行。那么让我们来看看下面的问题。为什么每次更新组件都要执行副作用函数?先看前面的模式:很清楚,我们在componentDidMount注册,然后在componentWillUnmount清除注册。但是如果此时props.friend.id改变了怎么办?我们不得不再添加一个componentDidUpdate来处理这种情况:看到了吗?这很繁琐,但是我们使用useEffect没有这个问题,因为它会在每次组件更新后重新执行。所以代码的执行顺序是这样的:第一次渲染页面注册一个friend.id=1的朋友,突然friend.id变成2,重新渲染页面清除friend的绑定.id=1注册一个friend.id=2的朋友...如何跳过一些不必要的副作用函数按照上一节的思路,每次重新执行这些副作用函数显然是不经济的使成为。如何跳过一些不必要的计算?我们只需要将第二个参数传递给useEffect。使用第二个参数告诉react只有当这个参数的值发生变化时才执行我们传递的副作用函数(第一个参数)。当我们传递一个空数组[]作为第二个参数时,其实相当于只在第一次渲染时才执行。即componentDidMount加componentWillUnmount的方式。不过这种用法可能会导致bug,所以少用。还有哪些其他内置的EffectHooks?useReduceruseReducer是hooks提供的一个类似redux的api,可以让我们通过action,或者state来管理上下文//reducer.jsconstinitialState={count:0};functionreducer(state,action){switch(action.type){case'reset':返回初始状态;case'increment':return{count:state.count+1};case'decrement':return{count:state.count-1};}}exportdefaultfunctionCounter({initialCount}){const[state,dispatch]=useReducer(//const[state,dispatch]=useReducer(reducer,initialState);reducer,initialState,{type:'reset',payload:initialCount},//,在初始渲染期间应用初始动作);return(<>Count:{state.count}dispatch({type:'reset'})}>重置dispatch({type:'增量'})}>+dispatch({type:'decrement'})}>->);}useContextconstcontext=useContext(Context);接受一个上下文(context)对象(来自React.createContext)并返回由最近的上下文提供者给出的当前上下文值。当提供程序更新时,此Hook将触发使用最新上下文值的重新渲染。//reducer.jsimportReact,{useReducer}来自"react";constinitialState=0;constmyContext=React.createContext();functionreducer(state,action){switch(action.type){case"reset":return初始状态;case"increment":return{count:state.count+1};case"decrement":return{count:state.count-1};默认值:返回状态;}}constContextProvider=props=>{const[state,dispatch]=useReducer(reducer,{count:0});return({props.children});};export{reducer,myContext,ContextProvider};//Counter.jsimportReact,{useContext}来自“反应”;从“./reducer”导入{myContext};函数计数器(){const{state,dispatch}=useContext(myContext);return(CounterCount:{state.count}dispatch({type:"reset"})}>重置dispatch({type:"increment"})}>+dispatch({type:"decrement"})}>-
);}exportdefaultCounter;//CounterTest.jsimportReact,{useContext}from"react";import{myContext}from"./reducer";functionCounterTest(){const{state,dispatch}复制代码=useContext(myContext);return(CounterTestCount:{state.count}dispatch({type:"reset"})}>重置dispatch({type:"increment"})}>+dispatch({type:"decrement"})}>-
);}exportdefaultCounterTest;//index.jsimportReactfrom"react";import{ContextProvider}from"./reducer";importCounterfrom"./Counter";importCounterTestfrom"./CounterTest";constApp=()=>{返回(
);};constrootElement=document.getElementById("root");ReactDOM.render(,根元素);除了上面重点介绍的useState和useEffect,useContext、useReducer、react也为我们提供了很多有用的hook:useCallbackuseMemouseRefuseImperativeMethodsuseMutationEffectuseLayoutEffect我就不一一介绍了。可以自行查看官方文档HowtowritecustomEffectHooks?为什么要自己写一个EffectHooks?这样,我们就可以把可重用的逻辑抽取出来,变成可以随意插拔的“插头”。想用哪个组件我就插上,soeasy!看一个完整的例子,你就会明白。比如我们可以从上面写的FriendStatus组件中提取出判断好友是否在线的功能,新建一个useFriendStatushook来判断某个id是否在线。这时候FriendStatus组件可以简写为:如果此时我们还有另外一个好友列表,我们还需要显示在线信息: