React-Hooks

本文最后更新于:2 年前

在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI 组件我们可以使用函数,无状态组件来展示 UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给 UI 组件进行渲染。在我看来,使用 React Hooks 相比于从前的类组件有以下几点好处:

  1. 代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护
  2. 组件树层级变浅,在原本的代码中,我们经常使用 HOC/render props 等方式来复用组件的状态,增强功能等,无疑增加了组件树层数及渲染,而在 React Hooks 中,这些功能都可以通过强大的自定义的 Hooks 来实现

配置环境

第一步:安装 node.js

官网下载安装https://nodejs.org/en/

第二步:安装 create-react-app 脚手架工具

1
cnpm install -g create-react-app

第三步:创建 react 项目

1
create-react-app demo01

npm start启动项目时可能会出错

删除 node_modules 文件夹,在项目下的命令行输出入cnpm install重新安装依赖包

第四步:精简代码

vscode 打开项目,精简代码

demo01\src\Example.js 新建一个文件

image-20210206164703130.png

Example.js 中原来 react 写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react'

export default class Example extends Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}

render() {
return (
<div>
<p> You clicked {this.state.count} times</p>
<button onClick={this.addCount.bind(this)}>Click me</button>
</div>
)
}
addCount() {
this.setState({ count: this.state.count + 1 })
}
}

并在 inde.js 中调用import Example from './Example'以及<Example>

hooks 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState } from 'react'

function Example() {
//函数执行几次? n+1次 因为useState执行时有缓存,不会将count重新设置成0,会继续累加
console.log('函数执行')
const [count, setCount] = useState(0)
return (
<div>
<p> You clicked {count} times</p>
<button
onClick={() => {
setCount(count + 1)
}}>
Click me
</button>
</div>
)
}
export default Example

useState 有两个参数[count,setCount], count 就是元素的状态,setCount 是更新元素状态的方法,
const [count, setCount] = useState(0)//是 ES6 解构赋值写法,useState(0)等于{ count: 0 }

Example()函数执行几次? n+1 次 因为 React 底层做了处理,useState 执行时有缓存,将上一次执行的状态保存,不会将 count 重新设置成 0,会继续累加

hooks 是 react 新特性,react16.8 以上才可以用

image.png

useState

在 demo01\src\Example2.js 中,先写一个错误例子useState不能用在条件语句中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState } from 'react'
let showSex = true

function Example2() {
const [age, setAge] = useState(18)
if (showSex) {
const [sex, setSex] = useState('男') //useState不能用在条件语句中
showSex = false
}
const [work, setWork] = useState('程序员')

return (
<div>
<p>JS今年:{age}</p>
<p>性别:{sex}</p>
<p>工作:{work}</p>
</div>
)
}
export default Example2

正确写法就是 React Hooks 不能出现在条件判断语句中,因为它必须有完全一样的渲染顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState } from 'react'
function Example2() {
const [age, setAge] = useState(18)
const [sex, setSex] = useState('男')
const [work, setWork] = useState('程序员')

return (
<div>
<p>JS今年:{age}</p>
<p>性别:{sex}</p>
<p>工作:{work}</p>
</div>
)
}
export default Example2

useEffect 代替常用生命周期函数

image.png
useEffect 是异步

原始的方式

先用原始的方式把计数器的 Demo 增加两个生命周期函数componentDidMountcomponentDidUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { Component } from 'react'

export default class Example extends Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
componentDidMount() {
console.log(`componentDidMount=>You clicked ${this.state.count} times`)
}
componentDidUpdate() {
console.log(`componentDidUpdate=>You clicked ${this.state.count} times`)
}
render() {
return (
<div>
<p> You clicked {this.state.count} times</p>
<button onClick={this.addCount.bind(this)}>Click me</button>
</div>
)
}
addCount() {
this.setState({ count: this.state.count + 1 })
}
}

useEffect 实现 componentDidUpdate 生命周期函数

useEffect不传参数时,监听所有的状态,类似componentDidUpdate,只要页面的 state 或者 model 中的 state 中定义的变量值发生改变,这个方法就会执行

