2020-12-03
React
00

目录

前言
1. React 中如何编写 CSS ?
1.1 内联样式
1.2 普通的 css
1.3 css modules
1.4 styled-components
1.4.1. 安装 styled-components
1.4.2. styled 的基本使用
1.4.3 props、attrs属性
1.4.4 styled高级特性
1.5 添加 class
2. Redux 使用详解
2.1 核心概念
2.2 Redux 的三大原则
2.3 Redux 测试项目
2.4 redux中异步操作
2.5 如何使用 redux-thunk
2.6 Redux Toolkit
3. React-Router 路由详解
3.1 Router 基本使用
4. React Hooks解析
4.1 Class组件存在的问题
4.2 useState
4.3 useEffect
4.4 useContext
4.5 useReducer
4.6 useCallback
4.7 useMemo
4.8 useRef
4.9 useImperativeHandle
4.10 useLayoutEffect
4.11 自定义 hook

前言

本文适合零基础的同学、技术栈主 Vue 的同学快速入门 React,对于有一定的 React 基础同学,相信看完也能有所收获

1. React 中如何编写 CSS ?

事实上,css 一直是 React 的痛点,也是被很多开发者吐槽、诟病的一个点。

在这一点上,Vue 做的要好于 React

  1. Vue 通过在 .vue 文件中编写 <style><style> 标签来编写自己的样式;
  2. 通过是否添加 scoped 属性来决定编写的样式是全局有效还是局部有效;
  3. 通过 lang 属性来设置你喜欢的 less、sass 等预处理器;
  4. 通过内联样式风格的方式来根据最新状态设置和改变 css
  5. 等等...

VueCSS 上虽然不能称之为完美,但是已经足够简洁、自然、方便了,至少统一的样式风格不会出现多个开发人员、多个项目 采用不一样的样式风格。

相比而言,React官方并没有给出在 React 中统一的样式风格:

  • 由此,从普通的 css,到 css modules,再到 css in js,有几十种不同的解决方案,上百个不同的库;
  • 大家一致在寻找最好的或者说最适合自己的 CSS 方案,但是到目前为止也没有统一的方案;

1.1 内联样式

内联样式是官方推荐的一种css样式的写法:

  • style 接受一个采用小驼峰命名属性的 JavaScript 对象,而不是 CSS 字符串;
  • 并且可以引用 state 中的状态来设置相关的样式;

优点:

  1. 内联样式, 样式之间不会有冲突
  2. 可以动态获取当前 state 中的状态

缺点

  1. 写法上都需要使用驼峰标识
  2. 某些样式没有提示
  3. 大量的样式, 代码混乱
  4. 某些样式无法编写(比如伪类/伪元素)

所以官方依然是希望内联合适和普通的 css 来结合编写;

1.2 普通的 css

普通的 css 我们通常会编写到一个单独的文件,之后再进行引入。

这样的编写方式和普通的网页开发中编写方式是一致的:

  • 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
  • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
  • 但是普通的 css 都属于全局的 css ,样式之间会相互影响;

这种编写方式最大的问题是样式之间会相互层叠掉

1.3 css modules

css modules 并不是 React 特有的解决方案,而是所有使用了类似于 webpack 配置的环境下都可以使用的。

如果在其他项目中使用它,那么我们需要自己来进行配置,比如配置 webpack.config.js 中的 modules: true 等。

React 的脚手架已经内置了 css modules 的配置:.css/.less/.scss 等样式文件都需要修改成 .module.css/.module.less/.module.scss 等,之后就可以引用并且进行使用了。

css modules 确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案。

但是这种方案也有自己的缺陷:

  1. 引用的类名,不能使用连接符(.home-title),在 JavaScript 中是不识别的;
  2. 所有的 className 都必须使用 {style.className} 的形式来编写;
  3. 不方便动态来修改某些样式,依然需要使用内联样式的方式;

如果你觉得上面的缺陷还算OK,那么你在开发中完全可以选择使用css modules来编写,并且也是在React中很受欢迎的一种方式。

1.4 styled-components

官方文档也有提到过 CSS in JS 这种方案, CSS-in-JS 是指一种模式,其中 CSSJavaScript 生成而不是在外部文件中定义,注意此功能并不是 React 的一部分,而是由第三方库提供, React 对样式如何定义并没有明确态度。

