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

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

前言

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

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

正文

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

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

# 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

npm install 
npm run dev
复制代码

如下所示:

image.png

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

一.useState

接下来清空 App.jsx,添加如下代码:

import React, { useState } from 'react'

function App() {
const [data, setData] = useState([1, 2, 3, 4, 5])
return (
<div className="App">
    {
        data.map((item, index) => <div key={index}>{item}</div>)
    }
</div>
)
}

export default App
复制代码

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

扫描二维码关注公众号,回复: 13674642 查看本文章

二.useEffect

此时,通过 useEffect 副作用,请求一个接口数据,如下所示:

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 (
<div className="App">
    {
    data.map((item, index) => <span key={index}>{item}</span>)
    }
</div>
)
}

export default App
复制代码

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

image.png

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

所以如果传一个空数组 [],则该副作用只会在组件渲染的时候,执行一次,如下所示:

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

image.png

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

此时需要给请求一个 query 参数,如下所示:

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 (
    <div className="App">
      {
        data.map((item, index) => <span key={index}>{item}</span>)
      }
      <input onChange={(e) => setQuery(e.target.value)} type="text" placeholder='请输入搜索值' />
    </div>
  )
}

export default App
复制代码

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

image.png

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

三.自定义 Hook

可以将上述的请求,抽离成一个自定义 hook,方便在多个地方调用,新建 useApi.js 如下所示:

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 中做如下改动:

import React from 'react'
import useApi from './useApi'

function App() {
  const [{ data }, setQuery] = useApi()
  return (
    <div className="App">
      {
        data.map((item, index) => <span key={index}>{item}</span>)
      }
      <input onChange={(e) => setQuery(e.target.value)} type="text" placeholder='请输入搜索值' />
    </div>
  )
}

export default App
复制代码

查看浏览器展示结果:

image.png

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

四.useMemo

修改 App.jsx,在内部新增一个子组件,子组件接收父组件传进来的一个对象,作为子组件的 useEffect 的第二个依赖参数。

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

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

  return <div>子组件</div>
}


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

  const data = {
    name,
    phone
  }

  return (
    <div className="App">
      <input onChange={(e) => setName(e.target.value)} type="text" placeholder='请输入姓名' />
      <input onChange={(e) => setPhone(e.target.value)} type="text" placeholder='请输入电话' />
      <input onChange={(e) => setKw(e.target.value)} type="text" placeholder='请输入关键词' />
      <Child data={data} />
    </div>
  )
}

export default App
复制代码

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

image.png

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

image.png

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

import React, { useEffect, useState, useMemo } from 'react'

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

  return <div>子组件</div>
}


function App() {

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

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

  return (
    <div className="App">
      <input onChange={(e) => setName(e.target.value)} type="text" placeholder='请输入姓名' />
      <input onChange={(e) => setPhone(e.target.value)} type="text" placeholder='请输入电话' />
      <input onChange={(e) => setKw(e.target.value)} type="text" placeholder='请输入关键词' />
      <Child data={data} />
    </div>
  )
}

export default App
复制代码

效果如下:

image.png

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

五.useCallback

同理,useCallback 也是和 useMemo 有类似的功能,比如传递一个函数给子组件,如下所示:

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

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

  return <div>子组件</div>
}


function App() {

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

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

  return (
    <div className="App">
      <input onChange={(e) => setName(e.target.value)} type="text" placeholder='请输入姓名' />
      <input onChange={(e) => setPhone(e.target.value)} type="text" placeholder='请输入电话' />
      <input onChange={(e) => setKw(e.target.value)} type="text" placeholder='请输入关键词' />
      <Child callback={callback} />
    </div>
  )
}

export default App
复制代码

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

image.png

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

image.png

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

六.重新认识 useEffect

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

将上述 App.jsx 作如下修改:

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

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

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

  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>点击{count}次</button>
      <button onClick={handleClick}>展示点击次数</button>
    </div>
  )
}

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。

用一份伪代码来解释,大致如下:

// 默认初始化
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 值也是当时的快照的一个常量值。

这和之前的类组件是不同的,改成类组件的实现形式如下:

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 <button onClick={() => this.setState({ count: this.state.count + 1 })}>点击{this.state.count}次</button>
  }
}
复制代码

image.png

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

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

猜你喜欢

转载自juejin.im/post/7055972934019448839