1
2
3
useEffect(() => {
console.log('componentDidUpdate')
})

useEffect 实现 componentDidMonut 生命周期函数

useEffect传第二个参数时[ ],第二个参数是要监听哪些状态的改变,类似componentDidMonut,组件挂在完成后执行

1
2
3
useEffect(() => {
console.log('componentDidMonut')
}, [])

使用React Hooks的情况下

我们可以使用下面的代码来完成上边代码的生命周期效果,代码如下(修改了以前的 diamond): 记得要先引入useEffect后,才可以正常使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { useState, useEffect } from 'react'
function Example() {
const [count, setCount] = useState(0)
//---关键代码---------start-------
useEffect(() => {
console.log(`useEffect=>You clicked ${count} times`)
})
//useEffect不传参数时,类似componentDidUpdate,useEffect传第二个参数时,类似componentDidMonut
// useEffect(()=>{
// console.log(`useEffect=>You clicked ${count} times`)
// },[])

//---关键代码---------end-------

return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => {
setCount(count + 1)
}}>
click me
</button>
</div>
)
}
export default Example

useEffect 两个注意点

  1. React 首次渲染和之后的每次渲染都会调用一遍useEffect函数,而之前我们要用两个生命周期函数分别表示首次渲染(componentDidMonut)和更新导致的重新渲染(componentDidUpdate)。
  2. useEffect 中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数时异步执行的,而componentDidMonutcomponentDidUpdate中的代码都是同步执行的。个人认为这个有好处也有坏处吧,比如我们要根据页面的大小,然后绘制当前弹出窗口的大小,如果时异步的就不好操作了。

useEffect 实现 componentWillUnmount 生命周期函数

在 useEffect 函数里return ( )=>{ }再 return 一个函数,这个函数会在组件卸载之前执行类似componentWillUnmount

1
2
3
4
5
6
useEffect(() => {
//返回的是一个函数,这个函数会在组件卸载之前执行类似componentWillUnmount
return () => {
console.log('componentWillUnmount.....')
}
}, [])

在写 React 应用的时候,在组件中经常用到componentWillUnmount生命周期函数(组件将要被卸载时执行)。比如我们的定时器要清空,避免发生内存泄漏;比如登录状态要取消掉,避免下次进入信息出错。定时器没有关闭等

useEffect 关闭定时器

1
2
3
4
5
6
7
8
9
React.useEffect(() => {
let timer = setInterval(() => {
setCount(count => count + 1)
}, 1000)
//返回的是一个函数,这个函数会在组件卸载之前执行
return () => {
clearInterval(timer)
}
}, [])

useEffect 解绑副作用

学习React Hooks 时,我们要改掉生命周期函数的概念(人往往有先入为主的毛病,所以很难改掉),因为Hooks叫它副作用,所以componentWillUnmount也可以理解成解绑副作用。这里为了演示用useEffect来实现类似componentWillUnmount效果,先安装React-Router路由,进入项目根本录,使用npm进行安装。

1
npm install --save react-router-dom

然后打开Example.js文件,进行改写代码,先引入对应的React-Router组件。

1
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'

在文件中编写两个新组件,因为这两个组件都非常的简单,所以就不单独建立一个新的文件来写了。

1
2
3
4
5
6
7
function Index() {
return <h2>JSPang.com</h2>
}

function List() {
return <h2>List-Page</h2>
}

有了这两个组件后,接下来可以编写路由配置,在以前的计数器代码中直接增加就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => {
setCount(count + 1)
}}>
click me
</button>

<Router>
<ul>
<li>
{' '}
<Link to="/">首页</Link>{' '}
</li>
<li>
<Link to="/list/">列表</Link>{' '}
</li>
</ul>
<Route path="/" exact component={Index} />
<Route path="/list/" component={List} />
</Router>
</div>
)