styled-components 可以说是是社区最流行的一种 CSS-in-JS 库了

1.4.1. 安装 styled-components

sh
npm i styled-components

1.4.2. styled 的基本使用

styled-components 的本质是通过函数的调用,最终创建出一个组件:

  • 这个组件会被自动添加上一个不重复的 class
  • styled-components 会给该 class 添加相关的样式;
js
import React, { PureComponent } from "react"; import styled from "styled-components"; const HomeWrapper = styled.div` color: red; `; class App extends PureComponent { render() { return ( <HomeWrapper> <h1>我是标题</h1> </HomeWrapper> ); } } export default App;

image.png

另外,它支持类似于 CSS 预处理器一样的样式嵌套:

  • 支持直接子代选择器或后代选择器,并且直接编写 样式;
  • 可以通过 & 符号获取当前元素;
  • 直接伪类选择器、伪元素等;

1.4.3 props、attrs属性

props可以传递

html
<CustomWrapper left="20px">

props 可以被传递给 styled 组件

  • 获取 props 需要通过 ${} 传入一个插值函数,props 会作为该函数的参数;
  • 这种方式可以有效的解决动态样式的问题;
js
const CustomWrapper = styled.div.attrs({ paddingLeft: props => props.left || '5px' })` padding-left: ${props => props.paddingLeft}; `

1.4.4 styled高级特性

支持样式的继承

js
const Parent = styled.button` padding: 8px 30px; border-radius: 5px; ` const Child = styled(Parent)` background-color: red; color: #fff; `

styled 设置主题

js
import { ThemeProvider } from 'styled-components' <ThemeProvider theme={{color: "red", fontSize: "30px"}}> <Home /> <Profile /> </ThemeProvider> const ProfileWrapper = styled.div` color: ${props => props.theme.color}; font-size: ${props => props.theme.fontSize} `

1.5 添加 class

vue中添加class是一件非常简单的事情:

你可以通过传入一个对象:

html
<div :class="{active: isAcitive}"></div>

你也可以传入一个数组:

html
<div :class="[activeClass, errClass]"></div>

甚至是对象和数组混合使用:

html
<div :class="[{active: isActive}, errorClass]"></div>

React在JSX给了我们开发者足够多的灵活性,你可以像编写JavaScript代码一样,通过一些逻辑来决定是否添加某些class:

js
<div className={"title " +(isActive ? "active" : "")}> </div>

这个时候我们可以借助于一个第三方的库:classnames

image.png

很明显,这是一个用于动态添加classnames的一个库。

2. Redux 使用详解

JavaScript 开发的应用程序,已经变得越来越复杂了:

  • JavaScript 需要管理的状态越来越多,越来越复杂
  • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页

管理不断变化的 state 是非常困难的:

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View 页面也有可能会引起状态的变化;
  • 当应用程序复杂时,state 在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;

React 是在视图层帮助我们解决了 DOM 的渲染过程,但是 State 依然是留给我们自己来管理:

  • 无论是组件定义自己的 state,还是组件之间的通信通过 props 进行传递;也包括通过Context 进行数据之间的共享
  • React 主要负责帮助我们管理视图state 如何维护最终还是我们自己来决定
UI = rendeer(state)

Redux 就是一个帮助我们管理 State 的容器:ReduxJavaScript 的状态容器,提供了可预测的状态管理

Redux 除了和 React 一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有 2kb

2.1 核心概念

Redux 的核心理念非常简单。

1. store

比如我们需要一个朋友列表需要管理

  1. 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的
  2. 比如页面的某处通过 products.push的方式增加了一条数据;
  3. 比如另一个页面通过 products[0].age = 25 修改了一条数据;

整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化;

js
const initalState = [ friends: [ { name: '张三', age: 25 }, { name: '李四', age: 24 }, ] ]

**2. action **

Redux 要求我们通过 action 来更新数据:

  1. 所有数据的变化,必须通过派发(dispatch)action来更新
  2. action 是一个普通的 JavaScript 对象,用来描述这次更新的 typecontent

