React

React简介
React是什么?
React
是用于构建用户界面的
JavaScript库,换句话来说,React是一个将数据渲染为HTML视图的
开源JavaScript库。
React是谁开发的?
由Facebook
公司开发,且开源。【2013年5月宣布开源】近十年沉淀,现被大厂广泛使用。
为什么要学React?
- 原生JavaScript操作DOM繁琐、效率低(
用DOM的API操作UI
) - 使用JavaScript
直接操作DOM
,浏览器会进行大量的重绘重排。 - 原生JavaScript
没有组件化编码方案
,代码复用率低。
同时这些也是原生JS的痛点
React的特点
- 采用
组件化
模式、声明式
编码,提高开发效率及组件复用率。 - 在
React Native
中可以使用React
语法进行移动端开发。 - 使用
虚拟DOM
+优秀的Diffing
算法,尽量减少与真实DOM的交互。 - 速度快(UI渲染中,React通过虚拟DOM与真实DOM的比较,直接避免了不必要的真实DOM操作)
- 跨浏览器兼容、兼容性好(requireJS加载&打包),...
[ 运行这段原生
JavaScript实现的代码之后,有两个人的数据,分别为张三、李四。如果我想再往这上面再添加一个王五,JS不会复用页面已有的张三、李四这两条数据,而是重新再生成
3个DOM,等于说前两个没必要重新渲染,但还是被渲染了,一旦数据量大,性能方面就有问题了。]
- 如果往100个人的后面新增一个人,前面100个人都没变,但最终渲染时,会重新生成101个DOM,你会觉得性能浪费吗?1000个人呢?10000个呢?
解释:
React没有直接操作真实DOM,而是通过操作虚拟DOM的方式来进行比较,最终才操作真实DOM。
上面的例子:100个人,第一次React会生成100个虚拟DOM通过比较,没有,则生成100个真实DOM;第二次追加一个人,React会进行虚拟DOM的比较,发现前面的100个React都有,那么最终只生成1个真实DOM。而不是像原生JS一样,追加一条数据,会全部重新渲染,又生成101个真实DOM,不能复用。这就是React的优点之一。
学习React之前你要掌握的JavaScript基础知识
- 判断this的指向
- class(类)
- ES6语法规范
- 包管理器(npm,yarn等)
- 原型、原型链
- 数组常用方法
- 模块化
使用create-react-app创建React应用
React脚手架
- xxx脚手架:用来帮助程序员
快速创建
一个基于xxx库的模板项目。
- (1)包含了所有需要的配置(语法检查、jsx编译、devServer...)
- (2)下载好了所有相关的依赖
- (3)可以直接运行一个简单的效果
- React提供了一个用于创建React项目的脚手架库:
creat-react-app
- 项目的整体技术架构为:react + webpack + es6 + eslint
- 使用脚手架开发的项目的特点:模块化、组件化、工程化
创建项目并启动
- 第一步,全局安装:
npm install -g create-react-app
- 第二步,切换到想创项目的目录,使用命令:
create-react-app
项目名 - 第三步,进入项目文件夹:
cd
项目名 - 第四步,启动项目:
npm start 或 yarn start
,看package.json文件具体命令
JSX(JavaScript XML)
前言(扯皮-可以不用知道)
-
React的特点之一就是
JSX
,JSX是JavaScript的语法的扩展
,使用JSX来开发
UI内容。React开发不一定需要
使用JSX,但使用JSX会非常便捷。 -
实际上
JSX
是React.createElement
函数的语法糖
,使用JSX需要使用Babel
来将JSX转化成createElement函数调用(React 16版本)。
// JSX Version
import React from 'react'
function App() {
return <h1>Hello,React</h1>
}
// Compiled Version - 编译后
import React from 'react'
function App() {
return React.createElement('h1', null, 'Hello,React')
}
3.React 17
提供了一个全新的,重构过
的 JSX 转换
的版本。jsx语法不再化为createElement函数,而是内部通过react/jsx-runtime
中jsx函数
生成虚拟对象。
注意:
在React 17
如果引入React仅为了JSX
存在你就可以从组件代码中删除
React的导入。
// import React from 'react'
function App() {
return <h1>Hello World!</h1>
}
了解:
React 17编译器从react/jsx-runtime
导入了一个新的依赖项,它处理JSX转换
。
import { jsx as _jsx } from 'react/jsx-runtime'
function App() {
return _jsx('h1', { children: 'Hello World!' })
}
什么是JSX?
-
JSX
是JavaScript
的一种语法扩展
,运用于React
架构中,其格式像模板语言,但事实上完全是在JavaScript内部实现的。 -
元素是构成React应用的最小单位,JSX就是用来
声明
React当中的元素,React使用JSX来描述用户界面。
React为什么推荐使用JSX?
官方
:React不强制要求使用
JSX,但大多数人发现,在JavaScript代码中将JSX和UI放在一起时,会在视觉上有辅助作用。它还可以使React显示更多有用的错误和警告信息。
个人
:JSX语法糖允许前端开发者使用我们最熟悉的类HTML标签语法
来创建虚拟DOM
,在降低学习成本的同时,也提升了研发效率与研发体验。
JSX的使用
函数式
function Demo() {
// JSX
return (
<div>
<p>函数式在这里写JSX</p>
</div>
)
}
类式
class Demo extends React.Component {
render() {
{/* JSX */}
return {
<div>
<p>类式在这里写JSX</p>
</div>
}
}
}
使用JSX创建React元素
const element = <h2>你好,React</h2>
在JSX中嵌入表达式
// 案例1
const name = 'JSX!'
const element = <h3>Hi, {name}</h3>
And
// 案例2 - 列表渲染
function Demo() {
const userInfo = [
{ id: '001', name: 'React', age: 10 },
{ id: '002', name: 'Vue', age: 7 },
{ id: '003', name: 'Angular', age: 3 }
]
return (
<ul>
// 在JSX语法中,你可以在大括号内放置任何有效的JavaScript表达式
{
userInfo.map(user => {
const {id, name, age} = user
return <li key={id}>name:{name},age:{age}</li>
})
}
</ul>
)
}
JSX条件渲染
// 案例3
function Demo() {
function test(flag) {
if (flag) {
return <h1>I Like React</h1>
}
return <h1>I Do Not Like React</h1>
}
return (
// <> </> 而不是 <div></div>,减少div层级嵌套
<>
{test(true)}
</>
)
}
JSX特定属性
// index.jsx
function FinalDemo() {
const info = {
img: 'http://www.xxx.com/xxx.png'
}
// 通过引号,将属性值指定为字符串字面量
const el0 = <p tabIndex="9"></p>
// 在属性值中插入一个JavaScript表达式
const el1 = <img src={info.img} />
// class -> className
const el2 = <div className="container">xxx</div>
return (
<>
{el0} - {el1} - {el2}
</>
)
}
JSX中绑定事件
function Demo() {
const name = 'Are You Happy?'
function changeName() {
return `Hi, ${name}`
}
return (
<div>
<button onClick={changeName}>按钮1</button>
or
<button onClick={() => `Hi, ${name}`}>按钮2</button>
</div>
)
}
组件三大核心属性
1:state
人的状态影响着行为,组件的状态驱动着页面
需求:定义一个展示天气信息的组件
1. 默认展示天气炎热 或 凉爽
2. 点击文字切换天气
import React, { Component } from 'react'
export default class Weather extends Component {
// 声明状态
state = {
isHot: true
}
changeWeather = () => {
// 改变状态
this.setState({ isHot: !this.state.isHot })
}
// 渲染函数
render () {
const { isHot } = this.state
return (
<>
{ isHot ? '天气炎热' : '天气凉爽' }
<button onClick={this.changeWeather}>改变天气</button>
</>
)
}
}
setState
用于更新状态
(3种方式)
注意:
在类组件中,通过setState
改变状态后立马获取该状态、获取到的值是上一次的。
import React, { Component } from 'react'
export default class Index extends Component {
state = {
num1: 0,
num2: 0,
num3: 0
}
try1() {
const { num1 } = this.state
// ①对象式的setState
this.setState({ num1: num1 + 1 })
console.log('num1:', num1)
}
try2 = () => {
const { num2 } = this.state
// ②对象式的setState
this.setState({
num2: num2 + 1
}, () => {
console.log('num2', num2)
// 对象式 & 函数式 第二个参数 - 可选
// 调用时机:它在状态更新、界面也更新后(调完render)之后
//调这个回调,可获取到最新的count值
console.log('this.state.num2:', this.state.num2)
})
}
try3 = () => {
const { num3 } = this.state
// ③函数式的setState
this.setState((state, props) => {
console.log('state', state)
console.log('props', props) // {}
return { num3: num3 + 1 }
})
console.log('num3:', num3)
}
render () {
return (
<>
<div>
num1:{this.state.num1}
<button onClick={this.try1.bind(this)}>尝试1</button>
</div>
<div>
num2:{this.state.num2}
<button onClick={this.try2}>尝试2</button>
</div>
<div>
num3:{this.state.num3}
<button onClick={this.try3}>尝试3</button>
</div>
</>
)
}
}
所以说,更新状态时不要在作用域内试图拿该状态去处理数据,容易出bug。
1.1:state理解
state
(React 状态)是组件对象最重要的属性,值是对象(可包含多个key-value的组合)
state = {
key1: value1,
key2: value2,
...
}
- React把组件看成是一个状态机(
State Machines
),通过更新组件的state
来更新对应的页面显示(重新渲染组件)
1.2:需要注意
-
类式组件中
render方法
中的this为组件实例对象 -
类式组件
自定义
的方法中this为undefined
,如何解决?
- a)
强制绑定this
:通过bind(this),↑ this.try1.bind(this) - b)
箭头函数
: func = () => {},通过作用域链查找获取this指向
- 状态数据,不能直接修改或更新
// wrong
this.state.isHot = false
// true
this.setState({ isHot: false })
2:(组件&)props
组件,从概念上类似于JavaScript函数。它接受任意的入参(即“props”
),并返回用于描述页面展示内容的React元素。
这段代码会在页面上渲染 Hello,React-props
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
const element = <Welcome name="React-props" />
ReactDOM.render(element, document.getElementsById('root'))
组合组件
我们可以创建一个可以多次渲染Welcome组件的App组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
function App() {
return (
<>
<Welcome name="React" />
<Welcome name="Angular" />
<Welcome name="Vue" />
</>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
提取组件:
将组件拆分为更小的组件
(可以忽略)
还未进行拆分的组件
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img
className="UserImg"
src={props.user.url}
alt={props.user.alt}
/>
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
//--------------------------
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
)
}
拆分第一步:拆img
function Img(props) {
return (
<img
className="UserImg"
src={props.info.url}
alt={props.info.alt}
/>
)
}
拆分第二步:将className为UserInfo的拆成组件
function UserInfo(props) {
return (
<Img info={props.msg} />
<div className="UserInfo-name">
{props.msg.name}
</div>
)
}
整合:
function Comment(props) {
<div className="Comment">
<UserInfo msg={props} />
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
}
Props的只读性
`组件无论是使用函数声明还是通过class声明,都决不能修改自身的props。
开发中遇到的问题
import React, { Component } from 'react'
export default class Index extends Component {
state = {
message: '3秒前值还未出现'
}
componentDidMount () {
this.changeMsg()
}
changeMsg = () => {
/*
1. 因为3秒后才更改了message的状态,所以在3秒之前父组件
传递的message还是之前的,意味着子组件接收到的props不是
最新的,3秒之后才能接收到最新的props。
*/
setTimeout(() => {
this.setState({ message: 'setTimeout-我出来了' })
}, 3000)
}
render () {
const { message } = this.state
return (
<>
<Text msg={message} />
</>
)
}
}
// 子组件
class Text extends Component {
lookProps = () => {
console.log('props', this.props.msg)
}
render () {
return (
<div>
<p>父组件传来的props:{this.props.msg}</p>
<button onClick={this.lookProps}>查看props</button>
</div>
)
}
}
注意:
开发中调用接口,async-await搭配使用,【await】需要等到数据拿到再进行后续操作,在调用接口之后通过this.setState({})改变状态(如果该状态依赖接口数据
)再传递给子组件,子组件拿不到最新的props。
伪代码:
const { list } = await request(API.xxx, params)
const data = list.filter(item => item.msg != '')
this.setState({ message: data.msg })
解决办法:
// 解决办法
// 与运算,第一项为true才会执行第二项,所以一定会拿到list数据,
// 解决了异步拿不到props数据
{
list.length > 0 && <Text1 msg={message} />
}
3:Refs
简介:
1. 用于获取DOM节点
或React组件实例
,(函数组件没有实例,这里指的是类组件)
创建方式:
① 字符串式声明ref
import React, { Component } from 'react'
export default class index extends Component {
onClick = () => {
console.log(this.refs)
}
render () {
return (
<div>
<input ref="inputRef1" type="text" />
<input ref="inputRef2" type="text" />
<button onClick={this.onClick}>打印this.refs</button>
</div>
)
}
}
打印结果:
将打印结果展开:
获取该input框的值:this.refs.inputRef1.value
同时,可以使用DOM上的API,比如:
import React, { Component } from 'react'
export default class index extends Component {
onClick = () => {
this.refs.inputRef1.focus()
}
render () {
return (
<div>
<input ref="inputRef1" type="text" />
<input ref="inputRef2" type="text" />
<button onClick={this.onClick}>点击按钮聚焦第一个输入框</button>
</div>
)
}
}
当然,这种字符串声明ref的方式已不再被推荐使用,将来可能会被废弃。
② 用回调的方式创建ref(推荐写法)
import React, { Component } from 'react'
export default class index extends Component {
getValue = () => {
console.log('第一个输入框的值:', this.inputRef1.value)
}
toFocus = () => {
this.inputRef2.focus()
}
render () {
return (
<div>
// e为input DOM本身
<input ref={e => this.inputRef1 = e} type="text" />
<input ref={e => this.inputRef2 = e} type="text" />
<button onClick={this.getValue}>获取第一个输入框的值</button>
<button onClick={this.toFocus}>聚焦第二个输入框</button>
</div>
)
}
}
③ createRef - 类组件
import { Component, createRef } from 'react'
export default class index extends Component {
constructor(props) {
super(props)
this.inputRef = createRef()
}
toFocus = () => {
console.log(this.inputRef)
this.inputRef.current.focus()
}
render () {
return (
<div>
<input ref={this.inputRef} type="text" />
<button onClick={this.toFocus}>点击按钮,打印并聚焦输入框</button>
</div>
)
}
}
creatRef-函数组件
import React from "react"
export default function Index () {
const inputRef = React.createRef()
const toFocus = () => {
console.log(inputRef)
inputRef.current.focus()
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={toFocus}>点击按钮,打印并聚焦输入框</button>
</>
)
}
效果和上面是一样的
④ 若将ref绑定在类组件上,是什么情况?
// 打印结果以注释形式显示
import { Component, createRef } from "react"
export default class Index extends Component {
indexRef = createRef(null)
// (2)点击按钮调用focus
focus = () => {
//(4)获取Child组件实例
const childInstance = this.indexRef.current
//(7)获取child组件input的DOM节点
console.log(childInstance.childRef.current)
//(8)聚焦input框
childInstance.childRef.current.focus()
//(9)清空input框内容
childInstance.childRef.current.value = ''
}
render () {
return (
<div>
{/* (3)组件绑定ref */}
<Child ref={this.indexRef} />
{/* (1)点击按钮后发生了什么? */}
<button onClick={this.focus}>聚焦Child组件输入框</button>
</div>
)
}
}
class Child extends Component {
// (5)在Child组件中创建ref
childRef = createRef(null)
render () {
//(6)ref绑定input框
return <input ref={this.childRef} />
}
}
效果:
⑤ 函数组件没有实例,但如果将ref绑定在函数组件上,可以吗?会是什么情况?
import { Component, createRef } from "react"
export default class Index extends Component {
indexRef = createRef(null)
focus = () => {
// (3)函数组件没有实例,那这个取到的是什么?
console.log('组件实例??', this.indexRef.current)
}
render () {
return (
<div>
// (1)函数组件绑定ref
<Child ref={this.indexRef} />
// (2)点击按钮触发focus
<button onClick={this.focus}>聚焦Child组件输入框</button>
</div>
)
}
}
function Child () {
const childRef = createRef(null)
return <input ref={childRef} type="text" />
}
取到的是报错:
翻译报错信息:
解决:
报错信息告诉我们,不能在函数组件上绑定ref,如果实在要绑定,也行,需要借助forwardRef的力量。
forwordRef
简单理解:forwordRef是用来做ref转发的,最终都是转发给某个DOM组件。
用法:它包裹着在函数组件绑定ref的组件,并接收两个参数,①props ②ref
import { Component, createRef, forwardRef } from "react"
export default class Index extends Component {
indexRef = createRef(null)
focus = () => {
const inputDOM = this.indexRef.current
console.log('Child中input框的DOM节点', inputDOM)
inputDOM.focus()
}
render () {
return (
<div>
<Child ref={this.indexRef} haha="哈哈">Child组件</Child>
<button onClick={this.focus}>聚焦Child组件输入框</button>
</div>
)
}
}
// 将被绑定ref的函数组件通过forwardRef包裹,通过接收ref实现ref转发
const Child = forwardRef(function (props, ref) {
console.log('props', props)
return <input ref={ref} type="text" />
})
打印结果:
效果:
Redux
Redux 理解
Redux官方文档
1. 英文文档: redux.js.org/
2. 中文文档: www.redux.org.cn/
- Github: github.com/reactjs/red…
Redux是什么?
- Redux是一个专门用于做
状态管理
的JS库(不是react插件库)。 - Redux除了和React一起用外,
还支持
其他界面库。可以用在React、Angular、Vue等项目中,但基本上是与React配合使用。 - 体小精悍(只有2KB,包括依赖)
- 作用:
集中式管理
React应用中多个组件共享
的状态
什么情况下需要使用Redux?
- 某个组件的状态,需要让其他组件可以随时拿到(共享)
如:
A、B、C、D都要用到E的某个状态,E只需将需要共享的状态存到Redux中去,需要用到该状态的去Redux中去取就可以了,这也是Redux的作用。
如:
A将状态存到Redux,B要用到A中存的状态,就去Redux中取
Redux原理图
总览:
redux提供一个管理state的仓库(store
),并且规定了store只能通过reducer
(函数)来更新,而reducer必须通过dispatch(action)
来触发,action
就是普通的JavaScript对象,它约定了执行的类型并且传递数据。使得state的变化
是可以预测的,同样的步骤会得到同样的state。
详细:
- redux是做集中式状态管理的,其中,store是扛把子,是redux的核心。
- store是管理state的仓库,使用它之前得先创建它。
import { legacy_createStore } from 'redux'
import reducer from '../reducers/reducer'
export default legacy_createStore(reducer)
- 创建store得传入reducer,
reducer
是什么?是个函数
。 - 作用:
reducer
接收到action
(type&data),根据type类型和data数据进行
对状态的处理并将
处理后的数据存入
store中。
- action是什么?
action
是个一般对象
- 目前创建store的方法由
createStore
改成legacy_createStore
,名字变更小问题,源码如下。
可以看到,创建store的代码是137-383行,来看一下创建完store后,里面有什么?
获取
存在redux仓库中状态的方法:store.getState()
更新
存在redux仓库中状态的方法:store.dispatch(action)
Redux用法
演示代码目录:
最终效果:

Count下的index.jsx
可能有的疑问:
- 父组件传入store,父组件是什么?
- store 从父组件传入 里面的逻辑是什么?
- action 里是怎么写的?
让我们看一下App.jsx
- 解决第一个疑问
再让我们看一下store.js
- 解决第二个疑问
使用redux必须要先安装依赖,npm i redux 或 yarn add redux
可能有的疑问: 4. reducer是什么?
让我们再看一下reducer
里写了什么 - 解决第四个疑问
让我们最后看一下action
里写了什么 - 解决第三个问题
捋一下使用redux的顺序:
1. 使用redux的目的是
多个组件共享状态;那么你要先有组件index.jsx
,才可以去使用redux
。
-
在组件中
使用redux
,无非两件事,①获取状态
②操作状态
-
两个API,①
store.getState()
②store.dispatch(action)
-
目前我们缺的:①
store
②action
-
在redux原理图中,可以看到,store站在C位,没有它,redux根本跑不起来。

2. index.jsx中缺少store,我们从App.jsx
中引入store传给需要的子组件。
- 有
两种方式引入
store,①组件自身引入
②从App.jsx中传入
,这里选择第二种,推荐写法。
3. 引入store,就需要创建store.js
-
安装
redux
,从redux
中引入legacy_createStore
并传入reducer
用于创建store
,并导出store -
当在
index.jsx组件中
调用dispatch API
并传入action
时,redux
会帮我们去通知reducer
进行状态加工。而在创建store
时传入reducer
的目的是:store与reducer从此建立起联系
。 -
问题来了?
传入reducer,reducer是什么?
4. 需要reducer,那么就要编写reducer.js
-
在组件调用
dispatch(action)
时,redux
会帮我们去调reducer
,通知reducer
干活。 -
为什么
’这个js文件‘就是reducer?,就因为文件名是reducer.js?不,因为
在创建store
时传入一个函数,这个函数被称为reducer
,它接收两个参数 ① 上一次的状态,没有就为undefined ② action对象。 -
当然可以在
store.js
中直接编写一个函数并传入legacy_createStore
中,但为了后期可维护性,我们选择引入的方式来创建reducer
。
5. 万事具备,只欠东风。缺少
传入dispatch
中的action
对象
-
哪里能用到action?答:
dispatch(action)
-
哪里能用到dispatch?答:
组件中,dispatch我理解为它是操作redux中状态的方法。
-
action是什么?答:
一般对象,{ type: '类型', payload: 值 }
-
为什么action仅仅是个Object对象,也要单写个文件,使用时将其引入呢?答:
后期好维护,并且一目了然,已经成为了一种规范。
-
在组件中操作状态时可以以dispatch({...})这种形式存在吗?答:
当然可以,但不推荐
。我的意思是将action用单独一个文件统一管理,再在组件需要action时按需引入会更好。最后将引入的action方法交给dispatch方法。当然,该action方法最终也是返回一个一般对象。
在redux的使用中,会发现当按上面编写完代码时,页面效果没有呈现出来
原因:
① redux不是Facebook公司出品,还有一定的小瑕疵,如:
组件通过dispatch更改redux中状态时,控制台打印该状态的值,确实已经更改,但没有引发页面的更新。由此推断,redux中状态的更改不会引起页面的重渲染(render)
。
解决:
在入口文件main.jsx
中,调用store
身上的subscribe
方法,该方法用于监听redux
中状态的改变,当状态改变,则执行回调,也就是手动调用了render
方法,重新渲染App组件
。
引起render的三种方法:
- 类式组件 - 只要调用了setState,不管状态是否改变,都会调render
- 函数式组件 - 通过useState解构出的setxxx改变状态,若该状态的值没发生更改,即使调了setxxx也不会重渲染,若状态发生改变,则调用render。(React通过Object.is来判断两个值是否相同,是浅比较)
- 父组件重新渲染会引起子组件的重渲染。
React Redux
React Redux 理解
简介
React
和Redux
是两个团体的作品。Facebook
公司发现,很多程序员写React
都喜欢用Redux
来做集中式状态管理,Facebook
本着简化编码的原则,直接官方出品 -React Redux
。Redux
与React Redux
,它们的关系是:使用react-redux
(插件库)可以让你更舒服的在React
项目中去使用redux
。
React Redux原理图
解释:
react-redux
将组件分为两种
。①容器组件
②UI组件
- 所有UI组件的外侧都要
包裹着
一个容器组件,他们是父子关系
。 - 在容器组件中
才能
和Redux打交道,可以随意使用
Redux的API。 - UI组件
通过
容器组件获取①redux里的状态
②操作状态的方法
。 - 容器组件
和
UI组件的通信通过props
React Redux用法
准确的说是 Redux + React Redux
代码目录结构:
代码效果:
代码实现:
先来看看Count
组件
需要解释的点:
- 能取到
Redux
中状态和
操作状态方法的是容器组件
而不是UI组件
!!! 声明
容器组件的方法
是:从react-redux
插件库中引入connect
。- 安装:
yarn add react-redux
- 声明容器组件:
connect(a,b)(UI组件)
- a和b都是函数,它们的
作用
是分别是①获取状态 ②操作状态 - react-redux底层经过处理,能使得
a函数接收到redux中的状态state
。 - 同样,
b函数能接收到dispatch方法
,用于操作状态。
- redux中的状态: state={sum, list}
- 操作状态的方法:dispatch(action)
要想使用Redux,必须要有store,那么,我们先把组件看完把,Person
组件
- nanoid - 每次获取都是全世界唯一的id
- 安装:yarn add nanoid
- 使用:const id = nanoid()
- 剩下的就是获取状态和操作状态
-
再次强调!connect()()
用于
创建容器组件 -
state => ({ key: value}) 经
react-redux
处理,接收到redux中的state
,返回一个对象
,该对象的key
用于UI组件
通过props
获取状态值
,而value
则是拿到存储在redux
中的状态值
。 -
在Count组件中,第二个参数是
function函数
,但是我们安装了react-redux
!!!React Redux官方出品能让我们在Redux的使用中
更加的舒服!第二个参数可以是
一个一般对象,里面是一组一组的key-value
。key
是用于UI组件
通过props
获取操作状态的方法
,而value
则是action
!!! -
解释:
UI组件中使用this.props.appendInfo({name,age,id:nanoid()})?
- react-redux会触发
dispatch方法
同时传入action,在这里传入的是addPerson
。addPerson是个函数,接收一个data并返回
一个一般对象(appendInfo括号里的参数最终是
传给addPerson这个函数的)
-
React Redux内部会帮我们去调
dispatch方法
,省去了
接收dispatch再dispatch(action) -
现在第二个参数
直接就
{ actionName1: action1, actioknName2: action2, ... }就可以了。
部分源码展示: 可以跳过,只是提供个查看的方向,不全~
先看ABCD,再看1234
|----------------------------------------------------------------|
|const mapStateToProps = initMapStateToProps(dispatch, options);|
|----------------------------------------------------------------|
initMapStateToProps = function initConstantSelector(dispatch)
最终initMapStateToProps(dispatch, options)返回了一个函数,是constantSelector
那么,mapStateToProps 就是 constantSelector 函数
调用constantSelector函数会返回一个对象,并且这个函数的原型上有个dependsOnOwnProps属性
这个对象由bindActionCreators(函数)返回
源码展示到此为止,太多,有兴趣的可以去看看。
================略过。。。看下面=================
(文件/代码编写顺序可以按自己习惯调整)需要操作Redux中的状态,就要dispatch(action),看下action.js
Count组件下的action
Person组件下的action
解释:
- UI组件
通过调用
容器组件定义的
操作状态的方法来改变状态
。 - 具体表现为:
this.props.xxx
(如果括号里的是参数,则该参数最终传给action处理函数) - 在容器组件中,
connect
第一个括号里的第二个参数{name: action对象或【action处理函数,处理完其实也就是一个action对象】}
,当你去调用它,this.props.name(),那么React Redux内部会去触发dispatch
方法。 - 而当调用了
dispatch(action)
,store
会通知reducer
进行状态加工,状态加工完后返回给store
,UI组件若是需要,则通过容器组件store.getState()
获取状态,UI组件再通过props
获取,问题来了,store
呢?reducer
呢?
先来看下reducer
Count下的reducer
Person下的reducer
再来看下站在C位的store,store.js
解释:
- 旧版用
createStore(reducer)
创建store
,而新版用legacy_createStore(reducer)
创建store
。 - 因为
Redux/React Redux + Redux
的使命就是集中式状态管理
,一个组件对应一个reducer
,当有多个组件时,reducer
的数量也随之增加,但是store-调度者
只有一个,所以当有多个reducer
时,就得去redux
中引入combineReducers
,并将这些reducer
集中起来传入legacy_createStore
中用于创建store
,当需要某个reducer时,store
就能通知'这个'reducer
进行状态加工。 - 而传入combineReducers的是一个对象,
该对象就是在Redux保存的总状态对象
。 - 对象中存放着一组一组的key-value,key代表着:
当我们需要去redux中取状态时应该通过哪个字段去取
;value则代表着:加工该状态(key)的reducer
。
具体使用:
5. import { legacy_createStore, combineReducers } from 'react-redux'可以吗?
react-redux帮我们做出的优化1:
解释:
- 在
纯Redux
中,我们需要在入口文件
中调用store.subscribe
来监测Redux
中状态的变化,当状态变化时,调用render方法
重新渲染页面; - 当我们安装了
react-redux
这个插件库并使用了
容器组件,容器组件connect()()
会帮我们监测Redux
中状态的改变,若Redux中
状态改变则重新渲染页面。图中×掉的部分就不用了。
react-redux帮我们做出的优化2:
解释:
1.在纯Redux中,我们需要在父组件App中将store传给子组件Count,因为Count要用到它(store.getState()、store.dispatch(action)),当组件个数逐渐变多时,难道我们也要一个一个的传入store吗?
react-redux帮我们做出了优化,从此引入index.js - 入口文件
解释:
- Provider:你整个应用里边但凡要用到
store的容器组件
,我都能给他传过去
,换句话说,哪个容器组件不需要store?
App里所有的容器组件都能收到store,只需要写一次即可。 - 容器组件
左边连着
UI组件,右边连着
Redux。在容器组件中才能
使用Redux相关的API,所以说这个store
对于容器组件来说是必要的
!
React Hooks
简介:
什么是Hook?
Hook
是React 16.8(版本)的新增特性
。它可以
让你在不编写class的情况下使用
state以及
其他的React特性。Hook
是一些可以让你在函数组件
里“钩入”React state
及生命周期
等特性的函数
。
Basic Hooks
- useState
- useEffect
- useContext
useState Hook
用法:
import { useState } from 'react'
const [state, setState] = useState(initState)
const [apple, setApple] = useState(initApple)
解释:
-
调用
useState
函数会返回一个state
,以及更新state的函数
。 -
在
初次渲染期间
,initState的值
是state的初始值
。 -
setState/setApple函数
用于
更新state
。它接受一个新的
state值并将
组件的一次重渲染
加入队列。
-
当调用setState/setApple时传入state值,
React会对新旧值进行浅比较
(Object.is(值1,值2)),若两个值相同,则不进行重渲染
。 -
对象浅比较
-> Object.is({a:2}, {a:2}) 结果为false
,比较的不是
对象的引用、该对象存储在内存中的地址,所以说setState
传入一个对象
,不管里面值是否相同
,都会重渲染
。
- 在
后续的重新渲染中
,useState返回的第一个值将始终是更新后最新的state
。
注意:
- useState
传入的参数
,也可以是函数
,该参数只在初次渲染期间有效
,后续渲染将被忽略
。作用是
:当初始化值需要通过计算得到,则可以在useState Hook中传入一个函数用于
计算初始值。如下:
import { useState } from 'react'
const [size, setSize] = useState(() => {
// 经过计算,为size赋的初始值
return undefined
})
代码:
效果:

useEffect Hook
简介:
Effect Hook
可以让你在函数组件中
执行副作用
操作。
问题:
- 什么是副作用?
① 数据获取
② 设置订阅
③ 手动更改React组件中的DOM
个人观点:
只要知道Effect Hook运行的时机, 剩下的就根据不同的业务场景去选择是否要用它。
提示:
- 如果你熟悉
React class
的生命周期函数
,你可以把useEffect Hook
看做componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个函数的组合。 - React会
保存
你传递的函数(我们称它为”effect
”)。
常见用法:
import { useEffect } from 'react'
1. useEffect(() => {})
2. useEffect(() => {}, [])
3. useEffect(() => {}, [依赖项1,依赖项2,...])
4. useEffect(() => {
return () => {}
}, [])
5. useEffect(() => {
return () => {}
}, [依赖项1,依赖项2,...])
第一种情况:useEffect(() => {})
代码:
效果:
解释:
useEffect(() => {
console.log('xxx')
})
这种情况下,Effect Hook
执行的时机是:
① 组件挂载/渲染完成之后
② 组件中只要有状态改变
③ 组件中传入的任意一个props的值改变
第二种情况:useEffect(() => {}, [])
代码:
效果:
解释:
useEffect(() => {
console.log('xxx')
}, [])
这种情况下,Effect Hook
执行的时机是:
① 组件挂载完成之后
第三种情况:useEffect(() => {}, [依赖项1,依赖项2,...])
代码:
效果:
解释:
useEffect(() => console.log('xxx'), [A])
这种情况下,Effect Hook
执行的时机是:
① 组件挂载完成之后
② 依赖项值发生变化
(A状态发生改变时)
第四种情况:
useEffect(() => {
return () => {}
}, [])
代码:
效果:
解释:
这种情况下,Effect Hook
执行的时机是:
① 组件挂载完成之后
② 在组件被卸载和销毁之前立即调用
第五种情况:useEffect(() => { return () => {} }, [依赖项])
代码:
index.js
- 入口文件
效果:
解释:
- 这种情况下,
Effect Hook
执行的时机是:
① 组件挂载完成之后
② 依赖项值更新
③ 组件将要卸载
- useEffect(effect),React会在执行当前effect之前对上一个effect进行清除。
useContext Hook
简介:
Context提供了
一个无需为
每层组件手动添加props
,就能在组件树间
进行数据传递
的方法。
问题:
- 何时使用Context?
Context 设计目的
是为了共享
那些对于一个组件树
而言是“全局
”的数据。换句话说,Context的出现避免了
子孙组件需要
爷爷组件的数据而从爷爷组件一层一层靠props
传递的问题。
个人观点:
- 用于组件通信
- 主要的应用场景:多个不同层级的组件需要访问相同的数据。
Context API
React.createContext
import React from 'react'
const MyContext = React.creactContext(defaultValue)
或
import { createContext } from 'react'
const MyContext = creactContext(defaultValue)
-
创建一个
Context对象
(创建上下文环境)。当React渲染一个订阅了这个Context对象的组件
(React渲染了一个使用useContext(Context对象)的组件),这个组件会从组件树中离自身最近且匹配的Provider中读取到当前的context值
。 -
只有当组件
(这个组件指的是要传递数据的组件<MyContext.Provider value={值}>组件(再嵌套组件)</MyContext.Provider>)所处的树中没有匹配到Provider时
(没有匹配到Context对象上的Provider组件),其defaultValue参数才会生效
。
Context.Provider
<MyContext.Provider value={/* 某个值 */}>
</MyContext.Provider>
-
每个Context对象(MyContext)都会返回一个Provider(React组件)
,具体如上。 -
消费组件:
只要被
这个Context对象上的Provider组件
包裹着,那么,这些被包裹的组件
或与这些被包裹组件能建立起联系的组件
(父子关系等)。->
都能以某种方式取到这个‘传递的值’。 -
Provider组件
接收一个value
属性,里面的值传递给消费组件
。 -
Provider组件
和消费组件
的关系是:一对多
。 -
Provider组件
可以嵌套使用
,里层的会覆盖
外层的数据。 -
当Provider的value值
发生变化
时,它内部的所有消费组件都会重新渲染
!
useContext
const value = useContext(MyContext)
-
useContext
接收一个context对象
(React.createContext的返回值)并返回该context
的当前值。当前context的值由
上层组件中距离当前组件最近的
<MyContext.Provider>的value属性
决定。换句话说
,创建Context对象
时都会有Provider组件
,当你的组件需要某个Provider组件
提供的value值
,那么一定是最近的优先
! -
当组件上层
最近的
<MyContext.Provider>更新
时,该Hook
(useContext)会触发重渲染
,并使用
最新传递给MyContext provider的contextvalue值
。换句话说
,组件的取值来自于
组件上层的MyContext provider的context value值
,如果上层的值发生改变
,是不是组件用于获取值的Hook(useContext)也该重新渲染
拿到最新的值? -
useContext的参数
必须是
context对象本身
代码1:
import { createContext, useContext } from 'react'
// 创建上下文环境,Context对象
const MyContext = createContext(null)
// 爷爷组件
function Father() {
return (
<>
// 注:Context对象上有个Provider组件,用于组件通信
<MyContext.Provider value={{name: '小明', age: 18}}>
<Son />
</MyContext.Provider>
</>
)
}
// 儿子组件
const Son = () => <GrandSon />
// 孙子组件
const GrandSon = () => {
// useContext用于获取上层组件中最近的Provider提供的value属性里的值
const { name, age } = useContext(MyContext)
return <p>{name + '同学'} - {age}</p>
}
效果1:

代码片段:
import { createContext } from 'react'
const MyContext = createContext(defaultValue)
怎么才能使用到默认值?
只有
当组件所处的树中没有匹配到
Provider时,其默认值defaultValue
才会生效。建议
将默认值defaultValue
和Provider组件上value属性的值
设置成同类型不同值。
代码2:
效果2:
Additional Hooks
- useReducer
- useCallback
- useMemo
- useImperativeHandle
- useRef
- useLayoutEffect
- useDebugValue(测试钩子,没用)
- useDeferredValue
- useTransition
- useId(用于服务端渲染,没啥用)
useReducer Hook
简介:
-
官方提供了
两种
状态管理的Hook,①useState
②useReducer
-
useReducer是useState的替代方案
。useReducer的工作流程几乎和Redux一致,但它不是Redux!
用法:
import { useReducer } from 'react'
const [state, dispatch] = useReducer(reducer, initState, initFunc)
-
useReducer函数接收三个参数,其中第三个是可选参数
-
第一个参数:reducer,
是个函数
,在dispatch方法中传入action动作对象时触发,用于处理状态
。 -
第二个参数:initState,
是初始状态
,传入useReducer中经过处理返回一个新的/当前的state
。 -
第三个参数:initFunc,
是个函数
,该函数能接受到初始值 - initState,并将处理过后的初始值返回,用于加工初始状态
import { useReducer } from 'react'
const countReducer = () => {}
export default function Index () {
const initCount = count => count + 2
// 如果useReducer有第三个参数,必须在其函数return一个值,否则没有初始值
const [count] = useReducer(countReducer, 0, initCount)
// 页面显示的是2
return <div>{count}</div>
}
下面,我们用代码来模拟一下useReducer的实现:
解释:
const [state, updateState] = useState(initState)
- 使用
state hook
,传入默认值
并返回一个数组
,数组第一个
是值,第二个
是改变值的方法。
const dispatch = action => updateState(reducer(state, action))
-
因为
dispatch
方法最终被useReducer
返回,所以一定要在useReducer中声明dispatch。 -
dispatch
方法传入action
动作对象才能通知reducer
去干活,去进行状态加工,所以dispatch
的参数是action对象
。 -
调用了dispatch方法就是 ①
通知reducer进行状态加工
②状态若发生改变则进行重渲染
。状态有无更新
都要调用state hook
返回的更新状态
的方法:updateState
,因为state hook返回的数组里的更新值的方法会对状态进行浅比较
(通过Object.is),若状态改变
则调render,若没有改变
则不会触发render。 -
不管最终有没有更新状态,updateState里都要有值
,不然它拿什么更新?值从哪里来,靠reducer
这个加工状态函数最终返回。 -
reducer接收两个参数①
上一次的状态
②action对象
,传入后经过reducer处理则返回一个加工后的状态
,updateState接收到
这个更新后的状态进行
浅比较,若两次值不等则就去更新它。 -
模拟useReducer结束
我们将模拟的useReducer
引入到文件去使用,看是否奏效:
效果:
问题:
1. useReducer和useState的区别?
- 个人理解:①
useState一次只能声明一个状态
②多个状态一起更新就推荐使用useReducer
③一个状态进行不同的操作,推荐使用useReducer
- 如果说某个state
独立于
其他的state,就没必要
放到Hook里去。
2. 为什么useReducer不是/不能取代Redux?
- 虽然
useReducer
及其reducer
是Redux
工作方式的一部分,但它不是
Redux。useReducer 函数
与它的reducer
紧密耦合,这也适用于
它的dispatch 函数
。我们只将
action 对象分派给该 reducer
。而在Redux
中,dispatch 函数将 action 对象发送到
store,store
将其分发给所有组合的 reducer 函数
。您可以将 Redux视为一个全局事件总线
,它接收任何事件(动作)并根据动作的有效负载
和先前的状态
将它们处理为新状态
。
性能优化(提前说明)
useCallback
->缓存函数
useMemo
->缓存值
React.memo
->缓存组件
useCallback Hook
简介:
import { useCallback } from 'react'
const Func = useCallback(fn, [依赖项,...])
- useCallback接收两个参数:①
需要被缓存的函数
②依赖项
(可选)
-
useCallback
用于缓存函数,如果useCallback没有
第二个参数,使用该hook就没有意义
,只要页面发生渲染,该函数还是会被重新生成
,没有达到缓存
的效果。 -
如果
useCallback
的第二个参数为空数组
,则说明它没有依赖项
,它在页面初次渲染时
就把该函数缓存
下来。之后这个函数也无法
再发生改变。 -
如果
useCallback
的第二个参数存在
且有依赖项
,那么,页面初次渲染时
会把这个函数缓存
下来,当依赖项的值发生变化
则重新生成函数
并将该函数重新缓存
到内存中去。
useCallback
通常与React.memo
一起使用。
import { memo } from 'react'
const List = memo(组件)
- List是被缓存的组件,当memo包裹的组件
所接收到的props
发生变化,memo会重新缓存
组件。
使用场景:
- 对于需要
传递函数给子组件
的场合,如果不使用useCallback
,只要父组件
发生重渲染,子组件
就会重新渲染。
- 即使子组件被
React.memo
包裹,当父组件
发生渲染,子组件
依然也会重新渲染,为什么呢?因为React.memo是通过浅比较
,比较props
的变化,当props
发生变化,则重新渲染
组件再将组件缓存
。因为父组件传给子组件的是函数
,所以当你没有用useCallback
将此函数缓存起来时,你通过某种方式
让父组件发生了渲染,那么父组件就会重新生成
这个函数,造成的后果是
props也发生了改变(重新生成function,引用、地址发生了改变),即子组件重新渲染
。但如果父组件传给子组件的是被缓存的值
,且子组件被React.memo
包裹,那么,只要该值没发生变化,父组件
无论怎么重渲染,子组件
都不会
发生渲染(变化)!
- 在调用节流、防抖函数时
案例:
在输入框中,用户停止输入300毫秒后开始请求接口获取数据,就得用到防抖函数bug出处:
父组件的每一次渲染都会重新生成一个函数解决:
useCallback
代码解释:
父组件给子组件传递函数,改变父组件的状态,观察子组件是否会被渲染
效果:
- 改变父组件的状态,引起了不必要的渲染 - ①
子组件重新渲染
②父组件的函数也重新生成
第一步:将子组件用React.memo包裹,将组件缓存
效果:
问题:
- 将子组件用React.memo缓存,但改变父组件的状态,子组件依然被更新,为什么?
因为React.memo会对父组件传来的props进行浅比较,当两次的值不同时,就会重新渲染、缓存组件。
- 传递给子组件的是函数,为什么两次的值会不同,好像没改到函数把!?
因为父组件发生渲染时,会重新生成函数,React.memo对props是浅比较,两次函数的props的引用不同,所以导致了即使被React.memo包裹也会重新渲染的问题。
- 如何解决?
导致这个问题的主要原因在于:父组件渲染时又重新生成了一个function,新生成的function和原来的function在内存中地址不同、引用不同。如果我们能把该函数缓存下来,且仅在依赖项改变时再去重新缓存函数,那么,当依赖项没有发生改变时,子组件就不会因为父组件的渲染而渲染。由此推出新的hook-useCallback。
第二步,将传递给子组件的函数缓存下来
效果:
小栗子:
输入框输入用户名称,点击按钮添加用户
优化前:
代码:
问题:
问题描述:输入框内容的变化会导致子组件重新渲染 ,这在性能方面是个极大的缺陷,如果子组件的体系庞大,当你键入或删除某个字符时,可能会发生延迟的现象。
问题原因:①父组件的重渲染会引起子组件的渲染 - setValue
② 父组件给子组件传递函数,新旧函数引用不同,对比不同,导致次次渲染
最终目的:点击添加用户按钮才触发子组件的渲染。
问题解决: ①React.memo
②useCallback
1. 缓存函数并添加依赖项,当函数中依赖项的值没有发生改变,则该函数就不会重新生成。
2. 缓存组件,当props没有改变,就不会触发子组件的渲染&重新缓存。
优化后:
useMemo Hook
简介:
从源码中可以看出,useMemo返回的是值,而useCallback返回的是函数。
- useMemo hook用于性能优化,功能是缓存值
- useMemo与useCallback可以互相转换(如下图),但不推荐,最好将这两个hook给区分开来。
用法:
import React from 'react'
const cacheValue = useMemo(() => 值,[依赖项,...])
useMemo Hook
根据第二个参数依赖项值的变化
来决定是否执行
第一个参数并缓存
第一个参数所返回的值,最终将缓存的值赋值给cacheValue。useMemo
和useCallback
都是用于性能优化
,但不能滥用,用法类似
,前者是
用于缓存值的,这个值可以是任意的。而后者是
缓存函数的。虽然可以转换,但还是希望各司其职
。- 若
useMemo
没有第二个参数,则使用useMemo
没有意义,页面渲染,该值还是会重新生成,没有达到缓存的效果
。 - 若
useMemo
第二个参数为空数组
,则代表没有依赖,页面首次渲染时将该值缓存,之后不再变化。 - 若
useMemo
第二个参数不为空
,有依赖项
,则当依赖项的值
改变时,React会重新执行
第一个参数并将返回的值进行缓存。 - 传入
useMemo
的函数会在渲染期间
执行。
使用场景:
一组账号密码对应着一个人的个人信息,默认将登录成功后的账号密码缓存,并将账号密码作为依赖项,当任一依赖项改变,切换个人信息。
useRef Hook
简介:
useRef一般是用于与DOM交互的,获取/操作(值)DOM。
用法:
import { useRef } from 'react'
const xxxRef = useRef(initValue)
// 伪代码
<input ref={xxxRef} />
- useRef返回一个可变的ref对象,该对象只有一个current属性,current属性的初始值为传入的参数initValue。
如:
打印结果:
- 更新ref对象上current属性的值不会触发重渲染(re-render)
- 返回的ref对象在组件的整个生命周期中保持不变
代码:(使用ref.current存值)
import { useState, useRef, useCallback } from "react"
export default function Index () {
const [time, setTime] = useState(Date.now())
const ref = useRef()
const start = useCallback(() => {
console.log('开始了...')
// 存储setInterval返回的ID
ref.current = setInterval(() => {
setTime(Date.now())
}, 500)
}, [])
function clear () {
console.log('结束了...')
clearInterval(ref.current)
}
return (
<>
<span>time: {time}</span>
<button onClick={start}>开始</button>
<button onClick={clear}>清除定时器</button>
</>
)
}
效果:
代码:(使用ref.current存DOM节点)
import { useRef } from "react"
export default function Index () {
// 初始化一个空引用
const inputRef = useRef(null)
function toFocus () {
console.log('inputRef', inputRef)
console.log('DOM节点', inputRef.current)
console.log('input值', inputRef.current.value)
inputRef.current.focus()
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={toFocus}>打印并聚焦</button>
</>
)
}
效果:
useImperativeHandle Hook
简介:
useImperativeHandle Hook
接收3个参数,第一个是
ref(该ref用于确认与哪个组件建立起联系,ref由父组件传入),第二个是
个回调函数,最终返回一个值,这个值是任意的,返回的值给‘提供ref
的这个组件使用’,第三个是
依赖项。
useImperativeHandle(ref, () => {
return ...
}, [])
- 依赖项改变,将会重新
执行/调用
第二个参数(回调函数)。依赖项改变的结果是
:提供给父组件使用的值可能会发生改变。
- 使用
useImperativeHandle Hook
需要绑定ref,而ref只能从父级传入,所以该Hook需要与useRef Hook
一起使用,当使用useRef创建ref并绑定在函数式组件上时,由于函数组件没有实例,当在父组件获取ref上的current属性时,控制台会报错,而报错信息推荐我们使用forwordRef来解决
函数组件没有实例不可绑定ref的问题。所以说当我们去使用useImperativeHandle Hook时要与useRef、forwordRef配合使用。
- React.forwordRef
用于将父组件创建的ref引用关联到子组件中的任意元素上
,也可以理解为子组件向父组件暴露DOM引用。
两个案例:
案例1:
效果图:
代码实现:
import { useRef, forwardRef, useImperativeHandle } from 'react'
export default function Index () {
const inputRef = useRef()
/*
1. inputRef.current的值为useImperativeHandle第二个参数返回的值
2. inputRef.current.userRef取到的也是ref
3. 需要再取该ref的current属性才能取到该DOM节点
*/
return (
<>
<div>
<button onClick={() => {
inputRef.current.userRef.current.focus()
}}>聚焦输入框</button>
<button onClick={() => {
inputRef.current.passwordFocus()
}}>聚焦密码</button>
</div>
// ref绑定在函数组件上
<Input ref={inputRef} />
</>
)
}
// 函数组件没有实例,借助forwordRef转发ref解决问题
// props为父组件传入的除ref以外的值,ref为父级传入
const Input = forwardRef((props, ref) => {
// 创建ref,分别绑定到用户名、密码输入框
const userRef = useRef()
const passwordRef = useRef()
useImperativeHandle(ref, () => ({
// 向提供‘ref’的组件提供值,该值是个对象
userRef,
passwordFocus: () => {
passwordRef.current.focus()
}
}), [])
return (
<>
<input ref={userRef} type="text" placeholder='用户名' />
<input ref={passwordRef} type="text" placeholder='密码' />
</>
)
})
案例2:
效果图:
代码实现:
import { useState,
useRef,
forwardRef,
useImperativeHandle
} from 'react'
// 以两种形式声明样式
import style from './style'
import './style.css'
// 弹窗组件 - 子组件
// forwordRef接收的第一个参数为props,是个对象,直接解构
// props解构出的children:<Dialog>这里的内容为children</Dialog>
const Dialog = forwardRef(({ children }, ref) => {
// 此处声明ref的目的是,获取DOM的位置&聚焦
const xRef = useRef()
const qxRef = useRef()
const qdRef = useRef()
useImperativeHandle(ref, () => {
return {
// 给父组件提供一个数组
tips: [
{
// 索引0,第一个,获取关闭按钮的位置
position: xRef.current.getBoundingClientRect(),
// 聚焦关闭按钮
action: () => xRef.current.focus(),
// 根据关闭按钮的位置来显示文字
context: '点击这里关闭弹窗'
},
{
position: qxRef.current.getBoundingClientRect(),
action: () => qxRef.current.focus(),
context: '点击这里 取消操作并关闭弹窗'
},
{
position: qdRef.current.getBoundingClientRect(),
action: () => qdRef.current.focus(),
context: '点击这里 确认操作并关闭弹窗'
}
]
}
}, [])
const xClick = () => {
console.log(xRef.current.getBoundingClientRect())
}
return (
<div style={style.container}>
<div style={style.first}>
<div>如何使用</div>
<button ref={xRef} className="xbtn" onClick={xClick}>X</button>
</div>
{children}
<div>
<button ref={qxRef} className="qxbtn">取消</button>
<button ref={qdRef} className="qdbtn">确定</button>
</div>
</div>
)
})
// 父组件
export default function StartStudy () {
const dialogRef = useRef()
/*
根据step来判断到底显示什么
step = -1,显示开始学习按钮,此时什么操作都没有
step = 0,取子组件数组第一个的内容
以此类推
*/
const [step, setStep] = useState(-1)
const study = () => {
// 点击开始学习按钮,(-1+1=0)聚焦关闭按钮,
dialogRef.current.tips[step+1].action()
// step设置为0
setStep(step + 1)
}
return (
<Dialog ref={dialogRef}>
{
// step = -1 显示开始学习按钮,显示在Dialog组件的children处
step < 0 &&
<button style={style.second} onClick={study}>开始学习</button>
}
{
// step = 0/1/2 分别操作 关闭/取消/确定按钮
step >= 0 &&
<div style={{
position: 'absolute',
top: `${dialogRef.current.tips[step].position.top - 22}px`,
left: `${dialogRef.current.tips[step].position.left - 70}px`,
padding: '2px',
backgroundColor: '#ffeb3b',
}}>
{dialogRef.current.tips[step].context}
<button
style={{ marginLeft: '8px' }}
onClick={() => {
// 点击‘知道了’判断step数值,如果大于数组长度-1,将值设为-1
// step = -1,显示 开始学习按钮
if (step > dialogRef.current.tips.lenght - 1) {
setStep(-1)
} else {
// step的值符合要求就进入下一步
setStep(prveStep => {
console.log('prveStep', prveStep)
if (prveStep >= 2) {
return -1
}
dialogRef.current.tips[prveStep + 1].action()
return prveStep + 1
})
}
}}
>
知道了
</button>
</div>
}
</Dialog>
)
}
// style.js
const style = {
container: {
margin: '200px auto',
padding: '20px',
width: '400px',
border: '1px solid'
},
first: {
display: 'flex',
justifyContent: 'space-between',
lineHeight: '30px',
marginBottom: '30px'
},
second: {
marginBottom: '25px',
width: '100%'
}
}
export default style
// style.css
.xbtn {
width: 100px;
}
.qxbtn {
margin-right: 8px;
width: 78px;
}
.qdbtn {
width: 78px;
}
.xbtn:focus,
.qxbtn:focus,
.qdbtn:focus{
border: 2px solid greenyellow;
background-color: deeppink;
}
useLayoutEffect Hook
简介:
useLayout Hook
和useEffect Hook
用法一致,只是触发时机
不同。且useEffect Hook是异步的,而useLayout Hook是同步执行的。
import { useEffect, useLayoutEffect } from "react"
export default function Index () {
console.log(1)
useEffect(() => {
console.log('useEffect')
}, [])
console.log(2)
useLayoutEffect(() => {
console.log('useLayoutEffect')
}, [])
console.log(3)
}
打印结果:
问题:
- useLayoutEffect Hook不是同步的吗?打印结果不应该是【1、2、useLayoutEffect、3、Effect】?
-
这里说的
useLayoutEffect Hook同步
指的是‘‘React会在所有的 DOM 变更之后同步调用
effect’’。DOM变更之后,浏览器还需要绘制然后是渲染才会呈现在页面上
。所以说当useLayoutEffect Hook的effect callback执行时DOM是还没渲染到页面上的,执行时机可以类比于componentDidMount
。 -
componentDidMount也是会在浏览器
更新屏幕之前
触发,即使render函数被调用多次(意味着改变了状态),用户也看不到中间状态。
- 什么意思?
- 不管useLayoutEffect还是useEffect,它们的执行时机都是在
render之后
的。函数的render方法在哪里?
解释:
- render函数在React中有两种形式
①在类组件中指的是render方法
class Index extends React.Component {
render() {
return <h1>2022-10-18晚</h1>
}
}
②在函数组件中render函数指的是函数组件本体
function Index() {
// 里面全是在render方法的范畴,某些Hook有自己特定的执行时机
return <h1>2022-10-18晚</h1>
}
也就是说:
-
是先执行了render方法才执行的useLayoutEffect/useEffect,这也就解释了为什么先打印出1、2、3了。
-
在
render
过程中,React将新调用的render
函数返回的树与旧版本的树进行比较,这一步是决定如何更新DOM
的必要步骤,然后进行diff
比较,更新DOM
树。而React Hooks
的存在是为了操作这些虚拟
DOM,最终生成真实
DOM,浏览器再绘制渲染到页面的这么一个过程。
执行时机:
useEffect
:DOM节点已加载完毕,浏览器绘制(渲染)完
后延迟执行effect callbackuseLayoutEffect
:DOM节点已加载完毕,浏览器绘制前
执行effect callback
假设:
- 通过某个事件更改了某个状态(state)
- React更新这个状态(state)
- React处理组件中return出来的DOM节点(diff算法)
- 浏览器绘制更新之后的DOM
- 渲染完成,将真实DOM展示在页面上
①:useLayoutEffect Hook是在第三点结束,第四点即将要开始时同步执行。同步意味着可能会阻塞后续的步骤
。
②:useEffect Hook是在第五点结束后异步执行
,浏览器什么时候闲了什么时候执行。
用法:(与useEffect用法一致,区别仅在触发时机不同)
import { useState, useLayoutEffect } from "react"
// export const root = ReactDOM.createRoot(document.getElementById('root'))
import { root } from '../index'
export default function Index () {
const [count, setCount] = useState(0)
useLayoutEffect(() => {
let timer
timer = setInterval(() => setCount(count + 1), 1500)
console.log('componentDidMount&componentDidUpdate & 依赖更新')
return function clearTimer () {
console.log('componentWillUnmount')
clearInterval(timer)
}
}, [count])
// 组件销毁时调用
const unmount = () => {
root.unmount()
}
return (
<>
<h1>useLayoutEffect</h1>
<h2>count: {count}</h2>
<button onClick={unmount}>销毁组件</button>
</>
)
}
效果:
总是在下一次更新前清除上一次的副作用(effect)
总结:
useLayout Hook
与useEffect Hook
用法一致,区别在调用时机。
useDeferredValue Hook
简介:
useDeferredValue
Hook用于降低渲染优先级
,从而腾出CPU资源来渲染优先级更高的更新,最终目的是提高用户体验。
用法:
import { useDeferredValue } from 'react'
const deferredValue = useDeferredValue(value)
- 这个Hook接收一个值并返回一个副本,该副本会被
延迟渲染
。 - 延迟渲染的意思是:其他的渲染优先级
都高于
该副本的渲染优先级,该副本将等待至
仅剩本身未渲染时开始渲染。
应用场景
- 数据量很大,导致页面卡顿,可以考虑使用
useDeferredValue
这个Hook。
需求:
有10万条数据,根据在输入框中的输入筛选出有关数据
代码演示:
下面会用两种方式进行演示:①没有用这个hook
②使用了该hook
第一种:
import { useState, useEffect } from "react"
import { nanoid } from 'nanoid'
import './useDeferredValue.css'
export default function Index () {
const [value, setValue] = useState('')
const [initData, setInit] = useState([])
const [filterData, setFilter] = useState([])
// 键盘输入
const inputChange = (e) => {
console.log('输入框:', e.target.value)
setValue(e.target.value)
}
// 根据键盘输入,筛选出相关数据
useEffect(() => {
initData && setFilter(initData.filter(item => {
return !!item['display'].includes(value)
}))
}, [value])
// 初始化,准备10万条数据
useEffect(() => {
const arr = []
for (let i = 0; i < 100000; i++) {
arr.push({ id: nanoid(), display: '数据' + i })
}
setInit(arr)
}, [])
return (
<div className="container">
<span>搜索:</span>
<input type="text" onChange={inputChange} />
<List data={filterData} />
</div>
)
}
function List ({ data = [] }) {
return (
<ul className="ul">
{
data &&
data.map(item => <li key={item.id}>{item.display}</li>)
}
</ul>
)
}
// useDeferredValue.css
.container {
margin: 0 auto;
width: 300px;
}
.ul {
list-style: none;
}
.ul > li {
margin-bottom: 10px;
padding: 5px;
text-align: center;
background-color: aqua;
}
代码效果:
解释&问题:
-
React18提出了一个概念 - “
concurrency
”,翻译成中文是并发性
,有点像同步、异步的意思。 -
可以这么理解:React将处理通道由1个变成2个,一个是
快速通道
,而另一个是慢速通道
。 -
默认都是在快速通道,大家都同时渲染
,就算A已经准备就绪,但还是要等待B处理完成,然后A、B同时渲染到页面上。这样的后果是:当数据量庞大时,A已经好了,B没好,等一段时间B好了,A、B同时出现在页面上,用户体验感极差。 -
上面的案例就是,键盘输入已经完成,但数据还未筛选出,渲染键盘输入的字符要等到筛选出数据之后才能渲染,导致卡顿,给人的感觉极差。
由此引出,useDeferredValue Hook
第二种:
先看效果:
使用了该Hook后,不管是从速度上还是视觉上,都有了极大的提升!
代码:
import { useState, useEffect, useDeferredValue } from "react"
import { nanoid } from 'nanoid'
// 样式同上
import './useDeferredValue.css'
export default function Index () {
const [value, setValue] = useState('')
const [initData, setInit] = useState([])
const [filterData, setFilter] = useState([])
// 键盘输入
const inputChange = (e) => {
console.log('输入框:', e.target.value)
setValue(e.target.value)
}
// 根据键盘输入,筛选出相关数据
useEffect(() => {
initData && setFilter(initData.filter(item => {
return !!item['display'].includes(value)
}))
}, [value])
// 初始化,准备10万条数据
useEffect(() => {
const arr = []
for (let i = 0; i < 100000; i++) {
arr.push({ id: nanoid(), display: '数据' + i })
}
setInit(arr)
}, [])
// 关键:该副本(passedValue)的渲染将会滞后!
const passedValue = useDeferredValue(filterData)
return (
<div className="container">
<span>搜索:</span>
<input type="text" onChange={inputChange} />
{/*副本传递给List组件*/}
<List data={passedValue} />
</div>
)
}
function List ({ data = [] }) {
return (
<ul className="ul">
{
data &&
data.map(item => <li key={item.id}>{item.display}</li>)
}
</ul>
)
}
解释:
-
将一个value值传入useDeferredValue这个Hook,它会返回一个值(
副本
),React会将该副本放入慢速通道
,在慢速通道的渲染都会滞后! -
慢速通道的
渲染时机
是在快速通道都已渲染完毕后
。 -
意思是:当A已处理完成但B因为某种原因处理时间较长时,可将B的值传给该Hook,使用其返回值
(副本)来进行渲染操作(虚拟DOM节点的生成)
,这样就不会因为B的处理时间长而影响了A的渲染。有点异步
的意思。 -
上述案例就是利用React18提出的“
并发性
”来解决数据量庞大,耗时长的问题。
useTransition Hook
简介:
useTransition
与useDeferredValue
作用相同,都是将‘渲染
’加入到慢速通道,延迟渲染。但它们用法不同,也有区别,之后会介绍。
import { useTransition } from 'react'
const [isPending, startTransition] = useTransition()
参数介绍:
- startTransition: 接收一个回调函数,在回调函数中设置更新,可将更新加入到慢速通道,延迟由该状态导致的UI渲染,换句话说,
startTransition可以防止立即执行昂贵的UI渲染
。
import {useState, useTransition} from 'react'
const [isPending, start] = useTransition()
const [list, setList] = useState('')
start(() => {
// 可以将昂贵的UI渲染包装在此函数中。
setList(列表)
})
为什么需要这个功能存在?
-
请记住,
强制昂贵的UI渲染立即完成会阻止更轻、更紧急的UI渲染及时渲染
。React默认是等待所有更新都完成再同时渲染,startTransition
的出现避免了这一现象,使用它时,轻量级、紧急的渲染将不会被阻塞! -
startTransition
允许你将应用程序中的某些更新标记为非紧急
,因此它们会暂停,同时优先考虑更紧急的更新
。这使您的应用程序感觉更快,因此,无论你在渲染什么,你的应用程序仍在响应用户的操作。
如果你想在等待昂贵的UI渲染完成时显示一些内容怎么办?你可能显示一个进度条以向用户提供即时反馈,以便用户知道应用程序正在处理他们的请求。
为此,我们可以使用isPending
来自useTransition
Hook(钩子)的变量。
- isPending: 值为true/false,翻译为中文:
是否在等待状态
。当isPending为真时,说明目前处在等待状态
,说明startTransition中的回调函数还未执行完成
,我们可以利用isPending为True的这一特性,增加一行提示,告知用户,如:
{
isPending && <div>正在输入中...</div>
}
isPending为false时,说明在慢速通道的UI渲染已全部完成。
需求:
跟useDeferredValue这个Hook的需求一样,都是根据输入框键入内容从10万条数据中筛选出相关数据。
如果你没有利用React18新提出的‘并发
’特性,一旦数据量大,处理速度就慢,一些轻量级的渲染将要等待‘这数据量大的处理(根据键盘键入从十万条数据中筛选出相关数据)’完之后,同时渲染
在页面上,页面将会出现卡顿、延迟的现象,用户体验极感极差。
直接来看使用useTransition
钩子函数后的效果???
效果:
代码:
import { useState, useEffect, useTransition } from "react"
import { nanoid } from 'nanoid'
import './useDeferredValue.css'
export default function Index () {
const [value, setValue] = useState('')
const [initData, setInit] = useState([])
const [filterData, setFilter] = useState([])
const [ispending, startTransition] = useTransition()
// 键盘输入
const inputChange = (e) => {
console.log('输入框:', e.target.value)
setValue(e.target.value)
}
// 根据键盘输入,筛选出相关数据
useEffect(() => {
// 看这!!!
initData && startTransition(() => {
// 滞后setFilter导致的UI渲染!
setFilter(initData.filter(item => {
return !!item['display'].includes(value)
}))
})
}, [value])
// 初始化,准备10万条数据
useEffect(() => {
const arr = []
for (let i = 0; i < 100000; i++) {
arr.push({ id: nanoid(), display: '数据' + i })
}
setInit(arr)
}, [])
return (
<div className="container">
<span>搜索:</span>
<input type="text" onChange={inputChange} />
{ ispending && <div>输入中...</div>}
<List data={filterData} />
</div>
)
}
function List ({ data = [] }) {
return (
<ul className="ul">
{
data &&
data.map(item => <li key={item.id}>{item.display}</li>)
}
</ul>
)
}
useDeferredValue&useTransition
问题:
- useDeferredValue和useTransition功能都一样,为什么React要创建这两个功能一样的Hook?
-
useTransition是用来处理更新函数的
,而useDeferredValue
是用来处理
更新函数执行后所更新的数据本身
的。有些情况下,你并不能直接获得更新函数,比如你用的是第三方的hooks库,在使用的时候更新函数并不能直接对外暴露,这个时候你只能去优化数据,从而你只能使用useDeferredValue这个Hook。 -
useTransition的好处是它可以
一次性处理好几个更新函数
。
注意事项:
-
对于同一个资源的优化,这两个Hook提供的优化效果是一样的,因此不需要同时使用;一旦同时使用,将会带来不必要的性能损耗
-
建议只有数据量大的时候才去考虑这两个Hook。
-
优先使用useTransition Hook,因为isPending变量能给用户带来不一样的视觉效果。
结语
- 坚持的本质是屏蔽失望,愿大家所愿即所想。工资发了想干什么就去干,
存钱,不存在的,人生就是要潇潇洒洒~~ JDG加油!

- 感谢我滴导师,wuwuwu~~ 大佬大佬大佬