然后到浏览器中查看一下,看看组件和路由是否可用。如果可用,我们现在可以调整useEffect了。在两个新组件中分别加入useEffect()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Index() {
useEffect(()=>{
console.log('useEffect=>老弟,你来了!Index页面')
)
return <h2>JSPang.com</h2>;
}

function List() {
useEffect(()=>{
console.log('useEffect=>老弟,你来了!List页面')
})

return <h2>List-Page</h2>;
}

这时候我们点击Link进入任何一个组件,在浏览器中都会打印出对应的一段话。这时候可以用返回一个函数的形式进行解绑,代码如下:

1
2
3
4
5
6
7
8
9
function Index() {
useEffect(() => {
console.log('useEffect=>老弟你来了!Index页面')
return () => {
console.log('老弟,你走了!Index页面')
}
})
return <h2>JSPang.com</h2>
}

这时候你在浏览器中预览,我们仿佛实现了componentWillUnmount方法。但这只是好像实现了,当点击计数器按钮时,你会发现老弟,你走了!Index页面,也出现了。这到底是怎么回事那?其实每次状态发生变化,useEffect都进行了解绑。

useEffect 的第二个参数

那到底要如何实现类似componentWillUnmount的效果那?这就需要请出useEffect的第二个参数,它是一个数组,数组中可以写入很多状态对应的变量,意思是当状态值发生变化时,我们才进行解绑。但是当传空数组[]时,就是当组件将被销毁时才进行解绑,这也就实现了componentWillUnmount的生命周期函数。

1
2
3
4
5
6
7
8
9
function Index() {
useEffect(() => {
console.log('useEffect=>老弟你来了!Index页面')
return () => {
console.log('老弟,你走了!Index页面')
}
}, [])
return <h2>JSPang.com</h2>
}

为了更加深入了解第二个参数的作用,把计数器的代码也加上useEffect和解绑方法,并加入第二个参数为空数组。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Example() {
const [count, setCount] = useState(0)

useEffect(() => {
console.log(`useEffect=>You clicked ${count} times`)

return () => {
console.log('====================')
}
}, [])

return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => {
setCount(count + 1)
}}>
click me
</button>

<Router>
<ul>
<li>
{' '}
<Link to="/">首页</Link>{' '}
</li>
<li>
<Link to="/list/">列表</Link>{' '}
</li>
</ul>
<Route path="/" exact component={Index} />
<Route path="/list/" component={List} />
</Router>
</div>
)
}

这时候的代码是不能执行解绑副作用函数的。但是如果我们想每次count发生变化,我们都进行解绑,只需要在第二个参数的数组里加入count变量就可以了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Example() {
const [count, setCount] = useState(0)

useEffect(() => {
console.log(`useEffect=>You clicked ${count} times`)

return () => {
console.log('====================')
}
}, [count])

return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => {
setCount(count + 1)
}}>
click me
</button>

<Router>
<ul>
<li>
{' '}
<Link to="/">首页</Link>{' '}
</li>
<li>
<Link to="/list/">列表</Link>{' '}
</li>
</ul>
<Route path="/" exact component={Index} />
<Route path="/list/" component={List} />
</Router>
</div>
)
}

这时候只要count状态发生变化,都会执行解绑副作用函数,浏览器的控制台也就打印出了一串=================

useContext 让父子组件传值更简单

有了useStateuseEffect已经可以实现大部分的业务逻辑了,但是React Hooks中还是有很多好用的Hooks函数的,比如useContextuseReducer

在用类声明组件时,父子组件的传值是通过组件属性和props进行的,那现在使用方法(Function)来声明组件,已经没有了constructor构造函数也就没有了 props 的接收,那父子组件的传值就成了一个问题。React Hooks 为我们准备了useContext。这节课就学习一下useContext,它可以帮助我们跨越组件层级直接传递变量,实现共享。需要注意的是useContextredux的作用是不同的,一个解决的是组件之间值传递的问题,一个是应用中统一管理状态的问题,但通过和useReducer的配合使用,可以实现类似Redux的作用。

这就好比玩游戏时有很多英雄,英雄的最总目的都是赢得比赛,但是作用不同,有负责输出的,有负责抗伤害的,有负责治疗的。
image.png

Context的作用就是对它所包含的组件树提供全局共享数据的一种技术。