比如下面就是几个更新friends的action:

  • 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的
  • 当然,目前我们的action是固定的对象
  • 真实应用中,我们会通过函数来定义,返回一个action
js
const action1 = { type: "ADD_FRIEND", info: {name: 'lucy', age: 20 }} const action2 = { type: "INC_AGE", index: 0} const action2 = { type: "CHANGE_NAME", payload: {index:0, newName: 'coder'}}

3. reducer

redux是如何将stateaction联系在一起呢?答案就是reducer

  1. reducer 是一个纯函数;
  2. reducer 做的事情就是将传入的 stateaction 结合起来生成一个新的 state
js
function reducer(state = initialState, action) { switch (action.type) { case "ADD_FFRIEND": return { ...state, friends: [...state.friends, action.info] }; case "INC_AGE": return { ...state, friend: state.friends.map((item, index) => { if (index === action.index) { return { ...item, age: item.age + 1 }; } }), }; case "CHANGE_NAME": return { ...state, friends: state.friends.map((item, index) => { if (index === action.index) { return { ...item, name: action.newName }; } return item; }), }; default: return state; } }

2.2 Redux 的三大原则

  1. 单一数据源
  • 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:
  • Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;
  • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;
  1. State是只读的
  • 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:
  • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;
  1. 使用纯函数来执行修改
  • 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State:
  • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;
  • 但是所有的reducer都应该是纯函数,不能产生任何的副作用;

2.3 Redux 测试项目

  1. 创建一个新的项目文件夹 redux-demo
# 执行初始化操作 npm init # 安装 redux npm i redux
  1. 创建 src 目录,并创建 index.js 文件
  2. 修改 package.json 可以执行 index.js
js
"scripts": { "start": "node src/index.js" }
  1. 结构划分

如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护。

  • 接下来,我会对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。
  • 创建store/index.js文件:
  • 创建store/reducer.js文件:
  • 创建store/actionCreators.js文件:
  • 创建store/constants.js文件:
  1. Redux 使用流程

image.png

image.png

  1. redux 融入 react代码

这里我创建了两个组件:

  • Home组件:其中会展示当前的counter值,并且有一个+1和+5的按钮;
  • Profile组件:其中会展示当前的counter值,并且有一个-1和-5的按钮;

image.png

核心代码主要是两个:

  • componentDidMount 中定义数据的变化,当数据发生变化时重新设置 counter;
  • 在发生点击事件时,调用 storedispatch 来派发对应的 action
  1. react-redux使用

redux 官方提供了 react-redux 的库来帮助我们完成 reduxreact 的连接,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。

  • 安装react-redux:
js
npm i react-redux
js
// index.js import { Provider } from 'react-redux' root.render( <React.StrictNode> <Provider store={store}> <APP /> </Provider> </React.StrictNode> )
// Home.js const mapStateToProps = state => ({ counter: state.counter }) const mapDispatchToProps = dispatch => ({ addNumber: function(num) { dispatch(addNumberAction(num)) }, subNumber: function(num) { dispatch(subNumberAction(num)) } }) export default connect(mapStateToProps, mapDispatchToProps)(Home)

2.4 redux中异步操作

事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给 redux 来管理

但是在 redux 中如何可以进行异步的操作呢?

  • 答案就是使用中间件(Middleware);
  • 学习过Express或Koa框架的童鞋对中间件的概念一定不陌生;
  • 在这类框架中,Middleware可以帮助我们在请求和响应之间嵌入一些操作的代码,比如cookie解析、日志记录、文件压缩等操作;

这里官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk;

redux-thunk是如何做到让我们可以发送异步的请求呢?

  • 我们知道,默认情况下的dispatch(action),action需要是一个JavaScript的对象;
  • redux-thunk可以让dispatch(action函数),action可以是一个函数;
  • 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
    • dispatch函数用于我们之后再次派发action;
    • getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;

2.5 如何使用 redux-thunk

  1. 安装redux-thunk
js
npm i redux-thunk
  1. 在创建 store 时传入应用了 middlewareenhance 函数
  • 通过applyMiddleware来结合多个Middleware, 返回一个enhancer;
  • 将enhancer作为第二个参数传入到createStore中;
