Node + React 实战:从0到1 实现记账本(九)


theme: channing-cyan

前端预备:从一个数据请求,入门 React Hooks

前言

React 早期的写法以 Class 类组件为主,附带一些纯用于展示的函数组件,但是函数组件是不能控制自身的状态的。

直到 16.8 版本出来之后,引入了全新的 Hooks 写法,这让之前的类写法就显得 比较累赘,函数组件的写法开始流行起来。函数组件引入了多种钩子函数如 useEffectuseStateuseRefuseCallbackuseMemouseReducer 等等,通过这些钩子函数来管理函数组件的各自状态。

正文

本节通过一个请求,入门整个 React Hook 知识体系。首先需要创建一个空项目,由于本实验采用的是 Vite 2.0 作为脚手架工具,所以 Node 版本必须要在 12.0.0 以上,目前我的版本是 12.9.0

通过指令新建一个联手项目,如下所示: ```cmd

npm 6.x npm init @vitejs/app hooks-demo --template react

npm 7+, 需要额外的双横线:

npm init @vitejs/app hooks-demo -- --template react

yarn

yarn create @vitejs/app hooks-demo --template react ```

根据你的需求,选择上述三个其中一个。新建之后项目目录如下所示:

image.png

cmd npm install npm run dev

如下所示:

image.png

看到如上述所示代表项目已经启动成功了。

一.useState

接下来清空 App.jsx,添加如下代码: ```jsx import React, { useState } from 'react'

function App() { const [data, setData] = useState([1, 2, 3, 4, 5]) return (

{ data.map((item, index) =>
{item}
) }

) }

export default App ```

函数内声明变量,可以通过 useState 方法,它接受一个参数,可以为默认值,也可以为一个函数。上述先分析默认值的情况,默认给一个数组 [1, 2, 3, 4, 5]data 参数便可以直接在 JSX 模板中使用。

二.useEffect

此时,通过 useEffect 副作用,请求一个接口数据,如下所示: ```jsx import React, { useEffect, useState } from 'react' // 模拟数据接口,3 秒钟返回数据。 const getList = () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve([6, 7, 8, 9, 10]) }, 3000) }) }

function App() { const [data, setData] = useState([1, 2, 3, 4, 5])

useEffect(() => { (async () => { const data = await getList() console.log('data', data) setData(data) })() }) return (

{ data.map((item, index) => {item}) }

) }

export default App ```

函数组件默认进来之后,会执行 useEffect 中的回调函数,但是当 setData 执行之后,App 组件再次刷新,刷新之后会再次执行 useEffect 的回调函数,这便会形成一个可怕的死循环,回调函数会一直被这样执行下去。

image.png

所以这里引出 useEffect 的第二个参数。它是一个数组,数组内接收回调函数内使用到的状态参数,一旦在组件内改变了状态参数,则会触发副作用 useEffect 的回调函数执行。

所以如果传一个空数组 [],则该副作用只会在组件渲染的时候,执行一次,如下所示: js useEffect(() => { (async () => { const data = await getList() console.log('data', data) setData(data) })() }, [])

image.png

执行一次之后,副作用不再被触发。

此时需要给请求一个 query 参数,如下所示: ```jsx import React, { useEffect, useState } from 'react'

const getList = (query) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('query', query) resolve([6, 7, 8, 9, 10]) }, 3000) }) }

function App() { const [data, setData] = useState([1, 2, 3, 4, 5]) const [query, setQuery] = useState('')

useEffect(() => { (async () => { const data = await getList(query) console.log('data', data) setData(data) })() }, [query]) return (

{ data.map((item, index) => ) }
) }

export default App ```

此时改变 query 的值,副作用函数便会被执行,如下所示:

image.png

所以,如果你的接口有查询参数,可以将参数设置在 useEffect 的第二个参数的数组值中,这样改变查询变量的时候,副作用便会再次触发执行,相应的函数也会重新带着最新的参数,获取接口数据。

三.自定义 Hook

可以将上述的请求,抽离成一个自定义 hook,方便在多个地方调用,新建 useApi.js 如下所示: ```jsx import React, { useEffect, useState } from 'react' // 模拟请求 const getList = (query) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('query', query) resolve([6, 7, 8, 9, 10]) }, 3000) }) } // 自定义 hook const useApi = () => { const [data, setData] = useState([1, 2, 3, 4, 5]) const [query, setQuery] = useState('')

useEffect(() => { (async () => { const data = await getList() console.log('data', data) setData(data) })() }, [query])

return [{ data }, setQuery]; }

export default useApi ```

如上述所示,最终将 data 数据,和设置请求参数的方法抛出,在 App.jsx 中做如下改动: ```jsx import React from 'react' import useApi from './useApi'

function App() { const [{ data }, setQuery] = useApi() return (

{ data.map((item, index) => ) }
) }

export default App ```

查看浏览器展示结果:

image.png

上述这类自定义 Hook 的使用,在开发中也非常常见,比如有一个请求公共数据的接口,在多个页面中被重复使用,你便可通过自定义 Hook 的形式,将请求逻辑提取出来公用,这也是之前 Class 类组件所不能做到的。

四.useMemo