createContext 函数创建 context

直接在src目录下新建一个文件Example4.js,然后拷贝Example.js里的代码,并进行修改,删除路由部分和副作用的代码,只留计数器的核心代码就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useEffect } from 'react'

function Example4() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => {
setCount(count + 1)
}}>
click me
</button>
</div>
)
}
export default Example4

然后修改一下index.js让它渲染这个Example4.js组件,修改的代码如下。

1
2
3
4
import React from 'react'
import ReactDOM from 'react-dom'
import Example from './Example4'
ReactDOM.render(<Example />, document.getElementById('root'))

之后在Example4.js中引入createContext函数,并使用得到一个组件,然后在return方法中进行使用。先看代码,然后我再解释。
创建Context容器对象要在祖组件和后代组件都能调用的地方
const CountContext = createContext()//CountContext首字母大写,因为是组件
在祖组件中使用<CountContext.Provider value={count}> </CountContext.Provider>,将count传递给后代组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, createContext } from 'react'
//===关键代码
const CountContext = createContext() //CountContext首字母大写,因为是组件

function Example4() {
const [count, setCount] = useState(0)

return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => {
setCount(count + 1)
}}>
click me
</button>
{/*======关键代码 */}
<CountContext.Provider value={count}></CountContext.Provider>
</div>
)
}
export default Example4

这段代码就相当于把count变量允许跨层级实现传递和使用了(也就是实现了上下文),当父组件的count变量发生变化时,子组件也会发生变化。接下来我们就看看一个React Hooks的组件如何接收到这个变量。

useContext 接收上下文变量

已经有了上下文变量,剩下的就时如何接收了,接收这个直接使用 useContext 就可以,但是在使用前需要新进行引入useContext(不引入是没办法使用的)。

1
import React, { useState, createContext, useContext } from 'react'

引入后写一个Counter组件,只是显示上下文中的count变量代码如下:

1
2
3
4
function Counter() {
const count = useContext(CountContext) //一句话就可以得到count
return <h2>{count}</h2>
}

得到后就可以显示出来了,但是要记得在<CountContext.Provider>的闭合标签中,代码如下。

1
2
3
<CountContext.Provider value={count}>
<Counter />
</CountContext.Provider>

其实useContext的用法比以前时简单很多,既然简单,就没必要讲解的那么难,希望小伙伴这节课都能 get 到知识点,完善自己的知识体系。

useReducer 介绍和简单使用

上节课学习了useContext函数,那这节课开始学习一下useReducer,因为他们两个很像,并且合作可以完成类似的 Redux 库的操作。在开发中使用useReducer可以让代码具有更好的可读性和可维护性,并且会给测试提供方便。那我们彻底的学习一下useReducer。这节课我们只是简单的学习一下useReducer语法和使用方法,尽量避免Redux的一些操作。这样讲更容易让不了解Redux的小伙伴接受。

reducer 到底是什么?

为了更好的理解useReducer,所以先要了解 JavaScript 里的Redcuer是什么。它的兴起是从Redux广泛使用开始的,但不仅仅存在Redux中,可以使用 JavaScript 来完成Reducer操作。那reducer其实就是一个函数,这个函数接收两个参数,一个是状态,一个用来控制业务逻辑的判断参数。我们举一个最简单的例子。

1
2
3
4
5
6
7
8
9
10
function countReducer(state, action) {
switch (action.type) {
case 'add':
return state + 1
case 'sub':
return state - 1
default:
return state
}
}

上面的代码就是 Reducer,你主要理解的就是这种形式和两个参数的作用,一个参数是状态,一个参数是如何控制状态。

useReducer 的使用

了解 reducer 的含义后,就可以讲 useReducer 了,它也是 React hooks 提供的函数,可以增强我们的Reducer,实现类似 Redux 的功能。我们新建一个Example5.js的文件,然后用 useReducer 实现计数器的加减双向操作。(此部分代码的介绍可以看视频来学习)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useReducer } from 'react'