js
const enhancer = applyMiddleware(thunkMiddleware) const store = createStore(reducer,enhancer)
  1. 定义返回一个函数的 action
  • 注意:这里不是返回一个对象,而是一个函数
  • 该函数在 dispatch 之后会被执行;
js
const getHomeMultidataAction = () => { return dispatch => { axios.get('xxxxx').then(res => { const data = res.data.data dispatch(changeBannersAction(data.banner.list)) }) } }

2.6 Redux Toolkit

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。

  • 在前面我们学习Redux的时候应该已经发现,redux的编写逻辑过于的繁琐和麻烦。
  • 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);
  • Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题;
  • 在很多地方为了称呼方便,也将之称为“RTK”;

安装Redux Toolkit:

npm install @reduxjs/toolkit react-redux

Redux Toolkit的核心API主要是如下几个:

  1. configureStore:包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。
  2. createSlice:接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions。
  3. createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分派动作类型的 thunk

3. React-Router 路由详解

目前前端流行的三大框架, 都有自己的路由实现:

  1. Angular的ngRouter
  2. React的ReactRouter
  3. Vue的vue-router

React Router在最近两年版本更新的较快,并且在最新的React Router6.x版本中发生了较大的变化。

  • 目前React Router6.x已经非常稳定,我们可以放心的使用;

安装React Router:

  • 安装时,我们选择react-router-dom;
  • react-router会包含一些react-native的内容,web开发并不需要;
npm install react-router-dom

3.1 Router 基本使用

react-router最主要的API是给我们提供的一些组件

BrowserRouter或HashRouter

  • Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;
  • BrowserRouter使用history模式;
  • HashRouter使用hash模式;
js
<React.StrictMode> <HashRouter> <App /> </HashRouter> </React.StrictMode>
  1. 路由映射配置

Routes:包裹所有的Route,在其中匹配一个路由。(Router5.x使用的是Switch组件)

Route:Route用于路径的匹配;

  • path属性:用于设置匹配到的路径;
  • element属性:设置匹配到路径后,渲染的组件;
    • Router5.x使用的是component属性
  • exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
    • Router6.x不再支持该属性
js
<Routes> <Route path="/" element={<Home/>}> <Route path="/about" element={<About/>}> <Route path="/profile" element={<Profile/>}> </Routes>
  1. 路由配置和跳转

Link和NavLink:

  • 通常路径的跳转是使用Link组件,最终会被渲染成a元素;
  • NavLink是在Link基础之上增加了一些样式属性(后续学习);
  • to属性:Link中最重要的属性,用于设置跳转到的路径;
js
<div className="header"> <Link to="/">首页</Link> <Link to="/about">关于</Link> <Link to="/profile">我的</Link> </div>

image.png

  1. NavLink的使用

需求:路径选中时,对应的a元素变为红色

这个时候,我们要使用NavLink组件来替代Link组件:

  • style:传入函数,函数接受一个对象,包含isActive属性
  • className:传入函数,函数接受一个对象,包含isActive属性
JS
<NavLink to="" className={this.getActiveClass}>首页</NavLink> <NavLink to="about" className={this.getActiveClass}>关于</NavLink> <NavLink to="profile" className={this.getActiveClass}>我的</NavLink>
js
getActiveClass({isActive}){ return classNames({ 'link-active': isActive }) }

默认的activeClassName:

  • 事实上在默认匹配成功时,NavLink就会添加上一个动态的active class;
  • 所以我们也可以直接编写样式 当然,如果你担心这个class在其他地方被使用了,出现样式的层叠,也可以自定义class
  1. Navigate导航

Navigate用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:

我们也可以在匹配到/的时候,直接跳转到/home页面

<Route path="/" element={<Navigate to="/home" />} />
  1. Not Found页面配置

如果用户随意输入一个地址,该地址无法匹配,那么在路由匹配的位置将什么内容都不显示。

很多时候,我们希望在这种情况下,让用户看到一个Not Found的页面。

这个过程非常简单:

  • 开发一个Not Found页面;
  • 配置对应的Route,并且设置path为*即可;
<Route path="*" element={<NotFound />} />
  1. 路由的嵌套 在开发中,路由之间是存在嵌套关系的。 这里我们假设Home页面中有两个页面内容:
  • 推荐列表和排行榜列表;
  • 点击不同的链接可以跳转到不同的地方,显示不同的内容;