修改 App.jsx,在内部新增一个子组件,子组件接收父组件传进来的一个对象,作为子组件的 useEffect 的第二个依赖参数。 ```jsx import React, { useEffect, useState } from 'react'

function Child({ data }) { useEffect(() => { console.log('查询条件:', data) }, [data])

return

子组件
}

function App() { const [name, setName] = useState('') const [phone, setPhone] = useState('') const [kw, setKw] = useState('')

const data = { name, phone }

return (

export default App ```

当修改姓名和电话的时候,观察子组件是否监听到依赖的变化,执行 useEffect 内的回调函数。

image.png

此时,上述的结果是预期的,只监听了 name 和 phone 两个参数,但是修改关键词输入框,会得到下面的结果。

image.png

子组件并没有监听 kw 的变化,但是结果却是子组件也被触发渲染了。原因其实是在父组件重新 setKw 之后,data 值和未作修改 kw 前的值已经不一样了。你可能会说,data 的值并没有变化,为什么说它已经不一样了呢?详细的分析放在后续部分,此时可以通过 useMemo 将 data 包装一下,告诉 data 它需要监听的值。 ```jsx import React, { useEffect, useState, useMemo } from 'react'

function Child({ data }) { useEffect(() => { console.log('查询条件:', data) }, [data])

return

子组件
}

function App() {

const [name, setName] = useState('') const [phone, setPhone] = useState('') const [kw, setKw] = useState('')

const data = useMemo(() => ({ name, phone }), [name, phone])

return (

export default App ``` 效果如下:

image.png

这便是 useMemo 的作用,它相当于把父组件需要传递的参数做了一个标记,无论父组件其他状态更新任何值,都不会影响要传递给子组件的对象。

五.useCallback

同理,useCallback 也是和 useMemo 有类似的功能,比如传递一个函数给子组件,如下所示: ```jsx import React, { useEffect, useState, useCallback } from 'react'

function Child({ callback }) { useEffect(() => { callback() }, [callback])

return

子组件
}

function App() {

const [name, setName] = useState('') const [phone, setPhone] = useState('') const [kw, setKw] = useState('')

const callback = () => { console.log('我是callback') }

return (

export default App ```

当修改任何状态值,都会触发子组件的回调函数执行,但是 callback 没有作任何变化。

image.png

此时,给要传递的函数,包裹一层 useCallback,如下所示: \

image.png

useCallback 的第二个参数同 useEffect 和 useMemo 的第二个参数,它是用于监听你需要监听的变量,如在数组内添加 namephonekw 等参数,当改变其中有个,都会触发子组件副作用的执行。

六.重新认识 useEffect

上述很多现象,都是因为没有很好地去理解 React Hooks 函数组件写法的渲染机制。通过一个小例子,来重新认识 useEffect

将上述 App.jsx 作如下修改: ```jsx import React, { useEffect, useState } from 'react'

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

const handleClick = () => { setTimeout(() => { console.log('点击次数: ' + count); }, 3000); }

return (

export default App ```

作下列几个动作:

1、点击增加按钮两次,将 count 增加到 2。

2、点击「展示点击次数」。

3、在 console.log 执行之前,也就是 3 秒内,再次点击新增按钮 2 次,将 count 增加到 4。

按照正常的思路,浏览器应该打印出 点击次数: 4,来查看浏览器的展示效果:

image.png

点击「展示点击次数」按钮,3 秒后,看到的结果是 点击次数: 2,这与我们的预期有出入。

函数组件 App,在每一次渲染都会被调用,而每一次调用都会形成一个独立的上下文,可以理解成一个快照。每一次渲染形成的快照,都是互相独立的。

默认进来的时候,形成一个快照,此时 count 为 0;当点击新增按钮第一次,执行 setCount,函数组件被刷新一次,此时的快照中,count 为 1;再次点击按钮,再次生成快照,此时的 count 为 2,此时点击 「展示点击次数」按钮,在这份快照中,我们的 count 参数就是 2。所以后面无论怎么新增 count,最终输出的结果 count 就是 2。

用一份伪代码来解释,大致如下: ```jsx // 默认初始化 function App() { const count = 0; // useState 返回默认值 // ... function handleClick() { setTimeout(() => { console.log('点击次数: ' + count); }, 3000); } // ... }

// 第一次点击 function App() { const count = 1; // useState 返回值 // ... function handleClick() { setTimeout(() => { console.log('点击次数: ' + count); }, 3000); } // ... }

// 第二次点击 function App() { const count = 2; // useState 返回值 // ... function handleAlertClick() { setTimeout(() => { console.log('点击次数: ' + count); }, 3000); } // ... } ```

image.png

每一次点击,都会重新执行 useEffect 内的回调,并且 count 值也是当时的快照的一个常量值。

这和之前的类组件是不同的,改成类组件的实现形式如下: ```jsx import React from 'react'

export default class App extends React.Component { constructor(props) { super(props) this.state = { count: 0 } } componentDidUpdate() { setTimeout(() => { console.log('点击次数: ' + this.state.count); }, 3000); }

render() { return

image.png

类组件,声明之后,会在内部生成一个实例 instance,所有的数据都会存在类的上下文中,所以 this.state.count 会一直指向最新的 count 值。

说到这里,应该对 React Hooks 的函数组件写法有了新的认识。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值