function ReducerDemo() {
const [count, dispatch] = useReducer((state, action) => {
switch (action) {
case 'add':
return state + 1
case 'sub':
return state - 1
default:
return state
}
}, 0)
return (
<div>
<h2>现在的分数是{count}</h2>
<button onClick={() => dispatch('add')}>Increment</button>
<button onClick={() => dispatch('sub')}>Decrement</button>
</div>
)
}

export default ReducerDemo

这段代码是 useReducer 的最简单实现了,这时候可以在浏览器中实现了计数器的增加减少。

修改index.js文件,让ReducerDemo组件起作用。

1
2
3
4
5
import React from 'react'
import ReactDOM from 'react-dom'
import Example from './Example5'

ReactDOM.render(<Example />, document.getElementById('root'))

这节课就先到这里,小伙伴们一定对useReducer有所了解啦,那下节课我们在来用一个具体的例子,实现类似Redux的用法。

useReducer 代替 Redux 小案例-1

使用useContextuseReducer是可以实现类似Redux的效果,并且一些简单的个人项目,完全可以用下面的方案代替 Redux,这种做法要比 Redux 简单一些。因为useContextuseReducer在前两节课已经学习过了,所以我们这节课把精力就放在如何模拟出Redux的效果。如果你目前还不能掌握基本的语法,可以再复习一下前两节的知识点。

本节课程参考了掘金上缪宇的文章 ,文章地址:https://juejin.im/post/5ceb37c851882520724c7504

理论上的可行性

我们先从理论层面看看替代Redux的可能性,其实如果你对两个函数有所了解,只要我们巧妙的结合,这种替代方案是完全可行的。

useContext:可访问全局状态,避免一层层的传递状态。这符合Redux其中的一项规则,就是状态全局化,并能统一管理。

useReducer:通过 action 的传递,更新复杂逻辑的状态,主要是可以实现类似Redux中的Reducer部分,实现业务逻辑的可行性。

经过我们在理论上的分析是完全可行的,接下来我们就用一个简单实例来看一下具体的实现方法。那这节课先实现useContext部分(也就是状态共享),下节再继续讲解useReducer部分(控制业务逻辑)。

编写基本 UI 组件

既然是一个实例,就需要有些界面的东西,小伙伴们不要觉的烦。在/src目录下新建一个文件夹Example6,有了文件夹后,在文件夹下面建立一个showArea.js文件。代码如下:

1
2
3
4
5
import React from 'react'
function ShowArea() {
return <div style={{ color: 'blue' }}>字体颜色为blue</div>
}
export default ShowArea

显示区域写完后,新建一个Buttons.js文件,用来编写按钮,这个是两个按钮,一个红色一个黄色。先不写其他任何业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react'

function Buttons() {
return (
<div>
<button>红色</button>
<button>黄色</button>
</div>
)
}

export default Buttons

然后再编写一个组合他们的Example6.js组件,引入两个新编写的组件ShowAreaButtons,并用<div>标签给包裹起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useReducer } from 'react'
import ShowArea from './ShowArea'
import Buttons from './Buttons'

function Example6() {
return (
<div>
<ShowArea />
<Buttons />
</div>
)
}

export default Example6

这步做完,需要到/src目录下的index.js中引入一下Example6.js文件,引入后 React 才能正确渲染出刚写的 UI 组件。

1
2
3
4
5
import React from 'react'
import ReactDOM from 'react-dom'
import Example from './Example6/Example6'

ReactDOM.render(<Example />, document.getElementById('root'))

做完这步可以简单的预览一下 UI 效果,虽然很丑,但是只要能满足学习需求就可以了。我们虽然都是前端,但是在学习时没必要追求漂亮的页面,关键时把知识点弄明白。我们写这么多文件,也就是要为接下来的知识点服务,其实这些组件都是陪衬罢了。

编写颜色共享组件color.js

有了 UI 组件后,就可以写一些业务逻辑了,这节课我们先实现状态共享,这个就是利用useContext。建立一个color.js文件,然后写入下面的代码。

1
2
3
4
5
6
7
import React, { createContext } from 'react'

export const ColorContext = createContext({})

