React学习
React学习
月晕React 脚手架搭建
Create-react-app 基础操作
当下前端开发的主流是组件化和模块化
- 有助于团队协作开发
- 便于组件的复用:提高开发效率、方便后期维护、减少冗余代码
划分组件:
- 业务组件:针对项目需求封装
- 普通业务组件:复用性低,只是单独拆选出来的一个模块
- 通用业务组件:具备复用性
- 功能组件:适用于多个项目「例如:UI 组件库中的组件」
- 通用功能组件
组件化开发必然会带来工程化,即基于 Webpack / Vite / Rollup / Turbopack 等工具实现组件的合并、压缩、打包等。
安装 create-react-app
我们可以基于 webpack 自己去搭建一套工程化打包的脚手架,但是过程会非常麻烦和繁琐,因此可以利用官方提供的脚手架create-react-app
创建 React 项目,基于该脚手架创建项目,默认就把 Webpack 的打包规则已经处理好了,把一些项目需要的基本文件也都创建好了。我们可以在其上面做修改
全局安装 create-react-app 脚手架:
1 | npm i create-react-app -g |
检查 create-react-app 的版本(是否安装完成·):
1 | create-react-app --version |
创建工程化项目
创建的命令为:
1 | create-react-app [项目名称] |
项目名称应该仅使用数字、小写字母和下划线
_
的组合。
创建的项目中会默认安装:
- react : React 框架的核心
- react-dom : React 视图渲染的核心 [基于 React 构建 WebApp(HTML 页面)]
- react-script: 脚手架为了让项目目录看起来干净一点,把 Webpack 打包的规则及相关的插件、预处理器等都隐藏到了 node_modules 目录下,react-scripts 就是脚手架中自己对打包命令的一种封装,基于它打包,会调用 node_modules 中的 Webpack 等进行处理
初始化项目的package.json
1 | { |
值得一提的是,JSON 文件对格式的要求十分严格,是不允许注释的,上面的注释仅帮助理解,在文件中不可使用。
根目录之下,除了 node_modules 子目录,还有两个非常重要的子目录分别为:
- src: 所有后续编写的代码,几乎都放在该目录下「打包的时候,一般只对这个目录下的代码进行处理」
- public:存放页面模版
将 src 目录下的大部分文件删除,仅留下 index.jsx(如果后缀是 .js,改为 .jsx)文件,其内容改为:
1 | import React from 'react' |
将 public 目录下的大部分文件删除,仅留下 favicon.ico(项目网站的 logo 图标)和 index.html 文件,并将 index.html 的内容改为:
1 |
|
脚手架的进阶应用
暴露 webpack 配置
前面说到,react-scripts 把 Webpack 打包的规则及相关的插件、预处理器等都隐藏到了 node_modules 目录下了。那么,如果我们想要修改 Webpack 的一些默认配置时,该怎么办呢?
这时就需要使用 eject
命令了,即:
1 | npm run eject # 或者 yarn eject |
注意:一旦暴露 Webpack 配置,该操作是永久的,就不能还原了。
这时,会发现根目录下会多了 config 和 scripts 两个文件夹,并且 package.json 中内容会变得非常多(把 Webpack 打包需要的所有模块都放在了依赖项中)。
其中,/config/webpack 下有几个文件值得注意:
- webpack.config.js:脚手架中默认 Webpack 打包的配置
- webpackDevServer.config.js:默认 web pack-dev-server 的配置
- paths.js:打包中用到的路径
scripts 目录中的 build.js 是后期执行相关打包命令的入口文件。
在 package.json 增加的依赖中,有几个模块值得注意:
babel-preset-react-app:它是对 @babel/preset-env 语法包的重写,目的是让语法包也可以识别 React 的 jsx 语法,实现代码转换
create-react- app 脚手架默认配置是使用的 sass 预编译语言,如果项目中使用的就是 sass,则无需处理;如果使用的是 less 或 stylus,则需要自己处理。
package.json 中的 scripts 也发生了变化,为:
1
2
3
4
5"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js"
},package.json 中还增加了 babel 配置项:
1
2
3
4
5
6// 对 babel-loader 进行额外配置,等价于 babel.config.js
"babel": {
"presets": [
"react-app"
]
}
常见配置修改
使用 less
前面提到,脚手架默认配置是使用的 sass 预编译语言,如果要使用 less,需要自己进行配置:
1 | npm install less less-loader@8 # 新版本的 less-loader 兼容性不好 |
然后修改暴露出来的 webpack.config.js 中的配置:
1 | // 修改前 |
增加 alias @
1 | // path.js中有 |
修改域名或端口号
默认情况下,启动项目使用的是 localhost:3000
,可以在 scripts/start.js 文件中修改:
1 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000 // 可修改端口号 |
如果想基于修改环境变量的方式来改,需要先安装 cross-env,如下:
1 | // npm i cross-env # 或 yarn add cross-env |
修改浏览器兼容
如果需要修改浏览器的兼容性,则需要修改 package.json 中 "browserslist"
项的内容。
但是,修改兼容列表实现浏览器兼容时,只能解决两个问题,即:
- 对 postcss-loader 生效,控制 CSS3 的前缀
- 对 babel-loader 生效,控制 ES6 的转换
但还存在一个问题,就是无法处理 ES6 内置 API 的兼容。
为了解决这个问题,你可以安装 @babel/polyfill(其作用是对常见的内置 API 进行重写),然后在入口文件(index.jsx)中引入 import '@babel/polyfill'
。
但是,在脚手架中,通常不需要安装它,因为脚手架默认已经安装了 react-app-polyfill,它是对 @babel/polyfill 的重写,仅需要在入口文件(index.jsx)中引入:
1 | // 对 ES6 内置 API 的兼容性处理 |
MVC 模式和 MVVM 模式
主流 web 前端框架
- React
- Vue
- Angular
主流设计思想:不直接操作 DOM,而是改成数据驱动的思想
操作 DOM 思想:
- 操作 DOM 比较消耗性能,不熟悉可能会导致 DOM 重排/重绘等
- 操作起来也相对麻烦一些
数据驱动思想:
- 操作数据,框架会按照相关的数据,让页面重新渲染
- 框架底层构建从虚拟 DOM(Virtual DOM)到真实 DOM 的渲染体系,有效避免 DOM 的重排和重绘
- 相比真实 DOM,虚拟 DOM 更为轻量级,效率更高
- 开发效率高,性能高
React 框架采用的是 MVC 体系;Vue 采用的是 MVVM 体系。
MVC = Model + View + Controler 控制层
- 单向驱动(视图 -> 数据需要开发者自行写代码实现)
- 需要按照专业的语法去构建视图(页面):React 中是基于 jsx 语法来构建视图的
- 构建数据层:但凡在视图中,需要“动态”处理的(需要变化的,不论是样式还是内容),都要有对应的数据模型
- 控制层:当在视图中(或者根据业务需求)进行某些操作时,都是去修改相关的数据,然后 React 框架会按照最新的数据,重新渲染视图,以此让用户看到最新的效果
MVVM = Model 数据层 + View 视图层 + ViewModel 数据视图监听层
- 双向驱动
- 数据驱动视图的渲染:监听数据的更新,让视图重新渲染
- 视图驱动数据的更改:监听页面中表单元素的内容改变,自动去修改相关的数据
- Vue 自己实现了 Template
JSX 语法
jsx:JavaScript amd XML (html) 把 JS 和 HTML 标签混合在一起
jsx 实际上就是一种js的增强性语法 babel中将jsx最后编译成为js 也就是将jsx中一些功能转化成js中可以实现的部分 比如渲染的
就会转换成js对象去表示当前元素的信息 类似于vue中的dom节点
1 | import React from 'react' // React 语法核心 |
可以通过 {}
嵌入 JS 表达式来渲染:
1 | import React from 'react'; |
常见的 JS 表达式有:
- 变量/值
- 数学运算
- 三目表达式
- 借助于数组迭代方法的循环,
map filter
等 - 有返回值的函数调用
{}
语法中嵌入不同的值,所呈现出来的特点如下:
number / string:值是什么,就渲染出来什么
bool / null / undefined / Symbol / Bigint:渲染内容是空
普通对象:不支持渲染
数组对象:把每一项拿出来,分别渲染(并不是变为字符串渲染,中间没有逗号,如果数组中有不支持渲染的元素,如普通对象,也会报错)
正则对象、时间对象、包装类对象:不支持渲染
函数对象:不支持在
{}
中渲染,但是可以作为函数组件,作为组件<componment />
渲染除数组对象之外,其余对象一般都不支持在
{}
中渲染,但也有特殊情况:- JSX 虚拟 DOM 对象
- 给元素设置 style 样式,要求写成一个对象格式
元素设置行内样式
行内样式,需要基于对象的格式处理,直接写成字符串会报错。
1 | <h2 style={{color: 'red', fontSize: '18px'}}>Learn React</h2> |
样式属性要基于小驼峰命名法。
设置样式类名:要把 class
替换为 className
:
1 | <h2 className="box"></h2> |
需求一:基于数据的值,来判断元素的显示隐藏
1 | <div> |
需求二:从服务器获取了一组列表数据,循环动态绑定相关的内容
1 | const root = ReactDOM.createRoot(document.getElementById('root')) |
对于
Array()
函数,如果参数仅传入一个数值,则该参数表示长度,即:
1 new Array(5) // 返回数组长度为 5 的稀疏数组,其每一项都是 empty使用数组的迭代方法(
forEach
或map
),它们不会去迭代稀疏数组,例如:
1
2
3
4 let arr = new Array(5)
arr.forEach(() => {
console.log('OK') // 不打印任何输出
})可以基于数组的
fill
方法,将稀疏数组进行填充,变为密集数组,就可以使用数组的迭代方法了。
1
2
3
4 let arr2 = arr.fill(null) // arr2 = [null, null, null, null, null]
arr2.forEach( () => {
console.log('OK') // 输出 5 次 'ok'
} )J
JSX 底层渲染机制
编写的 JSX 语法,编译成虚拟 DOM 对象,virtual DOM 对象是一个普通的 JS 对象,用来描述真实 DOM 对象的
虚拟 DOM 对象:框架内部自构建的一套对象体系(对象的相关成员都是 React 内部规定的,基于属性构建视图,相关特征)
基于 babel-preset-react-app 插件,把 JSX 语法编译成 React.createElement(…)语法,React.createElement(ele,props,…children)
ele:元素类型,字符串或者函数,函数就是组件
props:当前元素的属性对象,没有就是 nullchildren:当前元素的子元素,没有就是 undefined
React.createElement 执行,返回一个对象,对象中包含了当前元素的描述信息(虚拟 DOM 对象)
let virtualDOM = { $$typeof: Symbol(react.element), ref: null, key: null, type: 'h1', // 标签名 [组件] props: { id: 'title', className: 'title', children: 'hello world' } }
1
2
3
4
5
6
7
8
9
10
- 把构建的 virtual DOM 对象渲染成真实的 DOM 元素,第一次渲染是把所有的内容都渲染到页面中,第二次及以后的渲染,都是把最新的数据和上一次的虚拟 DOM 进行比对(DOM-DIFF),把不同的地方重新渲染(PATCH),这样可以提升渲染的性能
基于 ReactDom 中的 render 方法
- v16
```javascript
ReactDOM.render(virtualDOM, container, callback)
ReactDOM.render(<>...</>, document.getElementById('root'))v18
1
2
3
4cosnt root = ReactDOM.createRoot(doucment.getElementById('root'))
root.render(
<>...</>
)
之所以有区别是因为 在react18 中默认的create-react-app中引入的是”react-dom/client” 是客户端(浏览器)使用的包 而在react16中使用的是”react-dom” 其根对象中包括了ReactDom.render 方法
封装一个对象的迭代方法
- 基于传统的 for/in 循环,会存在一些弊端【性能差(公有私有都会迭代),只能迭代”可枚举”,非 Symbol 类型的属性】
- 解决办法:获取对象的所有私有属性
a.Object.getOwnPropertyNames(arr)
–> 获取对象的私有属性(不包含 Symbol 类型的属性)
b.Object.getOwnPropertySymbols(arr)
–> 获取对象的私有 Symbol 类型的属性名
c. 获取所有私有属性let keys = Object.getOwnPropertyNames(arr).concat(Object.getOwnPropertySymbols(arr))
- 可以基于 ES6 中的 reflect.ownKeys 代替上述操作 [不能兼容 IE]
let keys = Reflect.ownKeys(arr)
1 | export const each = (obj: any, callback: Function) => { |
函数组件的底层渲染机制
函数组件
使用函数去返回JSX
试图的组件的写法就是函数组件
调用: 基于 ES6Module 规范,导入创建的组件可以不用.jsx,写标签调用组件即可<Component/ >单闭合 <Component></Component> 双闭合
调用组件的时候可以给调用的组件设置传递各种各样的属性<DemoOne title="我是标题" x={10} data={[100,200]} className="box" style={{fontSize:'20px'}}></DemoOne>
a. 如果设置的属性不是字符串格式,需要基于{}
进行嵌套
b. 调用组件的时候,可以把一些数据/信息基于属性 props 的方式,传递给组件
命名:组件的名字,采用大驼峰 PascalCase 命名
渲染机制 :
1 基于babel-preset-react-app
把调用的组件转换为 createElement 格式(babel 插件)
1 | React.createElement(DemoOne,{ |
2 把 createElement 方法执行,创建出一个 virtualDom 对象
1 | { |
3 基于 root.render 把 virtualDom 变成真是的 Dom
type 值不再是一个字符串,而是变成一个函数此时
- 把函数执行 -> DemoOne() ->
<DemoOne />
- 把 virtualDom 中的 props,作为实参传递给函数 ->
DemoOne(props)
- 接收函数执行的返回结果(当前组件的
virtualDOM
对像) - 最后基于 render 把组件返回的虚拟 DOM 变成真实的 DOM,插入到
#root
容器中
属性 props 的处理
调用组件,传递进去的属性是“只读”的 [原理:props 对象被冻结了]
获取: props.xxx
修改:props.xxx = xxx ==> 报错 error
关于对象的规则设置
- 冻结
冻结对象:Object.freeze(obj)
检测是否被冻结:Object.isFrozen(obj) => true/false
被冻结的对象:不能修改成员值、不能新增成员、不能删除现有成员、不能给成员做劫持[Object.defineProperty]- 密封
密封对象:Object.seal(obj)
检测是否被密封:Object.isSealed(obj)
被密封的对象:可以修改成员的值,但也不能删、不能新增、不能劫持- 扩展
把对象设置成不可扩展:Object.preventExtensions(obj)
检测是否可扩展:Object,isExtensible(obj)
被设置不可扩展的对象:除了不能新增成员,其余都可操作
作用:父组件(index.jsx)调用子组件(DemoOne.jsx)的时候,可以基于属性,把不同的信息传递给子组件;子组件接受相应的属性值,呈现出不同的效果,让组件的复用性更强!!
虽然对于传递进来的属性,我们不能直接修改,但是可以做一些规则校验
设置默认值:
函数组件.defaultProps = {
x:0,
…..
}
设置其他规则,例如:数据值格式、是否必传。。。[依赖官方的插件:prop-types]
1 | `import PropTypes from 'prop-types'` |
传递进来的属性,首先会经历规则的校验,不管校验成功还是失败,最后都会把属性给 props,只不过如果不符合设定的规则,控制台会抛出警告错误(不影响属性值的获取)
如果想把传递的属性值进行修改,我们可以赋值给其他内容或者修改变量/状态值
插槽 slot 处理机制
封装 DemoOne 组件具有更强的复用性 [传递 HTML 结构]
1 | <DemoOne title='xx' x={100}> |
封装组件的时候,预留插槽位置,内容不用写,调用组件的时候基于双闭合调用的方式把插槽信息【子节点信息】传递给组件,组件内部进行渲染即可props.children
获取子节点信息
传递数据用属性
传递 HTML 结构用插槽
在组件中对children
进行处理
1 | const DemoOne = (props) => { |
比如下面的一个简单的例子
1 | root.render( |
1 | const DemoOne = (props) => { |
静态组件
函数组件是静态组件
第一次渲染组件,把函数执行:
- 产生一个私有的上下文:EC(V)
- 把解析出来的 props [包含 children] 传递进来 [被冻结]
- 对函数返回的 JSX 元素进行渲染
当我们点击按钮的时候,会把函数绑定并且执行
- 修改上级上下文 EC(V)中变量
- 私有变量值发生了改变
- 视图不会更新
类组件写法
1 | /* |
React 中的事件
1 | const App = () => { |
React 中的 state
1 | import './App.css' |
state 的注意
state
state 实际就是一个被 React 管理的变量,当我们通过 setState()修改变量的值时,会触发组件的自动重新渲染
只有 state 值发生变化时,组件才会重新渲染
当 state 的值是一个对象时,修改时是使用新的对象去替换已有对象
当通过 setState 去修改一个 state 时,并不表示修改当前的 state 它修改的是组件下一次渲染时 state 值
setState()会触发组件的重新渲染,它是异步的所以当调用 setState()需要用旧 state 的值时,一定要注意有可能出现计算错误的情况为了避免这种情况,可以通过为 setState()传递回调函数的形式来修改 state 值
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
38
39
40
41
42
43import {useState} from "react";
const [counter, setCounter] = useState(1);
const [user, setUser] = useState({name: '孙悟空', age: 18});
const addHandler = () => {
setTimeout(() => {
// setCounter(counter + 1); // 将counter值修改为2
setCounter((prevCounter)=>{
/*
* setState()中回调函数的返回值将会成为新的state值
* 回调函数执行时,React会将最新的state值作为参数传递
* */
return prevCounter + 1;
});
// setCounter(prevState => prevState + 1);
}, 1000);
const updateUserHandler = () => {
// setUser({name:'猪八戒'});
// 如果直接修改旧的state对象,由于对象还是那个对象,所以不会生效
// user.name = '猪八戒';
// console.log(user);
// setUser(user);
// const newUser = Object.assign({}, user);
// newUser.name = '猪八戒';
// setUser(newUser);
setUser({...user, name: '猪八戒'});
};
return <div className={'app'}>
<h1>{counter} -- {user.name} -- {user.age}</h1>
<button onClick={addHandler}>1</button>
<button onClick={updateUserHandler}>2</button>
</div>;
};
// 导出App
export default App;
React 中的双向数据绑定
使用useState
去控制表单
1 | import React, { useState } from "react" |
React 中的 portal
组件默认会作为父组件的后代渲染到页面中
存在一些问题 比如要写对话框和模态框的时候需要渲染到根目录下
通过 protal 可以将组件渲染到页面中的指定位置
使用方法
- 在
index.html
添加一个新元素 - 修改组件的渲染方式
- 通过 ReactDom.createPortal() 作为返回值创建元素
- 参数
- jsx (修改前 return 的代码)
- 目标位置 (DOM 元素)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import React from 'react'
import './Backdrop.css'
import ReactDOM from 'react-dom'
// 获取backdrop的根元素
const backdropRoot = document.getElementById('backdrop-root')
const Backdrop = (props) => {
return ReactDOM.createPortal(
<div className="backdrop">{props.children}</div>,
backdropRoot
)
}
export default Backdrop- 在
React 中的CSS-Module
CSS 模块
- 类似于 Vue 中的 style 中的
scope
可以避免被全局污染 - 使用步骤
- 创建一个 xxx.module.css
- 在组件中引入 css
import classes from './App.module.css';
- 通过 classes 来设置类
className={classes.p1}
- CSS 模块可以动态的设置唯一的 class 值
React.Fragment
- 是一个专门用来作为父容器的组件
- 它只会将它里边的子元素直接返回,不会创建任何多余的元素
- 当我们希望有一个父容器但同时又不希望父容器在网页中产生多余的结构时就可以使用 Fragment
React 中的 Context
Context
相当于一个公共的存储空间,我们可以将多个组件中都需要访问的数据统一存储到一个Context
中,这样无需通过props
逐层传递,即可使组件访问到这些数据
通过React.createContext()
创建 context
1 | import React from 'react' |
使用方式
使用方式一(不推荐)
引入 context
使用
Xxx.Consumer
组件来创建元素Comsumer 的标签体需要一个回调函数会将 context 设置为回调函数的参数通过参数能访问到 context 中存储的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import React from 'react'
import TestContext from '../store/testContext'
const A = () => {
return (
<TestContext.Consumer>
{(ctx) => {
return (
<div>
{ctx.name} - {ctx.age}
</div>
)
}}
</TestContext.Consumer>
)
}
export default A
使用 Context 方式二
- 导入 Context
- 使用钩子函数 useContext()获取到 context
- useContext() 需要一个 Context 作为参数它会将 Context 中数据获取并作为返回值返回
Xxx.Provider
表示数据的生产者,可以使用它来指定 Context 中的数据
通过 value 来指定 Context 中存储的数据, 这样一来,在该组件的所有的子组件中都可以通过 Context 来访问它所指定数据
当我们通过 Context 访问数据时,他会读取离他最近的 Provider 中的数据,如果没有 Provider,则读取 Context 中的默认数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14import React, { useContext } from 'react'
import TestContext from '../store/testContext'
const B = () => {
// 使用钩子函数获取Context
const ctx = useContext(TestContext)
return (
<div>
{ctx.name} -- {ctx.age}
</div>
)
}
export default B
React 中的 Effect
Effect
React组件有部分逻辑都可以直接编写到组件的函数体中,像是对数组调用filter、map
等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。
React.StrictMode
编写 React 组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果你的 React 使用了严格模式,也就是在 React 中使用了React.StrictMode
标签,那么 React 会非常“智能”的去检查你的组件中是否写有副作用的代码,当然这个智能是加了引号的,我们来看看 React 官网的文档是如何说明的:
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
- Class component
constructor
,render
, andshouldComponentUpdate
methods - Class component static
getDerivedStateFromProps
method - Function component bodies
- State updater functions (the first argument to
setState
) - Functions passed to
useState
,useMemo
, oruseReducer
上文的关键字叫做“double-invoking”即重复调用,这句话是什么意思呢?大概意思就是,React 并不能自动替你发现副作用,但是它会想办法让它显现出来,从而让你发现它。那么它是怎么让你发现副作用的呢?React 的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了 React 严格模式时,这些函数会被调用两次:
- 类组件的的
constructor
,render
, 和shouldComponentUpdate
方法 - 类组件的静态方法
getDerivedStateFromProps
- 函数组件的函数体
- 参数为函数的
setState
- 参数为函数的
useState
,useMemo
, oruseReducer
重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个console.log
你会发现它会执行两次
setState 执行流程
Too many re-renders
- 当我们直接在函数中调用 setState 时,就会触发上述报错
- setState() 的执行流程 (函数组件)
setCount()
—> dispatchSetDate()—>会先判断,组件当时处于什么阶段 ( 如果是渲染阶段 —> 不会检查 state 值是否相同 | 如果不是渲染阶段—> 值不同,重新渲染;值相同,不重新渲染)
使用 useEffect
useEffect()
是一个钩子函数,需要一个函数作为参数,这个作为参数的函数,将会在组件渲染完毕后执行,开发中可以将会产生副作用的代码编写到其中
默认情况下,useEffect()中的函数,会在组件渲染完成后调用,并且是每次渲染完成后都会调用
在 useEffect() 可以传递一个第二参数,第二参数是一个数组,在数组中可以指定 Effect 的依赖项,指定后,只有当依赖项发生变化时,Effect 才会被触发
通常会将 Effect 中使用的所有的变量都设置成依赖项
1
2
3
4
5
6useEffect(() => {
console.log('执行了~~')
if (ctx.totalAMount === 0) {
setShowDetails(false)
}
}, [ctx])**setState()**是由钩子函数 useState()生成的 不会发现变化可以不用加到 dep 里面
如果依赖项设置的是空数组,则意味 Effect 只会在组件初始化时触发一次
Effect 函数的返回
在 Effect 的回调函数中,可以指定一个函数作为返回值,这个函数可以称为清理函数,他会在下次 Effect 执行前调用可以在此函数中,做一些工作来清除上次 Effect 执行所带来的影响
1 | Effetc(() => { |
React 中的 Reducer
在 React 的函数组件中,我们可以通过 useState()来创建 state。这种创建 state 的方式会给我们返回两个东西 state 和 setState()。state 用来读取数据,而 setState()用来设置修改数据。但是这种方式也存在着一些不足,因为所有的修改 state 的方式都必须通过 setState()来进行,如果遇到一些复杂度比较高的 state 时,这种方式似乎就变得不是那么的优雅,比如现在有很多数据,但是useState()
只给我们提供了一个setCartData()
方法,就会很麻烦
为了解决复杂State
带来的不便,React
为我们提供了一个新的使用State
的方式。Reducer
个人认为Reducer
可以翻译为“整合器”,它的作用就是将那些和同一个state
相关的所有函数都整合到一起,方便在组件中进行调用。
当然工具都有其使用场景,Reducer
也不例外,它只适用于那些比较复杂的state
,对于简单的state
使用Reducer
只能是徒增烦恼。下面用一个简单的演示一下
和State
相同Reducer
也是一个钩子函数,语法如下:const [state, dispatch] = useReducer(reducer, initialArg, init);
参数
- reducer:整合函数
- 对于我们当前 state 的所有操作都应该在该函数中定义
- 该函数的返回值,会成为 state 的新值
- reducer 在执行时,会收到两个参数:
- state 当前最新的 state
- action 它需要一个对象 在对象中会存储 dispatch 所发送的指令
- initialArg : state 的初始值,作用和 useState()中的值是一样
返回值
- 数组
- 第一个参数,state 用来获取 state 的值
- 第二个参数,state 修改的派发器通过派发器可以发送操作 state 的命令具体的修改行为将会由另外一个函数(reducer)执行
1 | import React, { useReducer, useState } from 'react' |
React 中的 memo
React.memo() 是一个高阶组件
- 它接收另一个组件作为参数,并且会返回一个包装过的新组件
- 包装过的新组件就会具有缓存功能,装过后,只有组件的 props 发生变化化才会触发组件的重新的渲染,否则总是返回缓存中结果
React 中的 useCallback
useCallback:是一个钩子函数,用来创建 React 中的回调函数,创建的回调函数不会总在组件重新渲染时重新创建useCallback()
参数
- 回调函数
- 依赖数组
- 当依赖数组中的变量发生变化时,回调函数才会重新创建
- 如果不指定依赖数组,回调函数每次都会重新创建
- 一定要将回调函数中使用到的所有变量都设置到依赖数组中 除了(setState)
React 中的 Hooks
React 中的钩子函数只能在函数组件或自定义钩子中调用,我们需要将 React 中钩子函数提取到一个公共区域时,就可以使用自定义钩子
自定义钩子其实就是一个普通函数,只是它的名字需要使用 use 开头
比如下面封装了fetch
自定义函数
1 | import { useCallback, useState } from 'react' |
Redux
A Predictable State Container for JS Apps
- A Predictable State Container for JS Apps 是 Redux 官方对于 Redux 的描述,这句话可以这样翻译“一个专为 JS 应用设计的可预期的状态容器”,简单来说 Redux 是一个可预测的状态容器。
状态(State)
state 直译过来就是状态 state 不过就是一个变量,一个用来记录(组件)状态的变量。组件可以根据不同的状态值切换为不同的显示
容器(Container)
容器当然是用来装东西的,状态容器即用来存储状态的容器。状态多了,自然需要一个东西来存储,但是容器的功能却不是仅仅能存储状态,它实则是一个状态的管理器,除了存储状态外,它还可以用来对 state 进行查询、修改等所有操作。
可预测(Predictable)
可预测指我们在对 state 进行各种操作时,其结果是一定的。即以相同的顺序对 state 执行相同的操作会得到相同的结果。简单来说,Redux 中对状态所有的操作都封装到了容器内部,外部只能通过调用容器提供的方法来操作 state,而不能直接修改 state。这就意味着外部对 state 的操作都被容器所限制,对 state 的操作都在容器的掌控之中,也就是可预测。
总的来说,Redux 是一个稳定、安全的状态管理器。
使用
使用 Redux 之前,你需要先明确一点 Redux 是 JS 应用的状态容器,它并不是只能在 React 使用,而是可以应用到任意的 JS 应用中(包括前端 JS,和服务器中 Node.js)。总之,凡是 JS 中需要管理的状态的 Redux 都可以胜任。
在网页中使用
我们先来在网页中使用以下 Redux,在网页中使用 Redux 就像使用 jQuery 似的,直接在网页中引入 Redux 的库文件即可:<script src="https://unpkg.com/redux@4.2.0/dist/redux.js"></script>的
如果不使用 redux 的时候
1 | const btn01 = document.getElementById('btn01'); const btn02 = |
上述代码中 count 就是一个状态,只是这个状态没有专门的管理器,它的所有操作都在事件的响应函数中进行处理,这种状态就是不可预测的状态,因为在任何的函数中都可以对这个状态进行修改,没有任何安全限制。Redux 的真实使用场景依然是大型应用中的复杂 state。
Redux 是一个状态容器,所以使用 Redux 必须先创建容器对象,它的所有操作都是通过容器对象来进行的,创建容器的方式有多种,我们先说一种好理解的:Redux.createStore(reducer,[perloadedState],[enhancer])
createStore用来创建一个Redux 中的容器对象,它需要三个参数:reducer、preloadedState、enhancer
- reducer:是一个函数,state 操作的整合函数,每次修改 state 都会触发该函数,返回值会变成新的 state
- preloadedState:是 state 的初始值,可以在这里指定也可以在 reducer 中指定。
- enhancer:增强函数用来对 state 的功能进行扩展
三个参数中,只有 reducer 是必须的,来看一个 Reducer 的示例:
1 | const countReducer = (state = { count: 0 }, action) => { |
reducer 用来整合关于 state 的所有操作,容器修改 state 时会自动调用该函数,函数调用时会接收到两个参数:state 和 action,state 表示当前的 state,可以通过该 state 来计算新的 state。state = {count:0}
这是在指定 state 的默认值,如果不指定,第一次调用时 state 的值会是 undefined。也可以将该值指定为 createStore()的第二个参数。action 是一个普通对象,用来存储操作信息。
将 reducer 传递进 createStore 后,我们会得到一个 store 对象:const store = Redux.createStore(countReducer);
store 对象创建后,对 state 的所有操作都需要通过它来进行:
读取 state:store.getState()
修改 state:store.dispatch({type:'ADD'})
dipatch 用来触发 state 的操作,可以将其理解为是想 reducer 发送任务的工具。它需要一个对象作为参数,这个对象将会成为 reducer 的第二个参数 action,需要将操作信息设置到对象中传递给 reducer。action 中最重要的属性是 type,type 用来识别对 state 的不同的操作,上例中’ADD’表示增加操作,’SUB’表示减少的操作
除了这些方法外,store 还拥有一个 subscribe 方法,这个方法用来订阅 state 变化的信息。该方法需要一个回调函数作为参数,当 store 中存储的 state 发生变化时,回调函数会自动调用,我们可以在回调函数中定义 state 发生变化时所要触发的操作:
1 | store.subscribe(() => { |
如此一来,刚刚的代码被修改成了这个样子:
1 | const btn01 = document.getElementById('btn01') |
修改后的代码相较于第一个版本要复杂一些,同时也解决了之前代码中存在的一些问题:
- 前一个版本的代码 state 就是一个变量,可以任意被修改。state 不可预测,容易被修改为错误的值。新代码中使用了 Redux,Redux 中的对 state 的所有操作都封装到了 reducer 函数中,可以限制 state 的修改使 state 可预测,有效的避免了错误的 state 值。
- 前一个版本的代码,每次点击按钮修改 state,就要手动的修改 counterSpan 的 innerText,非常麻烦,这样一来我们如果再添加新的功能,依然不能忘记对其进行修改。新代码中,counterSpan 的修改是在 store.subscribe()的回调函数中进行的,state 每次发生变化其值就会随之变化,不需要再手动修改。换句话说,state 和 DOM 元素通过 Redux 绑定到了一起。
通过上例也不难看出,Redux 中最最核心的东西就是这个 store,只要拿到了这个 store 对象就相当于拿到了 Redux 中存储的数据。在加上 Redux 的核心思想中有一条叫做“单一数据源”,也就是所有的 state 都会存储到一课对象树中,并且这个对象树会存储到一个 store 中。所以到了 React 中,组件只需获取到 store 即可获取到 Redux 中存储的所有 state。
下面给出一个具体的代码示例
1 |
|
**RTX(Redux Toolkit)**工具包
在 React 中使用 RTKyarn add react-redux @reduxjs/toolkit
具体使用
1 | // 使用RTK来构建store |
RTX 具体使用案例
store/index.js
中
1 | import { configureStore, createSlice } from '@reduxjs/toolkit' |
App.js
1 | import React from 'react' |
拆分 RTX
当有不同的多个数据的时候比如存在学生姓名和学校schoolSlice.js
1 | //创建学校的slice |
stuSlice.js
1 | // createSlice 创建reducer的切片 |
store/index.js
1 | //使用RTK来构建store |
App.js
1 | import React from 'react' |
RTXQ 使用
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react' |
React 中的 router
使用 React 这些工具所编写的项目通常都是单页应用(SPA)。单页应用中,整个应用中只含有一个页面,React 会根据不同的状态在应用中显示出不同的组件。但是我们之前所编写应用还存在着一个问题,整个应用只存在一个页面,一个请求地址,这就使得用户只能通过一个地址访问应用,当我们点击组件中的不同链接时应用的地址是不会发生变化的。这又有什么问题呢?由于应用只有一个地址,所以我们通过该地址访问应用时,总会直接跳转到应用的首页。如此一来,我们便不敢随意的刷新页面,因为一旦刷新页面便直接跳转到首页。在对页面进行分享时,也只能分享网站的首页,而不能分享指定的页面。
怎么办呢?难道我们要将一个页面拆分为多个页面吗?很明显不能这么做,这么做以后应用的跳转便脱离了 React 的控制,增加应用的复杂度,提高了项目维护的成本。
为了解决这个问题,我们需要引入一个新的工具 React Router,React Router 为我们提供一种被称为客户端路由的东西,通过客户端路由可以将 URL 地址和 React 组件进行映射,当 URL 地址发生变化时,它会根据设置自动的切换到指定组件。并且这种切换完全不依赖于服务器。换句话说,在用户看来浏览器的地址栏确实发生了变化,但是这一变化并不由服务器处理,而是通过客户端路由进行切换。
… 懒的写了
关于 React 中的 hook
关于 React 中的钩子函数,我们已经非常熟悉了。钩子函数的功能非常的强大,而它的使用又十分简单。关于钩子函数的使用,我们只需记住两点:
- 钩子只能在 React 组件和自定义钩子中使用
- 钩子不能在嵌套函数或其他语句(if、switch、white、for 等)中使用
React 中自带的钩子函数
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useRef
- useMemo
- useImperativeHandle
- useLayoutEffect
- useDebugValue
- useDeferredValue
- useTransition
- useId
- useSyncExternalStore
- useInsertionEffect
useMemo
useMemo 和 useCallback 十分相似,useCallback 用来缓存函数对象,useMemo 用来缓存函数的执行结果。在组件中,会有一些函数具有十分的复杂的逻辑,执行速度比较慢。闭了避免这些执行速度慢的函数返回执行,可以通过 useMemo 来缓存它们的执行结果,像是这样:
1 | const result = useMemo(() => { |
useMemo 中的函数会在依赖项发生变化时执行,注意!是执行,这点和 useCallback 不同,useCallback 是创建。执行后返回执行结果,如果依赖项不发生变化,则一直会返回上次的结果,不会再执行函数。这样一来就避免复杂逻辑的重复执行。
UseImperativeHandle
在 React 中可以通过 forwardRef 来指定要暴露给外部组件的 ref:
1 | const MyButton = forwardRef((props, ref) => { |
上例中,MyButton 组件将 button 的 ref 作为组件的 ref 向外部暴露,其他组件在使用 MyButton 时,就可以通过 ref 属性访问:
1 | <MyButton ref={btnRef} /> |
通过 useImperativeHandle 可以手动的指定 ref 要暴露的对象,比如可以修改 MyButton 组件如下:
1 | const MyButton = forwardRef((props, ref) => { |
useImperativeHandle 的第二个参数是一个函数,函数的返回值会自动赋值给 ref(current 属性)。上例中,我们将返回值为{name:'孙悟空'}
,当然返回孙悟空没有什么意义。实际开发中,我们可以将一些操作方法定义到对象中,这样可以有效的减少组件对 DOM 对象的直接操作。
1 | const MyButton = forwardRef((props, ref) => { |
UseLayoutEffect
useLayoutEffect 的方法签名和 useEffect 一样,功能也类似。不同点在于,useLayoutEffect 的执行时机要早于 useEffect,它会在 DOM 改变后调用。在老版本的 React 中它和 useEffect 的区别比较好演示,React18 中,useEffect 的运行方式有所变化,所以二者区别不好演示。
useLayoutEffect 使用场景不多,实际开发中,在 effect 中需要修改元素样式,且使用 useEffect 会出现闪烁现象时可以使用 useLayoutEffect 进行替换。
UseDebugValue
用来给自定义钩子设置标签,标签会在 React 开发工具中显示,用来调试自定义钩子,不常用。
UseDeferredValue
useDeferredValue 用来设置一个延迟的 state,比如我们创建一个 state,并使用 useDeferredValue 获取延迟值:
1 | const [queryStr, setQueryStr] = useState(''); |
上边的代码中 queryStr 就是一个常规的 state,deferredQueryStr 就是 queryStr 的延迟值。设置延迟值后每次调用 setState 后都会触发两次组件的重新渲染。第一次时,deferredQueryStr 的值是 queryStr 修改前的值,第二次才是修改后的值。换句话,延迟值相较于 state 来说总会慢一步更新。
延迟值可以用在这样一个场景,一个 state 需要在多个组件中使用。一个组件的渲染比较快,而另一个组件的渲染比较慢。这样我们可以为该 state 创建一个延迟值,渲染快的组件使用正常的 state 优先显示。渲染慢的组件使用延迟值,慢一步渲染。当然必须结合 React.memo 或 useMemo 才能真正的发挥出它的作用。
UseTransition
当我们在组件中修改 state 时,会遇到复杂一些的 state,当修改这些 state 时,甚至会阻塞到整个应用的运行,为了降低这种 state 的影响,React 为我们提供了 useTransition,通过 useTransition 可以降低 setState 的优先级。
useTransition 会返回一个数组,数组中有两个元素,第一个元素是 isPending,它是一个变量用来记录 transition 是否在执行中。第二个元素是 startTransition,它是一个函数,可以将 setState 在其回调函数中调用,这样 setState 方法会被标记为 transition 并不会立即执行,而是在其他优先级更高的方法执行完毕,才会执行。
除了 useTransition 外,React 还直接为为我们提供了一个 startTransition 函数,在不需要使用 isPending 时,可以直接使用 startTransition 也可以达到相同的效果。
UseId
生成唯一 id,使用于需要唯一 id 的场景,但不适用于列表的 key。