React进阶 React Hooks | 学习笔记
React Hooks
useState
useState
是react自带的一个hook函数,它的作用是用来声明状态变量。
- 那我们从三个方面来看
useState
的用法,分别是声明、读取、使用(修改)。这三个方面掌握了,你基本也就会使用useState
了. - 先来看一下声明的方式,上节课的代码如下:
const [ count , setCount ] = useState(0); |
- 这种方法是ES6语法中的数组解构,这样看起来代码变的简单易懂。现在ES6的语法已经在工作中频繁使用,所以如果你对ES6的语法还不熟悉,我觉的有必要拿出2天时间学习一下。 如果不写成数组解构,上边的语法要写成下面的三行:
let _useState = useState(0) |
-
useState
这个函数接收的参数是状态的初始值(Initial state),它返回一个数组,这个数组的第0位是当前的状态值,第1位是可以改变状态值的方法函数。 所以上面的代码的意思就是声明了一个状态变量为count,并把它的初始值设为0,同时提供了一个可以改变count
的状态值的方法函数。 -
这时候你已经会声明一个状态了,接下来我们看看如何读取状态中的值。
<p>You clicked {count} times</p> |
-
你可以发现,我们读取是很简单的。只要使用
{count}
就可以,因为这时候的count就是JS里的一个变量,想在JSX
中使用,值用加上{}
就可以。 -
最后看看如果改变
State
中的值,看下面的代码:
<button onClick={()=>{setCount(count+1)}}>click me</button> |
-
直接调用setCount函数,这个函数接收的参数是修改过的新状态值。接下来的事情就交给
React
,他会重新渲染组件。React
自动帮助我们记忆了组件的上一次状态值,但是这种记忆也给我们带来了一点小麻烦,但是这种麻烦你可以看成规则,只要准守规则,就可以愉快的进行编码。 -
比如现在我们要声明多个状态,有年龄(age)、性别(sex)和工作(work)。代码可以这么写.
import React, { useState } from 'react'; |
-
其实细心的小伙伴一定可以发现,在使用
useState
的时候只赋了初始值,并没有绑定任何的key
,那React是怎么保证这三个useState找到它自己对应的state呢? -
答案是:React是根据useState出现的顺序来确定的
-
比如我们把代码改成下面的样子:
import React, { useState } from 'react'; |
- 这时候控制台就会直接给我们报错,错误如下:
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render |
- 意思就是useState不能在
if...else...
这样的条件语句中进行调用,必须要按照相同的顺序进行渲染。如果你还是不理解,你可以记住这样一句话就可以了:就是React Hooks不能出现在条件判断语句中,因为它必须有完全一样的渲染顺序。
useEffect
- 为了让你更好的理解
useEffect
的使用,先用原始的方式把计数器的Demo增加两个生命周期函数componentDidMount
和componentDidUpdate
。分别在组件第一次渲染后在浏览器控制台打印出计数器结果和在每次计数器状态发生变化后打印出结果。代码如下:
import React, { Component } from 'react'; |
- 这就是在不使用Hooks情况下的写法,那如何用Hooks来代替这段代码,并产生一样的效果那。
- 在使用
React Hooks
的情况下,我们可以使用下面的代码来完成上边代码的生命周期效果,代码如下(修改了以前的diamond): 记得要先引入useEffect
后,才可以正常使用。
import React, { useState , useEffect } from 'react'; |
-
写完后,可以到浏览器中进行预览一下,可以看出跟
class
形式的生命周期函数是完全一样的,这代表第一次组件渲染和每次组件更新都会执行这个函数。 那这段代码逻辑是什么?我们梳理一下:首先,我们生命了一个状态变量count
,将它的初始值设为0,然后我们告诉react,我们的这个组件有一个副作用。给useEffecthook
传了一个匿名函数,这个匿名函数就是我们的副作用。在这里我们打印了一句话,当然你也可以手动的去修改一个DOM
元素。当React要渲染组件时,它会记住用到的副作用,然后执行一次。等Reat更新了State状态时,它再一词执行定义的副作用函数。 -
useEffect两个注意点
-
- React首次渲染和之后的每次渲染都会调用一遍
useEffect
函数,而之前我们要用两个生命周期函数分别表示首次渲染(componentDidMonut)和更新导致的重新渲染(componentDidUpdate)。
- React首次渲染和之后的每次渲染都会调用一遍
-
- useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数时异步执行的,而
componentDidMonut
和componentDidUpdate
中的代码都是同步执行的。个人认为这个有好处也有坏处吧,比如我们要根据页面的大小,然后绘制当前弹出窗口的大小,如果时异步的就不好操作了。
- useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数时异步执行的,而
-
在写React应用的时候,在组件中经常用到
componentWillUnmount
生命周期函数(组件将要被卸载时执行)。比如我们的定时器要清空,避免发生内存泄漏;比如登录状态要取消掉,避免下次进入信息出错。所以这个生命周期函数也是必不可少的,这节课就来用useEffect
来实现这个生命周期函数,并讲解一下useEffect
容易踩的坑。 -
学习
React Hooks
时,我们要改掉生命周期函数的概念(人往往有先入为主的毛病,所以很难改掉),因为Hooks
叫它副作用,所以componentWillUnmount
也可以理解成解绑副作用。这里为了演示用useEffect
来实现类似componentWillUnmount
效果,先安装React-Router
路由,进入项目根本录,使用npm
进行安装。
npm install --save react-router-dom |
- 然后打开
Example.js
文件,进行改写代码,先引入对应的React-Router
组件。
import { BrowserRouter as Router, Route, Link } from "react-router-dom" |
- 在文件中编写两个新组件,因为这两个组件都非常的简单,所以就不单独建立一个新的文件来写了。
function Index() { |
- 有了这两个组件后,接下来可以编写路由配置,在以前的计数器代码中直接增加就可以了。
return ( |
- 然后到浏览器中查看一下,看看组件和路由是否可用。如果可用,我们现在可以调整
useEffect
了。在两个新组件中分别加入useEffect()
函数:
function Index() { |
- 这时候我们点击
Link
进入任何一个组件,在浏览器中都会打印出对应的一段话。这时候可以用返回一个函数的形式进行解绑,代码如下:
function Index() { |
-
这时候你在浏览器中预览,我们仿佛实现了
componentWillUnmount
方法。但这只是好像实现了,当点击计数器按钮时,你会发现老弟,你走了!Index页面
,也出现了。这到底是怎么回事那?其实每次状态发生变化,useEffect
都进行了解绑。 -
那到底要如何实现类似
componentWillUnmount
的效果那?这就需要请出useEffect
的第二个参数,它是一个数组,数组中可以写入很多状态对应的变量,意思是当状态值发生变化时,我们才进行解绑。但是当传空数组[]
时,就是当组件将被销毁时才进行解绑,这也就实现了componentWillUnmount
的生命周期函数。
function Index() { |
- 为了更加深入了解第二个参数的作用,把计数器的代码也加上
useEffect
和解绑方法,并加入第二个参数为空数组。代码如下:
function Example(){ |
- 这时候的代码是不能执行解绑副作用函数的。但是如果我们想每次
count
发生变化,我们都进行解绑,只需要在第二个参数的数组里加入count
变量就可以了。代码如下:
function Example(){ |
- 这时候只要
count
状态发生变化,都会执行解绑副作用函数,浏览器的控制台也就打印出了一串=================
。
这节课学完我们就对useEffect
函数有了一个比较深入的了解,并且可以通过useEffect
实现生命周期函数了,也完成了本节课学习的目的,现在用React Hooks
这种函数的方法编写组件,对比以前用Class
编写组件几乎一样了。但这并不是Hooks
的所有东西,它还有一些让我们惊喜的新特性。
useContext
- 有了
useState
和useEffect
已经可以实现大部分的业务逻辑了,但是React Hooks
中还是有很多好用的Hooks
函数的,比如useContext
和useReducer
。 - 在用类声明组件时,父子组件的传值是通过组件属性和
props
进行的,那现在使用方法(Function)来声明组件,已经没有了constructor
构造函数也就没有了props的接收,那父子组件的传值就成了一个问题。React Hooks
为我们准备了useContext
。这节课就学习一下useContext
,它可以帮助我们跨越组件层级直接传递变量,实现共享。需要注意的是useContext
和redux
的作用是不同的,一个解决的是组件之间值传递的问题,一个是应用中统一管理状态的问题,但通过和useReducer
的配合使用,可以实现类似Redux
的作用。☆☆☆☆☆
这就好比玩游戏时有很多英雄,英雄的最总目的都是赢得比赛,但是作用不同,有负责输出的,有负责抗伤害的,有负责治疗的。
Context
的作用就是对它所包含的组件树提供全局共享数据的一种技术。
- createContext 函数创建context
- 直接在
src
目录下新建一个文件Example4.js
,然后拷贝Example.js
里的代码,并进行修改,删除路由部分和副作用的代码,只留计数器的核心代码就可以了。
import React, { useState , useEffect } from 'react'; |
- 然后修改一下
index.js
让它渲染这个Example4.js
组件,修改的代码如下。
import React from 'react'; |
- 之后在
Example4.js
中引入createContext
函数,并使用得到一个组件,然后在return
方法中进行使用。先看代码,然后我再解释。
import React, { useState , createContext } from 'react'; |
- 这段代码就相当于把
count
变量允许跨层级实现传递和使用了(也就是实现了上下文),当父组件的count
变量发生变化时,子组件也会发生变化。接下来我们就看看一个React Hooks
的组件如何接收到这个变量。
- 已经有了上下文变量,剩下的就时如何接收了,接收这个直接使用useContext就可以,但是在使用前需要新进行引入
useContext
(不引入是没办法使用的)。
import React, { useState , createContext , useContext } from 'react'; |
- 引入后写一个
Counter
组件,只是显示上下文中的count
变量代码如下:
function Counter(){ |
- 得到后就可以显示出来了,但是要记得在
<CountContext.Provider>
的闭合标签中,代码如下。
<CountContext.Provider value={count}> |
useReducer
-
为了更好的理解
useReducer
,所以先要了解JavaScript里的Redcuer
是什么。它的兴起是从Redux
广泛使用开始的,但不仅仅存在Redux
中,可以使用冈的JavaScript来完成Reducer
操作。那reducer
其实就是一个函数,这个函数接收两个参数,一个是状态,一个用来控制业务逻辑的判断参数。我们举一个最简单的例子。
function countReducer(state, action) { |
-
上面的代码就是Reducer,你主要理解的就是这种形式和两个参数的作用,一个参数是状态,一个参数是如何控制状态。
-
了解reducer的含义后,就可以讲useReducer了,它也是React hooks提供的函数,可以增强我们的
Reducer
,实现类似Redux的功能。我们新建一个Example5.js
的文件,然后用useReducer实现计数器的加减双向操作。(此部分代码的介绍可以看视频来学习)
import React, { useReducer } from 'react'; |
-
这段代码是useReducer的最简单实现了,这时候可以在浏览器中实现了计数器的增加减少。
-
修改
index.js
文件,让ReducerDemo
组件起作用。
import React from 'react'; |
useReducer+useContext代替Redux小案例
- 理论上的可行性
- 我们先从理论层面看看替代
Redux
的可能性,其实如果你对两个函数有所了解,只要我们巧妙的结合,这种替代方案是完全可行的。 useContext
:可访问全局状态,避免一层层的传递状态。这符合Redux
其中的一项规则,就是状态全局化,并能统一管理。useReducer
:通过action的传递,更新复杂逻辑的状态,主要是可以实现类似Redux
中的Reducer
部分,实现业务逻辑的可行性。- 经过我们在理论上的分析是完全可行的,接下来我们就用一个简单实例来看一下具体的实现方法。那这节课先实现
useContext
部分(也就是状态共享),下节再继续讲解useReducer
部分(控制业务逻辑)。 - 编写基本UI组件
- 既然是一个实例,就需要有些界面的东西,小伙伴们不要觉的烦。在
/src
目录下新建一个文件夹Example6
,有了文件夹后,在文件夹下面建立一个showArea.js
文件。代码如下:
import React from 'react'; |
- 显示区域写完后,新建一个
Buttons.js
文件,用来编写按钮,这个是两个按钮,一个红色一个黄色。先不写其他任何业务逻辑。
import React from 'react'; |
- 然后再编写一个组合他们的
Example6.js
组件,引入两个新编写的组件ShowArea
和Buttons
,并用<div>
标签给包裹起来。
import React, { useReducer } from 'react'; |
- 这步做完,需要到
/src
目录下的index.js
中引入一下Example6.js
文件,引入后React才能正确渲染出刚写的UI组件。
import React from 'react'; |
-
做完这步可以简单的预览一下UI效果,虽然很丑,但是只要能满足学习需求就可以了。我们虽然都是前端,但是在学习时没必要追求漂亮的页面,关键时把知识点弄明白。我们写这么多文件,也就是要为接下来的知识点服务,其实这些组件都是陪衬罢了。
-
编写颜色共享组件
-
有了UI组件后,就可以写一些业务逻辑了,这节课我们先实现状态共享,这个就是利用
useContext
。建立一个color.js
文件,然后写入下面的代码。
import React, { createContext } from 'react'; |
-
代码中引入了
createContext
用来创建共享上下文ColorContext
组件,然后我们要用{props.children}
来显示对应的子组件。 -
有了这个组件后,我们就可以把
Example6.js
进行改写,让她可以共享状态。
import React, { useReducer } from 'react'; |
- 然后再改写
showArea.js
文件,我们会引入useContext
和在color.js
中声明的ColorContext
,让组件可以接收全局变量。
import React , { useContext } from 'react'; |
-
在color.js中添加Reducer
-
颜色(state)管理的代码我们都放在了
color.js
中,所以在文件里添加一个reducer,用于处理颜色更新的逻辑。先声明一个reducer的函数,它就是JavaScript中的普通函数,在讲useReducer
的时候已经详细讲过了。有了reducer后,在Color组件里使用useReducer
,这样Color组件就有了那个共享状态和处理业务逻辑的能力,跟以前使用的Redux
几乎一样了。之后修改一下共享状态。我们来看代码:
import React, { createContext,useReducer } from 'react'; |
-
注意,这时候我们共享出去的状态变成了color和dispatch,如果不共享出去dispatch,你是没办法完成按钮的相应事件的。
-
通过dispatch修改状态
-
目前程序已经有了处理共享状态的业务逻辑能力,接下来就可以在
buttons.js
使用dispatch
来完成按钮的相应操作了。先引入useContext
、ColorContext
和UPDATE_COLOR
,然后写onClick
事件就可以了。代码如下:
import React ,{useContext} from 'react'; |
useMemo
-
useMemo
主要用来解决使用React hooks产生的无用渲染的性能问题。使用function的形式来声明组件,失去了shouldCompnentUpdate
(在组件更新之前)这个生命周期,也就是说我们没有办法通过组件更新前条件来决定组件是否更新。而且在函数组件中,也不再区分mount
和update
两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。useMemo
和useCallback
都是解决上述性能问题的,这节课先学习useMemo
. -
先编写一下刚才所说的性能问题,建立两个组件,一个父组件一个子组件,组件上由两个按钮,一个是小红,一个是志玲,点击哪个,那个就像我们走来了。在
/src
文件夹下,新建立一个Example7
的文件夹,在文件夹下建立一个Example7.js
文件.然后先写第一个父组件。
import React , {useState,useMemo} from 'react'; |
- 父组件调用了子组件,子组件我们输出两个姑娘的状态,显示在界面上。代码如下:
function ChildComponent({name,children}){ |
- 然后再导出父组件,让
index.js
可以渲染。
export default Example7 |
-
这时候你会发现在浏览器中点击
志玲
按钮,小红对应的方法都会执行,结果虽然没变,但是每次都执行,这就是性能的损耗。目前只有子组件,业务逻辑也非常简单,如果是一个后台查询,这将产生严重的后果。所以这个问题必须解决。当我们点击志玲
按钮时,小红对应的changeXiaohong
方法不能执行,只有在点击小红
按钮时才能执行。 -
useMemo 优化性能
-
其实只要使用
useMemo
,然后给她传递第二个参数,参数匹配成功,才会执行。代码如下:
function ChildComponent({name,children}){ |
这时在浏览器中点击一下志玲
按钮,changeXiaohong
就不再执行了。也节省了性能的消耗。案例只是让你更好理解,你还要从程序本身看到优化的作用。好的程序员对自己写的程序都是会进行不断优化的,这种没必要的性能浪费也是绝对不允许的,所以useMemo
的使用在工作中还是比较多的。
useRef
-
useRef
在工作中虽然用的不多,但是也不能缺少。它有两个主要的作用: -
用
useRef
获取React JSX中的DOM元素,获取后你就可以控制DOM的任何东西了。但是一般不建议这样来作,React界面的变化可以通过状态来控制。 -
用
useRef
来保存变量,这个在工作中也很少能用到,我们有了useContext
这样的保存其实意义不大,但是这是学习,也要把这个特性讲一下。 -
useRef获取DOM元素
-
界面上有一个文本框,在文本框的旁边有一个按钮,当我们点击按钮时,在控制台打印出
input
的DOM元素,并进行复制到DOM中的value上。这一切都是通过useRef
来实现。 -
在
/src
文件夹下新建一个Example8.js
文件,然后先引入useRef,编写业务逻辑代码如下:
import React, { useRef} from 'react'; |
-
当点击按钮时,你可以看到在浏览器中的控制台完整的打印出了DOM的所有东西,并且界面上的
<input/>
框的value值也输出了我们写好的Hello ,JSPang
。这一切说明我们可以使用useRef获取DOM元素,并且可以通过useRefu控制DOM的属性和值。 -
useRef保存普通变量
-
这个操作在实际开发中用的并不多,但我们还是要讲解一下。就是
useRef
可以保存React中的变量。我们这里就写一个文本框,文本框用来改变text
状态。又用useRef
把text
状态进行保存,最后打印在控制台上。写这段代码你会觉的很绕,其实显示开发中没必要这样写,用一个state状态就可以搞定,这里只是为了展示知识点。 -
接着上面的代码来写,就没必要重新写一个文件了。先用
useState
声明了一个text
状态和setText
函数。然后编写界面,界面就是一个文本框。然后输入的时候不断变化。
import React, { useRef ,useState,useEffect } from 'react'; |
- 这时想每次
text
发生状态改变,保存到一个变量中或者说是useRef
中,这时候就可以使用useRef
了。先声明一个textRef
变量,他其实就是useRef
函数。然后使用useEffect
函数实现每次状态变化都进行变量修改,并打印。最后的全部代码如下。
import React, { useRef ,useState,useEffect } from 'react'; |
- 这时候就可以实现每次状态修改,同时保存到
useRef
中了。也就是我们说的保存变量的功能。那useRef
的主要功能就是获得DOM和变量保存,我们都已经讲过了。
自定义Hooks和useCallback
-
其实自定义Hooks函数和用Hooks创建组件很相似,跟我们平时用JavaScript写函数几乎一模一样,可能就是多了些
React Hooks
的特性,自定义Hooks函数偏向于功能,而组件偏向于界面和业务逻辑。由于差别不大,所以使用起来也是很随意的。如果是小型项目是可以的,但是如果项目足够复杂,这会让项目结构不够清晰。所以学习自定义Hooks函数还是很有必要的。 -
编写自定义函数
-
在实际开发中,为了界面更加美观。获取浏览器窗口的尺寸是一个经常使用的功能,这样经常使用的功能,就可以封装成一个自定义
Hooks
函数,记住一定要用use开头,这样才能区分出什么是组件,什么是自定义函数。 -
新建一个文件
Example9.js
,然后编写一个useWinSize,编写时我们会用到useState
、useEffect
和useCallback
所以先用import
进行引入。import React, { useState ,useEffect ,useCallback } from 'react';
- 然后编写函数,函数中先用useState设置
size
状态,然后编写一个每次修改状态的方法onResize
,这个方法使用useCallback
,目的是为了缓存方法(useMemo是为了缓存变量)。 然后在第一次进入方法时用useEffect
来注册resize
监听时间。为了防止一直监听所以在方法移除时,使用return的方式移除监听。最后返回size变量就可以了。
function useWinSize(){
const [ size , setSize] = useState({
width:document.documentElement.clientWidth,
height:document.documentElement.clientHeight
})
const onResize = useCallback(()=>{
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
},[])
useEffect(()=>{
window.addEventListener('resize',onResize)
return ()=>{
window.removeEventListener('resize',onResize)
}
},[])
return size;
} - 然后编写函数,函数中先用useState设置
-
这就是一个自定义函数,其实和我们以前写的JS函数没什么区别,所以这里也不做太多的介绍。
- 编写组件并使用自定义函数
- 自定义
Hooks
函数已经写好了,可以直接进行使用,用法和JavaScript
的普通函数用起来是一样的。直接在Example9
组件使用useWinSize
并把结果实时展示在页面上。
function Example9(){ |
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps]) |
useImperativeHandle
可以让你在使用ref
时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与forwardRef
一起使用:
function FancyInput(props, ref) { |
- 在本例中,渲染
<FancyInput ref={inputRef} />
的父组件可以调用inputRef.current.focus()
。
useLayoutEffect
-
其函数签名与
useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。 -
尽可能使用标准的
useEffect
以避免阻塞视觉更新。
提示
如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意
useLayoutEffect
与componentDidMount
、componentDidUpdate
的调用阶段是一样的。但是,我们推荐你一开始先用useEffect
,只有当它出问题的时候再尝试使用useLayoutEffect
。如果你使用服务端渲染,请记住,无论
useLayoutEffect
还是useEffect
都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入useLayoutEffect
代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至useEffect
中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到useLayoutEffect
执行之前 HTML 都显示错乱的情况下)。若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用
showChild && <Child />
进行条件渲染,并使用useEffect(() => { setShowChild(true); }, [])
延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。
useDebugValue
useDebugValue(value) |
-
useDebugValue
可用于在 React 开发者工具中显示自定义 hook 的标签。 -
例如,“自定义 Hook” 章节中描述的名为
useFriendStatus
的自定义 Hook:
function useFriendStatus(friendID) { |
提示
我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。
延迟格式化 debug 值
-
在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。
-
因此,
useDebugValue
接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。 -
例如,一个返回
Date
值的自定义 Hook 可以通过格式化函数来避免不必要的toDateString
函数调用:
掘金:前端LeBron
知乎:前端LeBron
持续分享技术博文,关注微信公众号👇🏻