export const Color = props => {
return <ColorContext.Provider value={{ color: 'blue' }}>{props.children}</ColorContext.Provider>
}

代码中引入了createContext用来创建共享上下文ColorContext组件,然后我们要用{props.children}来显示对应的子组件。详细解释我在视频中讲解吧。

有了这个组件后,我们就可以把Example6.js进行改写,让她可以共享状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useReducer } from 'react'
import ShowArea from './ShowArea'
import Buttons from './Buttons'
import { Color } from './color' //引入Color组件

function Example6() {
return (
<div>
<Color>
<ShowArea />
<Buttons />
</Color>
</div>
)
}

export default Example6

然后再改写showArea.js文件,我们会引入useContext和在color.js中声明的ColorContext,让组件可以接收全局变量。

1
2
3
4
5
6
7
8
9
import React, { useContext } from 'react'
import { ColorContext } from './color'

function ShowArea() {
const { color } = useContext(ColorContext)
return <div style={{ color: color }}>字体颜色为{color}</div>
}

export default ShowArea

这时候就通过useContext实现了状态的共享,可以到浏览器中看一下效果。然后我们下节课再实现复杂逻辑状态的变化。

useReducer 代替 Redux 小案例-2

在 color.js 中添加 Reducer

颜色(state)管理的代码我们都放在了color.js中,所以在文件里添加一个 reducer,用于处理颜色更新的逻辑。先声明一个 reducer 的函数,它就是 JavaScript 中的普通函数,在讲useReducer的时候已经详细讲过了。有了 reducer 后,在 Color 组件里使用useReducer,这样 Color 组件就有了那个共享状态和处理业务逻辑的能力,跟以前使用的Redux几乎一样了。之后修改一下共享状态。我们来看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { createContext, useReducer } from 'react'

export const ColorContext = createContext({})

export const UPDATE_COLOR = 'UPDATE_COLOR'

const reducer = (state, action) => {
switch (action.type) {
case UPDATE_COLOR:
return action.color
default:
return state
}
}

export const Color = props => {
const [color, dispatch] = useReducer(reducer, 'blue')
return <ColorContext.Provider value={{ color, dispatch }}>{props.children}</ColorContext.Provider>
}

注意,这时候我们共享出去的状态变成了 color 和 dispatch,如果不共享出去 dispatch,你是没办法完成按钮的相应事件的。

通过 dispatch 修改状态

目前程序已经有了处理共享状态的业务逻辑能力,接下来就可以在buttons.js使用dispatch来完成按钮的相应操作了。先引入useContextColorContextUPDATE_COLOR,然后写onClick事件就可以了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { useContext } from 'react'
import { ColorContext, UPDATE_COLOR } from './color'

function Buttons() {
const { dispatch } = useContext(ColorContext)
return (
<div>
<button
onClick={() => {
dispatch({ type: UPDATE_COLOR, color: 'red' })
}}>
红色
</button>
<button
onClick={() => {
dispatch({ type: UPDATE_COLOR, color: 'yellow' })
}}>
黄色
</button>
</div>
)
}

export default Buttons

这样代码就编写完成了,用useContextuseReducer实现了 Redux 的效果,这个代码编写过程比 Redux 要简单,但是也是有一定难度的。希望第一次接触的小伙伴能自己动手写 5 遍以上,把这种模式掌握好。

useMemo 优化 React Hooks 程序性能

useMemo主要用来解决使用 React hooks 产生的无用渲染的性能问题。使用 function 的形式来声明组件,失去了shouldCompnentUpdate(在组件更新之前)这个生命周期,也就是说我们没有办法通过组件更新前条件来决定组件是否更新。而且在函数组件中,也不再区分mountupdate两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗。useMemouseCallback都是解决上述性能问题的,这节课先学习useMemo.

性能问题展示案例

先编写一下刚才所说的性能问题,建立两个组件,一个父组件一个子组件,组件上由两个按钮,一个是小红,一个是志玲,点击哪个,那个就像我们走来了。在/src文件夹下,新建立一个Example7的文件夹,在文件夹下建立一个Example7.js文件.然后先写第一个父组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState, useMemo } from 'react'