<Outlet> 组件用于在父路由元素中作为子路由的占位元素。

image.png

  1. 手动路由的跳转

在Router6.x版本之后,代码类的API都迁移到了hooks的写法:

如果我们希望进行代码跳转,需要通过useNavigate的Hook获取到navigate对象进行操作;

js
import { useNavigate } from "react-router-dom"; function MyComponent() { const navigate = useNavigate(); function handleClick() { navigate("/path/to/some/page"); } return ( <button onClick={handleClick}> Go to page </button> ); }

对于类组件,可以使用 withNavigate 高阶组件来包装组件,以便获取 navigate 函数。具体的用法如下:

js
import { withNavigate } from "react-router-dom"; class MyComponent extends React.Component { handleClick = () => { this.props.navigate("/path/to/some/page"); } render() { return ( <button onClick={this.handleClick}> Go to page </button> ); } } export default withNavigate(MyComponent);
  1. 路由参数传递

传递参数有二种方式:

  • 动态路由的方式;
  • search传递参数;

动态路由的概念指的是路由中的路径并不会固定:

  • 比如/detail的path对应一个组件Detail;
  • 如果我们将path在Route匹配时写成/detail/
    ,那么 /detail/abc、/detail/123都可以匹配到该Route,并且进行显示;
  • 这个匹配规则,我们就称之为动态路由;
  • 通常情况下,使用动态路由可以为路由传递参数。
js
<Link to="detail/123">详情:123</Link> <Link to="user?name=coder">用户信息</Link>
js
const [searchParams] = useSearchParams() const query = Object.fromEntries(searchParams)

4. React Hooks解析

Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。

我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:

  • class组件可以定义自己的state,用来保存组件自己内部的状态;
    • 函数式组件不可以,因为函数每次调用都会产生新的临时变量;
  • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
    • 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;
    • 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
  • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;
    • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;

所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。

4.1 Class组件存在的问题

复杂组件变得难以理解:

  • 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
  • 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除);
  • 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;

难以理解的class:

  • 很多人发现学习ES6的class是学习React的一个障碍。
  • 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
  • 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;

组件复用状态很难:

  • 在前面为了一些状态的复用我们需要通过高阶组件;
  • 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
  • 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
  • 这些代码让我们不管是编写和设计上来说,都变得非常困难;

Hook的出现,可以解决上面提到的这些问题;

简单总结一下hooks:

  • 它可以让我们在不编写class的情况下使用state以及其他的React特性;
  • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;

Hook的使用场景:

  • Hook的出现基本可以代替我们之前所有使用class组件的地方;
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
  • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;

在我们继续之前,请记住 Hook 是:

  • 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
  • 100% 向后兼容的:Hook 不包含任何破坏性改动。
  • 现在可用:Hook 已发布于 v16.8.0

4.2 useState

State HookAPI 就是 useState

  • useState 会帮助我们定义一个 state 变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。
  • 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
  • useState 接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为undefined)。
  • useState 的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。

我们通过一个计数器案例,来对比一下 class 组件和函数式组件结合 hooks 的对比:

  1. 类组件
js
import React, { PureComponent } from "react"; export default class Counter01 extends PureComponent { constructor(props) { super(porps); this.state = { counter: 0, }; } render() { return ( <div> <h2>当前计数: {this.state.counter}</h2> <button onClick={(e) => this.increment()}>+1</button> <button onClick={(e) => this.decrement()}>-1</button> </div> ); } increment() { this.setState({ counter: this.state.counter + 1 }); } decrement() { this.setState({ counter: this.state.counter - 1 }); } }
  1. 函数组件
js
import React, { useState } from "react"; export default function Counter02() { const [count, setCount] = useState(0); return ( <div> <h2>当前计数: {count}</h2> <button onClick={(e) => setCount(count + 1)}>+1</button> <button onClick={(e) => setCount(count - 1)}>-1</button> </div> ); }

你会发现上面的代码差异非常大

  • 函数式组件结合 hooks 让整个代码变得非常简洁
  • 并且再也不用考虑 this 相关的问题

4.3 useEffect