function Example7() {
const [xiaohong, setXiaohong] = useState('小红待客状态')
const [zhiling, setZhiling] = useState('志玲待客状态')
return (
<>
<button
onClick={() => {
setXiaohong(new Date().getTime())
}}>
小红
</button>
<button
onClick={() => {
setZhiling(new Date().getTime() + ',志玲向我们走来了')
}}>
志玲
</button>
<ChildComponent name={xiaohong}>{zhiling}</ChildComponent>
</>
)
}

父组件调用了子组件,子组件我们输出两个姑娘的状态,显示在界面上。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ChildComponent({ name, children }) {
function changeXiaohong(name) {
console.log('她来了,她来了。小红向我们走来了')
return name + ',小红向我们走来了'
}

const actionXiaohong = changeXiaohong(name)
return (
<>
<div>{actionXiaohong}</div>
<div>{children}</div>
</>
)
}

然后再导出父组件,让index.js可以渲染。

1
export default Example7

这时候你会发现在浏览器中点击志玲按钮,小红对应的方法都会执行,结果虽然没变,但是每次都执行,这就是性能的损耗。目前只有子组件,业务逻辑也非常简单,如果是一个后台查询,这将产生严重的后果。所以这个问题必须解决。当我们点击志玲按钮时,小红对应的changeXiaohong方法不能执行,只有在点击小红按钮时才能执行。

useMemo 优化性能

其实只要使用useMemo,然后给她传递第二个参数,参数匹配成功,才会执行。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ChildComponent({ name, children }) {
function changeXiaohong(name) {
console.log('她来了,她来了。小红向我们走来了')
return name + ',小红向我们走来了'
}

const actionXiaohong = useMemo(() => changeXiaohong(name), [name])
return (
<>
<div>{actionXiaohong}</div>
<div>{children}</div>
</>
)
}

这时在浏览器中点击一下志玲按钮,changeXiaohong就不再执行了。也节省了性能的消耗。案例只是让你更好理解,你还要从程序本身看到优化的作用。好的程序员对自己写的程序都是会进行不断优化的,这种没必要的性能浪费也是绝对不允许的,所以useMemo的使用在工作中还是比较多的。希望小伙伴们可以掌握。

useRef 获取 DOM 元素和保存变量

image.png

useRef在工作中虽然用的不多,但是也不能缺少。它有两个主要的作用:

  • useRef获取 React JSX 中的 DOM 元素,获取后你就可以控制 DOM 的任何东西了。但是一般不建议这样来作,React 界面的变化可以通过状态来控制。
  • useRef来保存变量,这个在工作中也很少能用到,我们有了useContext这样的保存其实意义不大,但是这是学习,也要把这个特性讲一下。

界面上有一个文本框,在文本框的旁边有一个按钮,当我们点击按钮时,在控制台打印出input的 DOM 元素,并进行复制到 DOM 中的 value 上。这一切都是通过useRef来实现。

/src文件夹下新建一个Example8.js文件,然后先引入 useRef,编写业务逻辑代码如下:

当点击按钮时,你可以看到在浏览器中的控制台完整的打印出了 DOM 的所有东西,并且界面上的<input/>框的 value 值也输出了我们写好的Hello ,JSPang。这一切说明我们可以使用 useRef 获取 DOM 元素,并且可以通过 useRef 控制 DOM 的属性和值。

这个操作在实际开发中用的并不多,但我们还是要讲解一下。就是useRef可以保存 React 中的变量。我们这里就写一个文本框,文本框用来改变text状态。又用useReftext状态进行保存,最后打印在控制台上。写这段代码你会觉的很绕,其实显示开发中没必要这样写,用一个 state 状态就可以搞定,这里只是为了展示知识点。

接着上面的代码来写,就没必要重新写一个文件了。先用useState声明了一个text状态和setText函数。然后编写界面,界面就是一个文本框。然后输入的时候不断变化。

这时想每次text发生状态改变,保存到一个变量中或者说是useRef中,这时候就可以使用useRef了。先声明一个textRef变量,他其实就是useRef函数。然后使用useEffect函数实现每次状态变化都进行变量修改,并打印。最后的全部代码如下。

这时候就可以实现每次状态修改,同时保存到useRef中了。也就是我们说的保存变量的功能。那useRef的主要功能就是获得 DOM 和变量保存,我们都已经讲过了。你的编码能力有增加了一些,让我们一起加油。

useRef 获取 DOM 元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useRef } from 'react'
function Example8() {
const inputEl = useRef(null)
const onButtonClick = () => {
inputEl.current.value = 'Hello ,JSPang'
console.log(inputEl) //输出获取到的DOM节点
}
return (
<>
{/*保存input的ref到inputEl */}
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>在input上展示文字</button>
</>
)
}
export default Example8

useRef 保存普通变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { useRef, useState, useEffect } from 'react'

function Example8() {
const inputEl = useRef(null)
const onButtonClick = () => {
inputEl.current.value = 'Hello ,useRef'
console.log(inputEl)
}
const [text, setText] = useState('jspang')
return (
<>
{/*保存input的ref到inputEl */}
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>在input上展示文字</button>
<br />
<br />
<input
value={text}
onChange={e => {
setText(e.target.value)
}}
/>
</>
)
}

export default Example8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useRef, useState, useEffect } from 'react'
function Example8() {
const inputEl = useRef(null)
const onButtonClick = () => {
inputEl.current.value = 'Hello ,useRef'
console.log(inputEl)
}
//-----------关键代码--------start
const [text, setText] = useState('jspang')
const textRef = useRef()

useEffect(() => {
textRef.current = text
console.log('textRef.current:', textRef.current)
})
//----------关键代码--------------end
return (
<>
{/*保存input的ref到inputEl */}
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>在input上展示文字</button>
<br />
<br />
<input
value={text}
onChange={e => {
setText(e.target.value)
}}
/>
</>
)
}

export default Example8

自定义 Hooks 函数获取窗口大小

其实自定义 Hooks 函数和用 Hooks 创建组件很相似,跟我们平时用 JavaScript 写函数几乎一模一样,可能就是多了些React Hooks的特性,自定义 Hooks 函数偏向于功能,而组件偏向于界面和业务逻辑。由于差别不大,所以使用起来也是很随意的。如果是小型项目是可以的,但是如果项目足够复杂,这会让项目结构不够清晰。所以学习自定义 Hooks 函数还是很有必要的。

编写自定义函数

在实际开发中,为了界面更加美观。获取浏览器窗口的尺寸是一个经常使用的功能,这样经常使用的功能,就可以封装成一个自定义Hooks函数,记住一定要用 use 开头,这样才能区分出什么是组件,什么是自定义函数。

新建一个文件Example9.js,然后编写一个 useWinSize,编写时我们会用到useStateuseEffectuseCallback所以先用import进行引入。

1
import React, { useState, useEffect, useCallback } from 'react'

然后编写函数,函数中先用 useState 设置size状态,然后编写一个每次修改状态的方法onResize,这个方法使用useCallback,目的是为了缓存方法(useMemo 是为了缓存变量)。 然后在第一次进入方法时用useEffect来注册resize监听时间。为了防止一直监听所以在方法移除时,使用 return 的方式移除监听。最后返回 size 变量就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
}

这就是一个自定义函数,其实和我们以前写的 JS 函数没什么区别,所以这里也不做太多的介绍。

编写组件并使用自定义函数

自定义Hooks函数已经写好了,可以直接进行使用,用法和JavaScript的普通函数用起来是一样的。直接在Example9组件使用useWinSize并把结果实时展示在页面上。

1
2
3
4
5
6
7
8
9
10
function Example9() {
const size = useWinSize()
return (
<div>
页面Size:{size.width}x{size.height}
</div>
)
}

export default Example9

之后就可以在浏览器中预览一下结果,可以看到当我们放大缩小浏览器窗口时,页面上的结果都会跟着进行变化。说明自定义的函数起到了作用。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!