目前我们已经通过hook在函数式组件中定义state,那么类似于生命周期这些呢?

  • Effect Hook 可以让你来完成一些类似于class中生命周期的功能;
  • 事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);
  • 所以对于完成这些功能的Hook被称之为 Effect Hook;

假如我们现在有一个需求:页面的title总是显示counter的数字,分别使用class组件和Hook实现:

js
// class 组件 import React, { Component } from 'react'; class Counter extends Component { constructor(props) { super(props); this.state = { count: 0 }; } incrementCount = () => { this.setState(prevState => { return { count: prevState.count + 1 }; }); } componentDidMount() { document.title = `Counter: ${this.state.count}`; } componentDidUpdate() { document.title = `Counter: ${this.state.count}`; } render() { return ( <div> <h1>Counter: {this.state.count}</h1> <button onClick={this.incrementCount}>Increment</button> </div> ); } } export default Counter;
js
// hook 实现 import React, { useState, useEffect } from 'react'; function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `Counter: ${count}`; }); const incrementCount = () => { setCount(prevCount => prevCount + 1); } return ( <div> <h1>Counter: {count}</h1> <button onClick={incrementCount}>Increment</button> </div> ); } export default Counter;

useEffect的解析:

  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
  • useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;

在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除:

  • 比如我们之前的事件总线或Redux中手动调用subscribe;
  • 都需要在componentWillUnmount有对应的取消订阅;
  • Effect Hook通过什么方式来模拟componentWillUnmount呢?

useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B:

js
type EffectCallback = () => (void | (() => void | undefined));

为什么要在 effect 中返回一个函数?

  • 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
  • 如此可以将添加和移除订阅的逻辑放在一起;
  • 它们都属于 effect 的一部分;

React 何时清除 effect?

  • React 会在组件更新和卸载的时候执行清除操作;
  • 正如之前学到的,effect 在每次渲染的时候都会执行;

使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:

  • 比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中; 使用Effect Hook,我们可以将它们分离到不同的useEffect中:

Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:

  • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect;

默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

  • 某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅);
  • 另外,多次执行也会导致一定的性能问题; 我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?
  • useEffect实际上有两个参数:
  • 参数一:执行的回调函数;
  • 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)

但是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:

  • 那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了;

4.4 useContext

在之前的开发中,我们要在组件中使用共享的Context有两种方式:

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context;
  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;

但是多个Context共享时的方式会存在大量的嵌套:

  • Context Hook允许我们通过Hook来直接获取某个Context的值;
js
import React, { useContext } from 'react'; // 创建一个 Context 对象 const ThemeContext = React.createContext('light'); // 在 App 组件中提供一个值为 "dark" 的 Context function App() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } // 在 Toolbar 组件中使用 Context function Toolbar() { const theme = useContext(ThemeContext); return ( <div> <button style={{ background: theme }}>按钮</button> </div> ); }

注意事项:

  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的context value 值

4.5 useReducer

很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是。

useReducer仅仅是useState的一种替代方案:

  • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
  • 或者这次修改的state需要依赖之前的state时,也可以使用;
import React, { useReducer } from 'react'; // 定义一个 reducer 函数来管理 state function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } // 定义一个组件,使用 useReducer 管理 state function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <div> <h1>计数器:{state.count}</h1> <button onClick={() => dispatch({ type: 'increment' })}>+1</button> <button onClick={() => dispatch({ type: 'decrement' })}>-1</button> </div> ); } export default Counter;

数据是不会共享的,它们只是使用了相同的 reducer 的函数而已。

所以,useReducer只是useState的一种替代品,并不能替代Redux。

4.6 useCallback

useCallback 是一个用于缓存函数的 React 钩子函数。它可以优化函数组件的性能,避免因为函数组件的重新渲染导致函数重新定义而产生不必要的计算开销。下面是一个使用 useCallback 的简单示例:

js
import React, { useState, useCallback } from 'react'; function Counter() { const [count, setCount] = useState(0); // 定义一个回调函数,使用 useCallback 缓存 const handleIncrement = useCallback(() => { setCount(count + 1); }, [count]); return ( <div> <h1>计数器:{count}</h1> <button onClick={handleIncrement}>+1</button> </div> ); } export default Counter;

在上面的例子中,我们定义了一个 handleIncrement 回调函数,并使用 useCallback 钩子函数来缓存它。useCallback 接受两个参数,第一个参数是要缓存的函数,第二个参数是一个数组,用来指定缓存函数所依赖的状态变量。

在这个例子中,我们指定了 count 作为依赖项。当 count 发生变化时,useCallback 会返回一个新的缓存函数;当 count 没有变化时,useCallback 返回缓存的旧函数。这样,我们就避免了因为组件的重新渲染导致函数重新定义而产生不必要的计算开销。

最后,在组件中,我们使用缓存的 handleIncrement 函数来处理按钮的点击事件,从而实现了计数器的功能。

4.7 useMemo

useMemo 是一个用于缓存值的 React 钩子函数。它可以优化函数组件的性能,避免因为函数组件的重新渲染导致重复计算的开销。下面是一个使用 useMemo 的简单示例:

js
import React, { useState, useMemo } from 'react'; function Counter() { const [count, setCount] = useState(0); // 定义一个缓存值,使用 useMemo 缓存 const result = useMemo(() => { let sum = 0; for (let i = 1; i <= count; i++) { sum += i; } return sum; }, [count]); return ( <div> <h1>计数器:{count}</h1> <h2>1 到 {count} 的和为:{result}</h2> <button onClick={() => setCount(count + 1)}>+1</button> </div> ); } export default Counter;

在上面的例子中,我们定义了一个 result 变量,并使用 useMemo 钩子函数来缓存它。useMemo 接受两个参数,第一个参数是一个函数,用来计算缓存值;第二个参数是一个数组,用来指定缓存值所依赖的状态变量。

在这个例子中,我们指定了 count 作为依赖项。当 count 发生变化时,useMemo 会重新计算缓存值;当 count 没有变化时,useMemo 返回缓存的旧值。这样,我们就避免了因为组件的重新渲染导致重复计算的开销。

最后,在组件中,我们使用缓存的 result 变量来显示计算结果,并通过按钮的点击事件来更新计数器的值,从而实现了计算和的功能。

4.8 useRef

useRef 是一个用于创建可变引用的 React 钩子函数。它返回一个包含 current 属性的对象,可以用来存储任意可变值,并且不会触发组件的重新渲染。 useRef 可以用来保存 DOM 元素的引用、定时器、以及其他一些需要在组件渲染期间保持稳定的值。下面是一个使用 useRef 的简单示例:

js
import React, { useState, useRef } from 'react'; function TextInput() { const [text, setText] = useState(''); const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <div> <input ref={inputRef} value={text} onChange={(e) => setText(e.target.value)} /> <button onClick={handleClick}>聚焦</button> </div> ); } export default TextInput;

在上面的例子中,我们定义了一个 inputRef 引用,并使用 useRef 钩子函数来创建它。在组件中,我们将 input 元素的引用传递给 inputRef,这样就可以在组件中引用 input 元素了。

在 handleClick 函数中,我们使用 inputRef.current.focus() 来聚焦 input 元素。这里,inputRef.current 会返回 input 元素的引用,我们可以通过它来访问 input 元素的属性和方法。

最后,在组件中,我们使用 inputRef 来引用 input 元素,并通过按钮的点击事件来聚焦它。这样,我们就实现了一个可以聚焦输入框的组件。

4.9 useImperativeHandle

useImperativeHandle 是一个用于暴露自定义方法到父组件的 React 钩子函数。它可以让子组件向父组件暴露一些 API 方法,以便父组件可以通过子组件的引用来直接调用这些方法。 useImperativeHandle 的第一个参数是一个引用,它可以是任意可变的值,第二个参数是一个返回对象,这个对象包含一些自定义方法。这些自定义方法会被暴露到父组件中,以便父组件可以调用它们。下面是一个使用 useImperativeHandle 的简单示例:

js
import React, { useRef, useImperativeHandle } from 'react'; function FancyInput(props, ref) { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return ( <div> <input ref={inputRef} /> </div> ); } export default React.forwardRef(FancyInput);

在上面的例子中,我们定义了一个 FancyInput 组件,并使用 useRef 钩子函数来创建 inputRef 引用。在 useImperativeHandle 钩子函数中,我们将 ref 引用传递给它,然后定义了一个 focus 方法,它可以聚焦 input 元素。这样,父组件就可以通过子组件的 ref 引用来调用 focus 方法,从而实现聚焦输入框的功能。

最后,在组件中,我们使用 inputRef 引用来引用 input 元素,并使用 useImperativeHandle 钩子函数来暴露 focus 方法,以便父组件可以通过子组件的 ref 引用来调用它。注意,我们需要使用 React.forwardRef 函数来转发 ref 引用,以便父组件可以访问子组件的 ref 引用。

4.10 useLayoutEffect

useLayoutEffect 是 React 中的一个钩子函数,它非常类似于 useEffect,不同的是它会在所有的 DOM 变更之后同步执行,但在浏览器绘制 UI 之前同步执行,这样可以让我们在更新 DOM 之前同步获取 DOM 的尺寸和位置信息,并立即执行其他必要的操作。

和 useEffect 一样,useLayoutEffect 接收两个参数:一个函数和一个依赖项数组,其中第一个参数是需要执行的回调函数,而第二个参数则是一个可选的依赖项数组。当依赖项发生变化时,useLayoutEffect 钩子函数会再次触发执行。下面是一个示例:

js
import React, { useState, useLayoutEffect, useRef } from 'react'; function App() { const [width, setWidth] = useState(0); const boxRef = useRef(null); useLayoutEffect(() => { const { current } = boxRef; if (current) { setWidth(current.getBoundingClientRect().width); } }, []); return ( <div> <div ref={boxRef} style={{ width: '100px', height: '100px', background: 'red' }} /> <p>The box width is {width}px.</p> </div> ); } export default App;

在上面的示例中,我们使用了 useLayoutEffect 钩子函数来同步获取一个 div 元素的宽度,并将它存储在 width 状态中。这个 div 元素的引用是通过 useRef 钩子函数创建的,并作为 ref 属性传递给 div 元素。在 useLayoutEffect 回调函数中,我们通过 getBoundingClientRect() 方法获取 div 元素的尺寸信息,并将它的宽度存储在 width 状态中。最后,我们将这个宽度展示在一个 p 元素中。

需要注意的是,由于 useLayoutEffect 会在所有的 DOM 变更之后同步执行,所以在使用它时要非常小心,以免影响应用程序的性能和用户体验。一般来说,应该尽可能地使用 useEffect 钩子函数,只有在确实需要在 DOM 变更之后同步执行操作时,才使用 useLayoutEffect 钩子函数。

4.11 自定义 hook

自定义 Hook 是 React 中一种重要的代码复用机制。自定义 Hook 允许你在函数组件中复用状态逻辑、副作用逻辑或其他任何组件之间可以共享的逻辑。

一个自定义 Hook 就是一个普通的 JavaScript 函数,但它可以使用其他的 React 钩子函数,如 useState、useEffect、useContext 等。自定义 Hook 的名称以 use 开头,这是 React 的一个约定,它使得我们可以很容易地看出一个 Hook 是不是官方提供的。

下面是一个使用自定义 Hook 的示例:

js
import React, { useState, useEffect } from 'react'; function useCountdown(initialCount) { const [count, setCount] = useState(initialCount); useEffect(() => { const interval = setInterval(() => { setCount((prevCount) => prevCount - 1); }, 1000); return () => clearInterval(interval); }, []); return count; } function App() { const count = useCountdown(10); return ( <div> <h1>{count}</h1> </div> ); } export default App;

在上面的示例中,我们定义了一个名为 useCountdown 的自定义 Hook,它使用了 useState 和 useEffect 钩子函数来实现一个倒计时计数器。这个自定义 Hook 接收一个初始计数值,并返回一个当前计数值。然后,我们在组件中调用这个自定义 Hook,得到当前计数值,并将它渲染到页面上。

需要注意的是,自定义 Hook 本身不能包含 JSX 代码,因为它只是一个普通的 JavaScript 函数。但是,我们可以在自定义 Hook 中使用其他的 React 钩子函数来实现组件之间共享的逻辑。

使用自定义 Hook 的好处在于,它可以让我们更好地组织和复用代码,避免了组件之间的代码冗余,提高了代码的可读性和可维护性。

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:叶继伟

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!