组件基本介绍
我们从上面可以清楚地看到,组件本质上就是类和函数,但是与常规的类和函数不同的是,组件承载了渲染视图的 UI 和更新视图的 setState 、 useState 等方法。React 在底层逻辑上会像正常实例化类和正常执行函数那样处理的组件。
因此,函数与类上的特性在 React 组件上同样具有,比如原型链,继承,静态属性等,所以不要把 React 组件和类与函数独立开来。
接下来,我们一起着重看一下 React 对组件的处理流程。
对于类组件的执行,是在react-reconciler/src/ReactFiberClassComponent.js中:
对于函数组件的执行,是在react-reconciler/src/ReactFiberHooks.js中
从中,找到了执行类组件和函数组件的函数。那么为了搞清楚 React 底层是如何处理组件的,首先来看一下类和函数组件是什么时候被实例化和执行的?
在 React 调和渲染 fiber 节点的时候,如果发现 fiber tag 是 ClassComponent = 1,则按照类组件逻辑处理,如果是 FunctionComponent = 0 则按照函数组件逻辑处理。当然 React 也提供了一些内置的组件,比如说 Suspense 、Profiler 等。
什么是组件化开发
- 组件化是一种分而治之的思想
- 组件特点:可复用,独立,可组合
- 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
- 任何的应用都会被抽象成一颗组件树
React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:
- 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
- 函数组件又叫做无状态组件 函数组件是不能自己提供数据【前提:基于hooks之前说的】
- 类组件又叫做有状态组件 类组件可以自己提供数据,组件内部的状态(数据如果发生了改变,内容会自动的更新)数据驱动视图
- 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);
- 展示型组件就是我给你一些数据你给我渲染出来即可,无需过多的操作;容器型组件就是一般情况下我们包含的东西是非常多的,比如维护自己的状态、发送网络请求、监听一些全局的事件等等
- 函数组件、无状态组件、展示型组件主要关注UI的展示
- 类组件、有状态组件、容器型组件主要关注数据逻辑
使用模块化开发
在开发过程中,一些头部或者底部等等共用的部分需要进行复用,在vue或者是react中可以将这样的复用的部分封装进一个组件中,然后将这些组件组合起来就形成了一个网页。这样可以减少代码量,达到代码的复用性,也方便维护
什么是模块化
在早期的js中,没有模块化的概念,多人协作开发,可能会有变量名冲突问题,可以使用插件达到模块化效果,发展到es6时,出现了js自带的模块化export和import, 在node中就是
require和
module
注意:
- 定义react组件时,建议首字母大写
- 使用组件时,首字母也要大写,并且用驼峰,不要用横杠
React创建组件的两种方式
类与继承
class 基本语法
- 在 ES6 之前通过构造函数创建对象
- 在 ES6 中新增了一个关键字 class, 类 和构造函数类似,用于创建对象
-
- 类与对象的区别
-
- 类:指的是一类的事物,是个概念,比如车 手机 水杯等
-
- 对象:一个具体的事物,有具体的特征和行为,比如一个手机,我的手机等, 类可以创建出来对象。
- 类创建对象的基本语法
-
- 基本语法
class 类名{}
- 基本语法
-
- 构造函数
constructor
的用法,创建对象
- 构造函数
-
- 在类中提供方法,直接提供即可
-
- 在类中不需要使用,分隔
extends 实现继承
- extends 基本使用
- 类可以使用它继承的类中所有的成员(属性和方法)
- 类中可以提供自己的属性和方法
- 注意:如果想要给类中新增属性,必须先调用 super 方法
类组件
类组件:使用ES6的class语法创建组件
- 约定1:类组件的名称必须是大写字母开头
- 约定2:类组件应该继承
React.Component
父类,从而可以使用父类中提供的方法或者属性 - 约定3:类组件必须提供
render
方法 - 约定4:render方法
必须有返回值
,表示该组件的结构
基本使用
在ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义
使用class定义一个组件:
- constructor是可选的,我们通常在constructor中初始化一些数据
- this.state中维护的就是我们组件内部的数据
- render() 方法是 class 组件中唯一必须实现的方法
定义组件:
import React from 'react'
class Hello extends React.Component {
render() {
return <div>这是一个类组件</div>
}
}
// 或者
import React, { Component } from 'react'
class Hello extends Component {
render() {
return <div>这是一个类组件</div>
}
}
使用组件:
ReactDOM.render(<Hello />, document.getElementById('root'))
当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
React 元素:
- 通常通过 JSX 创建的都叫做react元素
- 例如,
- 无论是
数组或 fragments:使得 render 方法可以返回多个元素
Portals:可以渲染子节点到不同的 DOM 子树中
字符串或数值类型:它们在 DOM 中会被渲染为文本节点
布尔类型或 null:什么都不渲染
组件化
思考:项目中的组件多了之后,该如何组织这些组件呢?
- 选择一:将所有组件放在同一个JS文件中
- 选择二:将每个组件放到单独的JS文件中--推荐
- 组件作为一个独立的个体,一般都会放到一个单独的 JS 文件中
components/Password.jsx
import React, { Component } from 'react';
class Password extends Component {
render() {
return (
<>
密码:<input placeholder="请输入密码" />
</>
)
}
}
export default Password;
- 类组件需要继承自
Component
, Component
:是类组件的父类,所有的类组件都需要继承自它才能进行开发。render
:渲染DOM元素的方法,必须return返回一个DOM元素
App.jsx中引入使用
import Account from "./components/Account";
import Password from "./components/Password";
function App() {
return (
<div>
<Account />
<br />
<Password></Password>
</div>
);
}
export default App;
注意:react中两种空标签有区别
<></>
:不能在标签上添加属性
<React.Fragment></React.Fragment>
:可以在标签上添加属性
有状态/无状态组件
- 函数组件又叫做无状态组件 函数组件是不能自己提供数据【前提:基于hooks之前说的】
- 类组件又叫做有状态组件 类组件可以自己提供数据,组件内部的状态(数据如果发生了改变,内容会自动的更新)数据驱动视图
- 状态(state)即组件的私有数据,当组件的状态发生了改变,页面结构也就发生了改变。
- 函数组件是没有状态的,只负责页面的展示(静态,不会发生变化)性能比较高
- 类组件有自己的状态,负责更新UI,只要类组件的数据发生了改变,UI就会发生更新。
- 在复杂的项目中,一般都是由函数组件和类组件共同配合来完成的。【增加了使用者的负担,所以后来有了hooks】
比如计数器案例,点击按钮让数值+1, 0和1就是不同时刻的状态,当状态从0变成1之后,UI也要跟着发生变化。React想要实现这种功能,就需要使用有状态组件来完成。
类组件的状态
- 状态
state
即数据,是组件内部的私有数据
,只有在组件内部可以使用
state的值是一个对象
,表示一个组件中可以有多个数据
- state的基本使用
class Hello extends React.Component {
constructor() {
super()
// 组件通过state提供数据
this.state = {
msg: 'hello react'
}
}
render() {
return <div>state中的数据--{this.state.msg}</div>
}
}
- 简洁的语法
class Hello extends React.Component {
state = {
msg: 'hello react'
}
render() {
return <div>state中的数据--{this.state.msg}</div>
}
}
深入剖析
在 class 组件中,除了继承 React.Component ,底层还加入了 updater 对象,组件中调用的 setState 和 forceUpdate 本质上是调用了 updater 对象上的 enqueueSetState 和 enqueueForceUpdate 方法。
那么,React 底层是如何定义类组件的呢?
react/src/ReactBaseClasses.js
如上可以看出 Component 底层 React 的处理逻辑是,类组件执行构造函数过程中会在实例上绑定 props 和 context ,初始化置空 refs 属性,原型链上绑定setState、forceUpdate 方法。对于 updater,React 在实例化类组件之后会单独绑定 update 对象。
如果没有在 constructor 的 super 函数中传递 props,那么接下来 constructor 执行上下文中就获取不到 props ,这是为什么呢?
// 假设我们在 constructor 中这么写
constructor() {
super()
console.log(this.props) // 打印 undefiend 为什么?
}
答案很简单,刚才的 Component 源码已经说得明明白白了,绑定 props 是在父类 Component 构造函数中,执行 super 等于执行 Component 函数,此时 props 没有作为第一个参数传给 super() ,在 Component 中就会找不到 props 参数,从而变成 undefined ,在接下来 constructor 代码中打印 props 为 undefined 。
// 解决问题
constructor() {
super(props)
console.log(this.props)
}
为了更好地使用 React 类组件,我们首先看一下类组件各个部分的功能:
上述绑定了两个 handleClick ,那么点击 div 之后会打印什么呢?
- 结果是 111 。因为在 class 类内部,箭头函数是直接绑定在实例对象上的,而第二个 handleClick 是绑定在 prototype 原型链上的,它们的优先级是:实例对象上方法属性 > 原型链对象上方法属性。
函数组件
函数组件:使用JS的函数或者箭头函数创建的组件
- 通过 function 来进行定义的
- 为了区分和普通标签的差异,函数组件的名称必须
大写字母开头
- 函数组件
必须有返回值
,表示该组件的结构 - 如果返回值为null,表示不渲染任何内容
基本使用
使用函数创建组件
function Hello () {
return (
<div>这是我的函数组件</div>
)
}
使用箭头函数创建组件
const Hello = () => <div>这是一个函数组件</div>
使用组件
ReactDOM.render(<Hello />, document.getElementById('root'))
特点(基于hooks之前):
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数
- this关键字不能指向组件实例(因为没有组件实例)(不存在this)
- 没有内部状态(state)
组件化
components/Account.jsx
import Account from "./components/Account";
function App() {
return (
<div>
<Account />
<br />
密码:<input placeholder="请输入密码" />
</div>
);
}
export default App;
App.jsx中使用
import Account from "./components/Account";
function App() {
return (
<div>
<Account />
<br />
密码:<input placeholder="请输入密码" />
</div>
);
}
export default App;
一个函数组件里导出多个函数:
components/Head.jsx
export function Demo(){
return (
<div>123</div>
)
}
export function Demo1() {
return (
<div>789</div>
)
}
函数组件与类组件的区别
对于类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明。
为了能让函数组件可以保存一些状态,执行一些副作用钩子,React Hooks 应运而生,它可以帮助记录 React 中组件的状态,处理一些额外的副作用。
编写形式
两者最明显的区别在于编写形式的不同,同一种功能的实现可以分别对应类组件和函数组件的编写形式
状态管理
在 hooks 出来之前,函数组件就是无状态组件,不能保管组件的状态,不像类组件中调用 setState
如果想要管理 state 状态,可以使用 useState,如下:
在使用 hooks 情况下,一般如果函数组件调用 state,则需要创建一个类组件或者 state 提升到你的父组件中,然后通过 props对象传递到子组件
生命周期
在函数组件中,并不存在生命周期,这是因为这些生命周期钩子都来自于继承的 React.Component 所以,如果用到生命周期,就只能使用类组件
但是函数组件使用 useEffect 也能够完成替代生命周期的作用,这里给出一个简单的例子:
上述简单的例子对应类组件中的 componentDidMount 生命周期
如果在 useEffect 回调函数中 return 一个函数,则 return 函数会在组件卸载的时候执行,正如 componentwillUnmount
调用方式
如果是一个函数组件,调用则是执行函数即可:
如果是一个类组件,则需要将组件进行实例化,然后调用实例对象的 render 方法:
获取渲染的值
首先给出一个示例
函数组件对应如下:
类组件对应如下:
两者看起来实现功能是一致的,但是在类组件中,输出 this.props.user , Props 在 React 中是不可变的所以它永远不会改变,但是 this 总是可变的,以便您可以在 render 和生命周期函数中读取新版本
因此,如果我们的组件在请求运行时更新。this.props 将会改变。showMessage 方法从“最新”的 props 中读取 user
而函数组件,本身就不存在 this,props 并不发生改变,因此同样是点击,alert 的内容仍旧是之前的内容
两种组件都有各自的优缺点:
- 函数组件语法更短、更简单,这使得它更容易开发、理解和测试
- 而类组件也会因大量使用 this 而让人感到困惑
组件中图片的使用
在 html 中可以按照如下方式使用图片:
<img src="./images/xx.png" />
但是在 react 中这样做是无法正常渲染的,因为在打包后图片的地址发生了改变,无法正确找到路径
require引入图片
<img src={require('./assets/images/logo192.png')} />
如果是以前的react版本,需要这样引入图片:
<img src={require('./assets/images/logo192.png').default} />
使用import方式引入图片
import Logo from './assets/images/logo192.png'
<img src={Logo} />
在行内样式中使用图片
也要使用上述两种方式引入图片
<div style={{ width: '100px', height: '100px', backgroundImage: `url(${require('./assets/images/logo192.png')})` }}></div>
React性能优化SCU
react渲染流程:
更新机制
react更新流程:
React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树
React需要基于这两棵不同的树之间的差别来判断如何有效的更新UI:
如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n^2),其中 n 是树中元素的数量
参考地址:https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf
如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围
这个开销太过昂贵了,React的更新性能会变得非常低效
于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?
- 同层节点之间相互比较,不会垮节点比较
- 不同类型的节点,产生不同的树结构
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定
keys的优化
我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性:
◼ 方式一:在最后位置插入数据
这种情况,有无key意义并不大
◼ 方式二:在前面插入数据
这种做法,在没有key的情况下,所有的li都需要进行修改;
◼ 当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:
在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改;
将key为333的元素插入到最前面的位置即可;
◼ key的注意事项:
key应该是唯一的;
key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
使用index作为key,对性能是没有优化的;
render函数被调用
我们使用之前的一个嵌套案例:
- 在App中,我们增加了一个计数器的代码
- 当点击+1时,会重新调用App的render函数
- 而当App的render函数被调用时,所有的子组件的render函数都会被重新调用
那么,我们可以思考一下,在以后的开发中,我们只要是修改了,App中的数据,但是其里面的所有的组件(包括子组件)都需要重新render,进行diff算法,这样一来其性能必然是很低的:
- 事实上,很多的组件没有必须要重新调用render函数进行渲染
- 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法
如何来控制render方法是否被调用呢
- 通过shouldComponentUpdate方法即可,简称SCU
shouldComponentUpdate
该方法有两个参数:
- 参数一:nextProps 修改之后,最新的props属性
- 参数二:nextState 修改之后,最新的state属性
该方法返回值是一个boolean类型:
- 返回值为true,那么就需要调用render方法;
- 返回值为false,那么久不需要调用render方法;
- 默认返回的是true,也就是只要state发生改变,就会调用render方法;
比如我们在App中增加一个message属性:
- jsx中并没有依赖这个message,那么它的改变不应该引起重新渲染
- 但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了;
在 类组件 中使用
import React, { Component } from 'react'
export default class demoClassComponent extends Component {
constructor(props) {
super(props)
this.state = {
count: 0,
}
}
shouldComponentUpdate(nextProps, nextState) {
// 自定义逻辑判断是否重新渲染组件
if (nextState.count === this.state.count) {
return false // 不重新渲染
}
return true // 重新渲染
}
handleClick = () => {
this.setState((prevState) => ({
count: prevState.count + 1,
}))
}
render() {
const { count } = this.state
return (
<div>
<button onClick={this.handleClick}>点击增加</button>
<p>Count: {count}</p>
</div>
)
}
}
在 函数组件 中使用
在函数组件中比较特殊,我们可以使用 React 的 memo
函数来实现类似于 shouldComponentUpdate
的功能。memo
是一个高阶组件(Higher-Order Component),它接收一个组件作为参数,并返回一个新的优化后的组件。
import React, { memo } from 'react'
// import PropTypes from 'prop-types'
function demoFuncClassComponent(props) {
return (
<div>
<p>{props.text}</p>
</div>
);
}
// demoFuncClassComponent.propTypes = {}
export default memo(demoFuncClassComponent)
- 在上面的示例中,使用
memo
包装了demoFuncClassComponent
组件,使其成为一个优化后的组件。只有当传入demoFuncClassComponent
的属性text
发生变化时,才会重新渲染组件 - 需要注意的是,
memo
只进行浅层比较,即只检查属性的值是否相等。如果属性是一个对象或数组,只有当引用发生变化时,才会重新渲染组件。如果需要进行深层比较,可以考虑使用其他方式,例如使用useMemo
钩子。 - 对于 Hooks
useMemo
的相关介绍将在后面学react 的 Hooks 函数时学到
PureComponent
如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量
- 我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?
- props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false;
事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?
将class继承自PureComponent
import React, { PureComponent } from 'react'
export default class demoClassComponent extends PureComponent {
constructor(props) {
super(props)
this.state = {
count: 0,
}
}
handleClick = () => {
this.setState((prevState) => ({
count: prevState.count + 1,
}))
}
render() {
const { count } = this.state
return (
<div>
<button onClick={this.handleClick}>点击增加</button>
<p>Count: {count}</p>
</div>
)
}
}
注意:
只做了浅层比较,也就是只比较第一层,只针对于类组件
但是如果是函数组件呢?这里就需要用到react的高阶组件 memo
import {memo} from 'react'
const funHello = memo(function (props) {
return (
<div>
<h3>函数组件</h3>
</div>
)
})
export default funHello
注意:
const newBook = {name: 'zz', price: 20}
// 1. 直接修改原有的state,即重新设置一遍,在PureComponent里是不能引入重新渲染的
this.state.books.push(newBook)
this.setState({books: this.state.books})
// 2. 赋值一份books,在新的books中修改,设置新的books
const books = [...this.state.books]
books.push(newBook)
this.setState({books: books})
//
const books = [...this.state.books]
newBook.price += 5;
books.push(newBook)
this.setState({books: books})
组件的生命周期
前言
在讲 React 生命周期之前,有必要先来简单聊聊 React 两个重要阶段,render 阶段和 commit 阶段,React 在调和( render )阶段会深度遍历 React fiber 树,目的就是发现不同( diff ),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行 render 函数。在一次调和过程完毕之后,就到了commit 阶段,commit 阶段会创建修改真实的 DOM 节点。
如果在一次调和的过程中,发现了一个fiber tag = 1类组件的情况,就会按照类组件的逻辑来处理。对于类组件的处理逻辑,首先判断类组件是否已经被创建过,首先来看看源码里怎么写的。
react-reconciler/src/ReactFiberBeginWork.js
几个重要的概念:
- instance 类组件对应实例。
- workInProgress 树,当前正在调和的 fiber 树 ,一次更新中,React 会自上而下深度遍历子代 fiber ,如果遍历到一个 fiber ,会把当前 fiber 指向 workInProgress。
- current 树,在初始化更新中,current = null ,在第一次 fiber 调和之后,会将 workInProgress 树赋值给 current 树。React 来用workInProgress 和 current 来确保一次更新中,快速构建,并且状态不丢失。
- Component 就是项目中的 class 组件。
- nextProps 作为组件在一次更新中新的 props 。
- renderExpirationTime 作为下一次渲染的过期时间。
上面这个函数流程我已经标的很清楚了,同学们在学习React的过程中,重要的属性一定要拿小本本记下来,比如说类组件完成渲染挂载之后, React 用什么记录组件对应的 fiber 对象和类组件实例之间的关系。只有搞清楚这些,才能慢慢深入学习 React 。
在组件实例上可以通过_reactInternals属性来访问组件对应的 fiber 对象。在 fiber 对象上,可以通过stateNode来访问当前 fiber 对应的组件实例。两者的关系如下图所示。
这里主要针对于类组件,函数组件没有生命周期,也就没有生命周期函数,但是可以用相关hooks(主要是:useEffect和useLayoutEffect)去进行模拟。
概述
- 意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等
- 组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程
- 钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。
- 只有 类组件 才有生命周期。
react生命周期主要分为三个阶段:
- 组件的挂载阶段:初始化组件数据,以及渲染元素
- 组件的更新阶段:这个节点是最长的阶段,只要props或者state数据被修改后就会进入这个阶段
-
- 组件的卸载阶段:当组件被卸载到就会进入这个阶段,可以清理定时器,订阅发布,性能优化等等。
生命周期的整体说明
- 每个阶段的执行时机
- 每个阶段钩子函数的执行顺序
- 每个阶段钩子函数的作用
- React lifecycle methods diagram
React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
- 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调
- 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调
- 比如实现componentWillUnmount函数:组件即将被移除时,就会回调
我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过hooks来模拟一些生命周期的回调)
挂载阶段
执行时机:组件创建时(页面加载时)---组件第一次在DOM树中被渲染的过程
执行顺序:
钩子 函数 | 触发时机 | 作用 |
constructor | 创建组件时,最先执行【组件的构造器,可以在这里进行数据的初始化,比如state数据的初始化】 | 1. 初始化state 2. 创建Ref等 |
render | 每次组件渲染都会触发【render函数将DOM元素进行渲染,将数据等等挂载到DOM元素上】 | 渲染UI(注意: 不能调用setState() ) |
componentDidMount | 组件挂载(完成DOM渲染)后【元素挂载完成后执行的生命周期函数】 | 1. 发送网络请求 2.DOM操作 |
componentDidMount 可以做的事情有:
- 发送请求获取后端数据
- 获取DOM元素
- 设置定时器,延时器等等
- 绑定全局事件等等
执行顺序:constructor->render->componentDidMount
import React, { PureComponent } from 'react'
export default class demoClassComponent extends PureComponent {
constructor() {
super()
this.state = {}
console.log('hello world constructor')
}
render() {
console.log('hello world render')
return <div>hello world</div>
}
componentDidMount() {
console.log('hello world componentDidMount')
}
}
更新阶段
组件状态发生变化,重新更新渲染的过程
- 执行时机:1. setState() 2. forceUpdate() 3. 组件接收到新的props
- 说明:以上三者任意一种变化,组件就会重新渲染
- 这个节点是最长的阶段,只要props或者state数据被修改后就会进入这个阶段
- 执行顺序
钩子函数 | 触发时机 | 作用 |
render | 每次组件渲染都会触发 | 渲染UI(与 挂载阶段 是同一个render) |
componentDidUpdate | 组件更新(完成DOM渲染)后 | DOM操作,可以获取到更新后的DOM内容,不要调用setState |
shouldComponentUpdate | 在props和state数据更新后,会进入到这个生命周期函数,需要返回一个布尔值,为true就会更新渲染组件,为false就不会更新渲染组件 |
shouldComponentUpdate:
在props和state数据更新后,会进入到这个生命周期函数,需要返回一个布尔值,为true就会更新渲染组件,为false就不会更新渲染组件。
shouldComponentUpdate() {
return true; // 要去更新渲染组件
}
根据业务逻辑判断是否需要更新渲染组件,达到性能优化的目的
shouldComponentUpdate(nextProps, nextState) {
console.log('nextProps', nextProps);
console.log('nextState', nextState);
console.log('state', this.state);
if (nextState.title === this.state.title) {
return false;
} else {
return true;
}
}
componentDidUpdate:
在数据更新完毕,DOM元素更新渲染完毕后执行的生命周期函数
componentDidUpdate(prevProps, prevState) {
console.log(prevProps, prevState);
console.log('=========componentDidUpdate');
}
在这里,你可以对比新旧数据达到监听数据的效果。
执行顺序: [shouldComponentUpdate->]render->componentDidUpdate
import React, { PureComponent } from 'react'
export default class demoClassComponent extends PureComponent {
constructor() {
super()
this.state = {
message: 'hello world',
}
console.log('render')
}
changeMessage() {
this.setState({
message: '你好,李银河',
})
}
render() {
const { message } = this.state
return (
<div>
<p>{message}</p>
<button onClick={() => this.changeMessage()}>更改</button>
</div>
)
}
// 组件DOM被更新完成
componentDidUpdate() {
console.log('componentDidUpdate')
}
}
卸载阶段
组件从DOM树中被移除的过程
- 执行时机:组件从页面中消失
钩子函数 | 触发时机 | 作用 |
componentWillUnmount | 组件卸载(从页面中消失) | 执行清理工作(比如:清理定时器等) |
componentWillUnmount
组件卸载会触发的生命周期,可以清理定时器,订阅发布,性能优化等等。
componentWillUnmount() {
console.log('======componentWillUnmount');
}
在componentDidMount生命周期中设置setInterval
定时器,组件卸载后需要将这个定时器清理掉,否则会一直执行,导致浪费性能。
import React, { Component } from 'react'
export default class Life extends Component {
timer = null;
componentDidMount() {
console.log('=======componentDidMount');
this.timer = setInterval(() => {
console.log(1);
}, 1000);
}
componentWillUnmount() {
console.log('======componentWillUnmount');
clearInterval(this.timer);
}
render() {
return (
<div></div>
)
}
}
生命周期总结
Constructor:
- 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
- constructor中通常只做两件事情:
-
- 通过给 this.state 赋值对象来初始化内部的state
-
- 为事件绑定实例(this)
componentDidMount:
- componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用
- componentDidMount中通常进行哪里操作呢?
-
- 依赖于DOM的操作可以在这里进行
-
- 在此处发送网络请求就最好的地方;(官方建议)
-
- 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)
componentDidUpdate:
- componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。
- 当组件更新后,可以在此处对 DOM 进行操作;
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
componentWillUnmount:
- componentWillUnmount() 会在组件卸载及销毁之前直接调用
- 在此方法中执行必要的清理操作;
- 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等
补充-不常用生命周期函数
getDerivedStateFromProps:
- state 的值在任何时候都依赖于 props时使用;
- 该方法返回一个对象来更新state;
getSnapshotBeforeUpdate:
- 在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置)
shouldComponentUpdate:
- 该生命周期函数很常用
- 接收三个参数:prevProps、prevState、snapshot
更详细的生命周期相关的内容,可以参考官网:React.Component – React
表单处理
我们在开发过程中,经常需要操作表单元素,比如获取表单的值或者是设置表单的值。
react中处理表单元素有两种方式:
- 受控组件
- 非受控组件(DOM操作)
受控组件
基本概念
- HTML中表单元素是可输入的,即表单用户并维护着自己的可变状态(value)。
- 但是在react中,可变状态通常是保存在state中的,并且要求状态只能通过
setState
进行修改
- React中将state中的数据与表单元素的value值绑定到了一起,
由state的值来控制表单元素的值
- 受控组件:value值受到了react控制的表单元素
受控组件使用步骤
在受控组件中,我们要获取到用户录入信息:
- 在state中生命数据进行保存
- 通过
value
将数据绑定到DOM元素上
- 在DOM元素上绑定
onChange
事件,通过这个事件可以获取到事件信息,以此来获取用户录入信息e.target.value
- 通过
setState
将值保存到state数据中
- 在state中添加一个状态,作为表单元素的value值(控制表单元素的值)
- 给表单元素添加change事件,设置state的值为表单元素的值(控制值的变化)
import React, { PureComponent } from 'react'
export default class demoClassComponent extends PureComponent {
constructor(props) {
super(props)
this.state = {
msg: 'admin', // 表单元素的value值
}
}
handleChange = (e) => {
this.setState({
msg: e.target.value,
})
}
render() {
const { msg } = this.state
return (
<div>
账号:
<input
type="text"
placeholder="请输入账号"
value={this.state.msg}
onChange={this.handleChange}
/>
</div>
)
}
}
常见的受控组件
- 文本框、文本域、下拉框(操作value属性)
- 复选框(操作checked属性)
class App extends React.Component {
state = {
usernmae: '',
desc: '',
city: "2",
isSingle: true
}
handleName = e => {
this.setState({
name: e.target.value
})
}
handleDesc = e => {
this.setState({
desc: e.target.value
})
}
handleCity = e => {
this.setState({
city: e.target.value
})
}
handleSingle = e => {
this.setState({
isSingle: e.target.checked
})
}
render() {
return (
<div>
姓名:<input type="text" value={this.state.username} onChange={this.handleName}/>
<br/>
描述:<textarea value={this.state.desc} onChange={this.handleDesc}></textarea>
<br/>
城市:<select value={this.state.city} onChange={this.handleCity}>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
<option value="4">深圳</option>
</select>
<br/>
是否单身:<input type="checkbox" checked={this.state.isSingle} onChange={this.handleSingle}/>
</div>
)
}
}
多表单元素的优化
问题:每个表单元素都需要一个单独的事件处理程序,处理太繁琐
优化:使用一个事件处理程序处理多个表单元素
步骤
- 给表单元素添加name属性,名称与state属性名相同
- 根据表单元素类型获取对应的值
- 在事件处理程序中通过
[name]
修改对应的state
示例一
import React, { PureComponent } from 'react'
export default class demoClassComponent extends PureComponent {
state = {
username: '',
desc: '',
city: '2',
isSingle: true,
}
handleChange = (e) => {
let { name, type, value, checked } = e.target
console.log(name, type, value, checked)
value = type === 'checkbox' ? checked : value
console.log(name, value)
this.setState({
[name]: value,
})
}
render() {
return (
<div>
姓名:
<input
type="text"
name="username"
value={this.state.username}
onChange={this.handleChange}
/>
<br />
描述:
<textarea
name="desc"
value={this.state.desc}
onChange={this.handleChange}
></textarea>
<br />
城市:
<select
name="city"
value={this.state.city}
onChange={this.handleChange}
>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
<option value="4">深圳</option>
</select>
<br />
<label htmlFor="isSingle">
是否单身:
<input
id="isSingle"
type="checkbox"
name="isSingle"
checked={this.state.isSingle}
onChange={this.handleChange}
/>
</label>
</div>
)
}
}
示例二
import React, { Component } from "react";
class classHello extends Component {
constructor(){
super()
this.state = {
username: '',
password: ''
}
}
changeInputChange = (e) => {
this.setState({
[e.target.name]: e.target.value
})
console.log(this.state.username);
console.log(this.state.password);
}
render() {
let { username, password } = this.state
return (
<div>
<p>
用户名:<input type="text" name="username" value={username} onChange={e => this.changeInputChange(e)} />
</p>
<p>
密码:<input type="password" name="password" value={password} onChange = {e => this.changeInputChange(e)} />
</p>
</div>
);
}
}
export default classHello;
示例三
import React, { Component } from "react";
class classHello extends Component {
constructor() {
super()
this.state = {
username: "",
password: "",
isAgree: false,
hobbies: [
{ value: "sing", text: "唱", isChecked: false },
{ value: "dance", text: "跳", isChecked: false },
{ value: "rap", text: "rap", isChecked: false }
],
// fruit: ["orange"] // 多选
fruit: "orange"
}
}
handleSubmitClick(event) {
// 1.阻止默认的行为
event.preventDefault()
// 2.获取到所有的表单数据, 对数据进行操作
console.log("获取所有的输入内容")
console.log(this.state.username, this.state.password)
const hobbies = this.state.hobbies.filter(item => item.isChecked).map(item => item.value)
console.log("获取爱好: ", hobbies)
// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
}
handleInputChange(event) {
this.setState({
[event.target.name]: event.target.value
})
}
handleAgreeChange(event) {
this.setState({ isAgree: event.target.checked })
}
handleHobbiesChange(event, index) {
const hobbies = [...this.state.hobbies]
hobbies[index].isChecked = event.target.checked
this.setState({ hobbies })
}
handleFruitChange(event) {
// 多选
// const options = Array.from(event.target.selectedOptions)
// const values = options.map(item => item.value)
// this.setState({ fruit: values })
// 额外补充: Array.from(可迭代对象)
// Array.from(arguments)
const values2 = Array.from(event.target.selectedOptions, item => item.value)
console.log(values2)
}
render() {
const { username, password, isAgree, hobbies, fruit } = this.state
return (
<div>
<form onSubmit={e => this.handleSubmitClick(e)}>
{/* 1.用户名和密码 */}
<div>
<label htmlFor="username">
用户:
<input
id='username'
type="text"
name='username'
value={username}
onChange={e => this.handleInputChange(e)}
/>
</label>
<label htmlFor="password">
密码:
<input
id='password'
type="password"
name='password'
value={password}
onChange={e => this.handleInputChange(e)}
/>
</label>
</div>
{/* 2.checkbox单选 */}
<label htmlFor="agree">
<input
id='agree'
type="checkbox"
checked={isAgree}
onChange={e => this.handleAgreeChange(e)}
/>
同意协议
</label>
{/* 3.checkbox多选 */}
<div>
您的爱好:
{
hobbies.map((item, index) => {
return (
<label htmlFor={item.value} key={item.value}>
<input
type="checkbox"
id={item.value}
checked={item.isChecked}
onChange={e => this.handleHobbiesChange(e, index)}
/>
<span>{item.text}</span>
</label>
)
})
}
</div>
{/* 4.select */}
{/**<select value={fruit} onChange={e => this.handleFruitChange(e)} multiple></select> 多选 */}
<select value={fruit} onChange={e => this.handleFruitChange(e)}>
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
<div>
<button type='submit'>注册</button>
</div>
</form>
</div>
)
}
}
export default classHello;
示例四
import React, { PureComponent } from 'react'
export class App extends PureComponent {
constructor() {
super()
this.state = {
username: "",
password: ""
}
}
handleSubmitClick(event) {
// 1.阻止默认的行为
event.preventDefault()
// 2.获取到所有的表单数据, 对数据进行组件
console.log("获取所有的输入内容")
console.log(this.state.username, this.state.password)
// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
}
handleInputChange(event) {
this.setState({
[event.target.name]: event.target.value
})
}
render() {
const { username, password } = this.state
return (
<div>
<form onSubmit={e => this.handleSubmitClick(e)}>
{/* 1.用户名和密码 */}
<label htmlFor="username">
用户:
<input
id='username'
type="text"
name='username'
value={username}
onChange={e => this.handleInputChange(e)}
/>
</label>
<label htmlFor="password">
密码:
<input
id='password'
type="password"
name='password'
value={password}
onChange={e => this.handleInputChange(e)}
/>
</label>
<button type='submit'>注册</button>
</form>
</div>
)
}
}
export default App
非受控组件-ref
非受控组件借助于ref,使用原生DOM的方式来获取表单元素的值
基本概念
- 如果react中的组件全是受控组件,并不合理,因为有时候我们也需要去操作DOM元素,react提供了如何去操作DOM元素的方式。
在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:
- 管理焦点,文本选择或媒体播放
- 触发强制动画
- 集成第三方 DOM 库
- 我们可以通过refs获取DOM
创建 ref 的形式有三种:
非受控组件基本使用
不推荐!!!----相当于直接操作DOM
import React, { createRef, PureComponent } from 'react'
export class App extends PureComponent {
constructor() {
super()
this.state = {
intro: "哈哈哈"
}
this.introRef = createRef()
}
componentDidMount() {
this.introRef.current.addEventListener
}
handleSubmitClick(event) {
// 1.阻止默认的行为
event.preventDefault()
// 2.获取到所有的表单数据, 对数据进行组件
console.log("获取结果:", this.introRef.current.value)
// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
}
render() {
const { intro } = this.state
return (
<div>
<form onSubmit={e => this.handleSubmitClick(e)}>
{/* 5.非受控组件 */}
<input type="text" defaultValue={intro} ref={this.introRef} />
<div>
<button type='submit'>注册</button>
</div>
</form>
</div>
)
}
}
export default App
获取DOM常见的几种方法
传入字符串
给元素绑定ref字符串(不推荐使用)--使用时通过 this.refs.传入的字符串格式获取对应的元素;
<input ref="inputRef" />
this.refs.inputRef
传入一个函数
该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存
使用时,直接拿到之前保存的元素对象即可
关键代码:
<input ref={(el) => this.accountRef = el} placeholder='请输入账号' />
全代码:
import React, { Component } from 'react'
export default class LoginForm extends Component {
accountRef = null;
passwordRef = null;
login = () => {
let accountValue = this.accountRef.value;
console.log(accountValue);
let passwordValue = this.passwordRef.value;
console.log(passwordValue);
}
render() {
return (
<div>
账号:<input ref={(el) => this.accountRef = el} placeholder='请输入账号' />
<br />
密码:<input ref={(el) => this.passwordRef = el} placeholder='请输入密码' />
<br />
<button onClick={this.login}>登录</button>
</div>
)
}
}
传入一个对象
通过react提供的createRef函数去绑定ref--推荐
使用时获取到创建的对象其中有一个current属性就是对应的元素
关键代码:
constructor(props) {
super(props);
this.accountRef = React.createRef();
this.passwordRef = React.createRef();
}
<input ref={this.accountRef} placeholder='请输入账号' />
通过createRef
拿到的数据保存在了current
中,通过current
才能获取到真实的DOM元素
全代码:
import React, { Component } from 'react'
export default class LoginForm extends Component {
constructor(props) {
super(props);
this.accountRef = React.createRef();
this.passwordRef = React.createRef();
}
login = () => {
console.log(this.accountRef.current.value);
console.log(this.passwordRef.current.value);
}
render() {
return (
<div>
账号:<input ref={this.accountRef} placeholder='请输入账号' />
<br />
密码:<input ref={this.passwordRef} placeholder='请输入密码' />
<br />
<button onClick={this.login}>登录</button>
</div>
)
}
}
传入hook
通过 uSeRef 创建一个 ref,整体使用方式与 React.createRef一致
上述三种情况都是 ref 属性用于原生 HTML 元素上,如果 ref 设置的组件为一个类组件的时候, ref对象接收到的是组件的挂载实例
ref的类型
ref 的值根据节点的类型而有所不同:
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
- 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
- 你不能在函数组件上使用 ref 属性,因为他们没有实例
函数式组件是没有实例的,所以无法通过ref获取他们的实例:
- 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
- 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref
import React, {forwardRef} from 'react'
const funHello = forwardRef(
function (props,ref) {
return (
<div>
<h3 ref={ref}>函数组件</h3>
</div>
)
}
)
export default funHello
父组件:
import React, { Component, createRef } from 'react'
import FunHello from './components/funHello';
export default class App extends Component {
constructor () {
super()
this.funRef = createRef()
}
componentDidMount(){
console.log(this.funRef.current);
}
render() {
return (
<div>
<FunHello ref={this.funRef} />
</div>
)
}
}
应用场景
在某些情况下,我们会通过使用 refs 来更新组件,但这种方式并不推荐,更多情况我们是通过 prop s与 state 的方式进行去重新渲染子元素
过多使用 refs,会使组件的实例或者是 DOM 结构暴露,违反组件封装的原则
例如,避免在 Dialog 组件里暴露 open()和 close()方法,最好传递 isOpen 属性但下面的场景使用 refs 非常有用:
state解析(针对于类组件)
React 项目中 UI 的改变来源于 state 改变,类组件中setState是更新组件,渲染视图的主要方式。
基本用法
setState(obj,callback)
第一个参数:当 obj 为一个对象,则为即将合并的 state ;如果 obj 是一个函数,那么当前组件的 state 和 props 将作为参数,返回值用于合并新的 state。
第二个参数 callback :callback 为一个函数,函数执行上下文中可以获取当前 setState 更新后的最新 state 的值,可以作为依赖 state 变化的副作用函数,可以用来做一些基于 DOM 的操作。
// 第一个参数为function类型
this.setState((state,props) => {
return {number: 1}
})
// 第一个参数为object类型
this.setState({number:1}, () => {
console.log(this.state.number); // 获取最新的 number
})
假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?
- 首先,setState 会产生当前更新的优先级(老版本用 expirationTime ,新版本用 lane )。
- 接下来 React 会从 fiber Root 根部 fiber 向下调和子节点,调和阶段将对比发生更新的地方,更新对比 expirationTime ,找到发生更新的组件,合并 state,然后触发 render 函数,得到新的 UI 视图层,完成 render 阶段。
- 接下来到 commit 阶段,commit 阶段,替换真实 DOM ,完成此次更新流程。
- 此时仍然在 commit 阶段,会执行 setState 中 callback 函数,如上的()=>{ console.log(this.state.number) },到此为止完成了一次 setState 全过程。
render 阶段 render 函数执行 -> commit 阶段真实 DOM 替换 -> setState 回调函数执行 callback 。
类组件如何限制 state 更新视图
对于类组件如何限制 state 带来的更新作用的呢?
① pureComponent 可以对 state 和 props 进行浅比较,如果没有发生变化,那么组件不更新。
② shouldComponentUpdate 生命周期可以通过判断前后 state 变化来决定组件需不需要更新,需要更新返回true,否则返回false。
setState原理揭秘
知其然,知其所以然,想要吃透 setState,就需要掌握一些 setState 的底层逻辑。 上一章节讲到对于类组件,类组件初始化过程中绑定了负责更新的Updater对象,对于如果调用 setState 方法,实际上是 React 底层调用 Updater 对象上的 enqueueSetState 方法。
因为要弄明白 state 更新机制,所以接下来要从两个方向分析。
- 一是揭秘 enqueueSetState 到底做了些什么?
- 二是 React 底层是如何进行批量更新的?
首先,这里极致精简了一波 enqueueSetState 代码。如下
react-reconciler/src/ReactFiberClassComponent.js
enqueueSetState作用实际很简单,就是创建一个 update ,然后放入当前 fiber 对象的待更新队列中,最后开启调度更新,进入上述讲到的更新流程。
那么问题来了,React 的 batchUpdate 批量更新是什么时候加上去的呢?
这就要提前聊一下事件系统了。正常state 更新、UI 交互,都离不开用户的事件,比如点击事件,表单输入等,React 是采用事件合成的形式,每一个事件都是由 React 事件系统统一调度的,那么 State 批量更新正是和事件系统息息相关的。
react-dom/src/events/DOMLegacyEventPluginSystem.js
重点来了,就是下面这个 batchedEventUpdates 方法。
react-dom/src/events/ReactDOMUpdateBatching.js
如上可以分析出流程,在 React 事件执行之前通过isBatchingEventUpdates=true打开开关,开启事件批量更新,当该事件结束,再通过isBatchingEventUpdates = false;关闭开关,然后在 scheduleUpdateOnFiber 中根据这个开关来确定是否进行批量更新。
举一个例子,如下组件中这么写:
export default class Index extends React.Component{
state = { number: 0 }
handleClick = () => {
this.setState({number: this.state.number+1}, () => {console.log('callback1', this.state.number);})
console.log(this.state.number);
this.setState({number: this.state.number+1}, () => {console.log('callback2', this.state.number);})
console.log(this.state.number);
this.setState({number: this.state.number+1}, () => {console.log('callback3', this.state.number);})
console.log(this.state.number);
}
render() {
return (
<div>
{ this.state.number }
<button onClick={ this.handleClick }>number++</button>
</div>
)
}
}
点击打印:0,0,0,'callback1',1,'callback2',1,'callback3',1
如上代码,在整个 React 上下文执行栈中会变成这样:
那么,为什么异步操作里面的批量更新规则会被打破呢?比如用 promise 或者 setTimeout 在 handleClick 中这么写:
setTimeout(() => {
this.setState({number: this.state.number+1}, () => {console.log('callback1', this.state.number);})
console.log(this.state.number);
this.setState({number: this.state.number+1}, () => {console.log('callback2', this.state.number);})
console.log(this.state.number);
this.setState({number: this.state.number+1}, () => {console.log('callback3', this.state.number);})
console.log(this.state.number);
})
打印:'callback1',1,1,'callback2',2,2,'callback3',3,3
那么在整个 React 上下文执行栈中就会变成如下图这样:
所以批量更新规则被打破
那么,如何在如上异步环境下,继续开启批量更新模式呢?
React-Dom 中提供了批量更新方法 unstable_batchedUpdates,可以去手动批量更新,可以将上述 setTimeout 里面的内容做如下修改:
import ReactDOM from "react-dom";
const { unstable_batchedUpdates } = ReactDOM;
setTimeout(() => {
unstable_batchedUpdates(() => {
this.setState({number: this.state.number+1}, () => {console.log('callback1', this.state.number);})
console.log(this.state.number);
this.setState({number: this.state.number+1}, () => {console.log('callback2', this.state.number);})
console.log(this.state.number);
this.setState({number: this.state.number+1}, () => {console.log('callback3', this.state.number);})
console.log(this.state.number);
})
})
打印:0,0,0,'callback1',1,'callback2',1,'callback3',1
在实际工作中,unstable_batchedUpdates 可以用于 Ajax 数据交互之后,合并多次 setState,或者是多次 useState 。原因很简单,所有的数据交互都是在异步环境下,如果没有批量更新处理,一次数据交互多次改变 state 会促使视图多次渲染。
那么如何提升更新优先级呢?
React-dom 提供了 flushSync ,flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。React 设定了很多不同优先级的更新任务。如果一次更新任务在 flushSync 回调函数内部,那么将获得一个较高优先级的更新。
接下来,将上述handleClick改版如下样子:
首先flushSync this.setState({ number: 3 })设定了一个高优先级的更新,所以 2 和 3 被批量更新到 3 ,所以 3 先被打印。
更新为 4。
最后更新 setTimeout 中的 number = 1。
flushSync补充说明:flushSync 在同步条件下,会合并之前的 setState | useState,可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,就会一起合并了,所以就解释了如上,2 和 3 被批量更新到 3 ,所以 3 先被打印。
综上所述, React 同一级别更新优先级关系是:
flushSync 中的 setState>正常执行上下文中 setState>setTimeout ,Promise 中的 setState。
state属性
state数据是组件的内部数据,一旦更新后会引起组件内部dom的更新和渲染,类似于vue中的data数据
react中函数组件没有内部状态,只有类组件有内部状态,因此state只针对于类组件
定义/使用数据
在react类组件中定义state有两种方式:
- 定义数据:
1)在构造器constructor中定义state
import React, { Component } from 'react'
export default class State extends Component {
constructor(props) {
super(props);
this.state = {
title: '今天星期五,心情是大不同。'
}
}
render() {
return (
<div>
标题:{this.state.title}
</div>
)
}
}
2)直接给属性state定义数据
import React, { Component } from 'react'
export default class State extends Component {
state = {
title: '今天星期五,心情是大不同。'
}
render() {
return (
<div>
标题:{this.state.title}
</div>
)
}
}
- 使用state数据:
render() {
return (
<div>
标题:{this.state.title}
</div>
)
}
修改数据
在react中修改state数据,如果直接通过=
赋值,可以修改数据,但是不能引起组件的更新渲染:
this.state.title = '今天不上晚自习';
正确修改state数据,应该使用this.setState()
函数去修改:
changeTitle = () => {
this.setState({
title: '今天不上晚自习'
})
}
setState修改状态
- 组件中的状态是可变的
- 语法
this.setState({要修改的数据})
- 注意:不要直接修改state中的值,必须通过
this.setState()
方法进行修改
setState
的作用
-
- 修改state
-
- 更新UI
- 思想:数据驱动视图
class App extends React.Component {
state = {
count: 1
}
handleClick() {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<p>次数: {this.state.count}</p>
<button onClick={this.handleClick.bind(this)}>点我+1</button>
</div>
)
}
}
- react中核心理念:状态不可变
-
- 不要直接修改react中state的值,而是提供新的值
-
- 直接修改react中state的值,组件并不会更新
state = {
count: 0,
list: []
}
// 直接修改值的操作
this.state.count++
this.state.list.push('a')
// 创建新的值的操作
this.setState({
count: this.state.count + 1,
list: [...this.state.list, 'b']
})
修改对象
this.setState({
user: {
name: '永恩'
}
})
修改state中的对象属性时,要注意将其余的属性都要加上,如果不加会丢失属性
正确的修改方式:
1)将对象扩展
this.setState({
user: {
...this.state.user,
name: '永恩',
}
})
2)直接修改state中对象的属性,然后再通过setState
更新渲染
this.state.user.name = '永恩';
this.setState({
user: this.state.user
})
减轻state
- 减轻 state:只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading 等)
- 注意:不用做渲染的数据不要放在 state 中,比如定时器 id等
- 对于这种需要在多个方法中用到的数据,应该直接放在 this 中
-
- this.xxx = 'bbb'
-
- this.xxx
class Hello extends Component {
componentDidMount() {
// timerId存储到this中,而不是state中
this.timerId = setInterval(() => {}, 2000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
render() { … }
}
vue中不要把和渲染无关的数据放到data中
setState
为什么要使用 setState
Vue和React数据管理和渲染流程对比:
为什么使用setState:
开发中我们并不能直接通过修改state的值来让界面发生更新:
- 因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化
- React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化
- 我们必须通过setState来告知React数据已经发生了变化
在组件中并没有实现setState的方法,为什么可以调用呢?
- 原因很简单,setState方法是从Component中继承过来的
执行流程
执行过程
setState异步/同步
在react18版本,任何情况下setState都是异步
要是想在react18中进行同步操作,需要按照如下操作:
import { flushSync } from 'react-dom'
change = () => {
flushSync(() => {
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
console.log(this.state.count)
})
}
在react17版本(18版本以前),如果平常使用就是异步的,但是在定时器或者js原生事件中是同步的
add = (num) => {
// setTimeout(() => {
// this.setState({
// count: this.state.count + num
// });
// console.log(this.state.count);
// }, 0);
this.setState({
count: this.state.count + num
});
console.log(this.state.count);
}
为什么setState设计为异步呢?
- setState设计为异步,可以显著的提升性能;
-
- 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的
-
- 最好的办法应该是获取到多个更新,之后进行批量更新
- 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步
-
- state和props不能保持一致性,会在开发中产生很多的问题
那么如何可以获取到更新后的值呢?(如何获取异步的结果)
方式一:setState的回调
- setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行
- 格式如下:setState(partialState, callback)
方式二:在生命周期函数中获取
- 当然,我们也可以在生命周期函数中获取更新后的值
setState的合并
在react中连续执行setState会合并成一次执行,官方解释是为了做性能优化。
是通过 Object.assign(this.state, newState) 进行合并,即如果后面存在同名的属性,那么后者覆盖前者,再调用 render() 函数进行渲染
// 挂载完成后执行的,类似于vue的mounted
componentDidMount() {
this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
});
}
加1加三次的解决方案:
1)将值解构出来进行操作
// 挂载完成后执行的,类似于vue的mounted
componentDidMount() {
let { count } = this.state;
count += 1;
count += 1;
count += 1;
this.setState({
count
})
}
2)通过setState第一个参数去更新数据
setState第一个参数可以是一个函数,接收一个原来的state数据作为参数,这个函数需要返回一个对象,作为修改后的数据。
好处:
- 可以在回调函数中编写新的state的逻辑
- 当前的回调函数会将之前的state和props传递进来
// 挂载完成后执行的,类似于vue的mounted
componentDidMount() {
this.setState((prevState) => {
return {
count: prevState.count + 1
}
});
this.setState((prevState) => {
return {
count: prevState.count + 1
}
});
this.setState((prevState) => {
console.log(this.state.xxx, this.props)
return {
count: prevState.count + 1
}
});
}
setState第二个参数
setState第二个参数是一个函数
这个函数执行的时机是在数据更新完毕,DOM元素更新渲染完毕后执行
在这个函数里面可以获取到最新的数据和最新的DOM元素
- 场景:在状态更新(页面完成重新渲染)后获取对应的结果并立即执行某个操作
- 语法:
setState(updater[, callback])
this.setState(
(state) => ({}),
() => {console.log('这个回调函数会在状态更新后立即执行')}
)
this.setState({
count: this.state.count + num
}, () => {
console.log(this.state.count);
});
总结
setState设计为异步,可以显著的提升性能:
- 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的
- 最好的办法应该是获取到多个更新,之后进行批量更新
increment(){
this.setState((prevState) => {
return {
count: prevState.count + 1
}
});
this.setState((prevState) => {
return {
count: prevState.count + 1
}
});
......
}
如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步:
- state和props不能保持一致性,会在开发中产生很多的问题
综合案例
评论列表案例
列表展示功能
渲染评论列表(列表渲染)
- 在state中初始化评论列表数据
- 使用数组的map方法遍历列表数据
- 给每个li添加key属性
发表评论功能
获取评论信息,评论人和评论内容(受控组件)
- 使用受控组件的方式获取评论数据
发表评论,更新评论列表(更新状态)
- 给comments增加一条数据
边界处理
- 清空内容
- 判断非空
清空评论功能
- 给清空评论按钮注册事件
- 清空评论列表
- 没有更多评论的处理
源码:
commit.jsx
/**
* 1. 导入react和react-dom
* 2. 创建 react 元素
* 3. 把 react 元素渲染到页面
*/
import React from 'react';
import ReactDom from 'react-dom/client';
import { Component } from 'react';
import './index.css'
/*
主要实现的功能:
1. 展示评论功能
1.1 通过 state 提供评论列表数据
1.2 通过 map 动态渲染
2. 清空评论功能
3. 发表评论功能
4. 删除评论功能
5. 没有更多评论的处理
*/
class App extends Component {
state = ({
list: [
{
id: 1,
name: '张三',
content: '宝,我昨天去输液了。你知道是输的什么液吗?是想你的夜'
},
{
id: 2,
name: '李四',
content: '哈哈,笑死!!!居然还有这种土味情话'
},
{
id: 3,
name: '王五',
content: '我要定一个小目标,那就是先挣它一个亿!'
},
{
id: 4,
name: '赵六',
content: '嚯哟,您这小目标可真是够小的,祝早日实现!!!'
}
],
name: '',
content: ''
})
render() {
return (
<div className="app">
<div>
<input className="user" type="text" placeholder="请输入评论人" value={this.state.name} onChange={this.handleChange} name="name" />
<br />
<textarea
className="content"
cols="30"
rows="10"
placeholder="请输入评论内容"
value={this.state.content}
onChange={this.handleChange}
name="content"
/>
<br />
<button onClick={this.add}>发表评论</button>
<button onClick={this.clearAll}>清空评论</button>
</div>
{
this.renderList()
}
</div>
)
}
// 清空评论
clearAll = () => {
this.setState({
list: []
})
}
// 没有更多评论的处理
renderList() {
if(this.state.list.length === 0) {
return (<div className="no-comment">暂无评论</div>);
} else {
return (
<ul>
{
this.state.list.map(item =>
<li key={item.id}>
<h3>评论人:{item.name}</h3>
<p>评论内容:{item.content}</p>
<button onClick={this.del.bind(this, item.id)}>删除</button>
</li>
)
}
</ul>
)
}
}
// 删除评论功能
del = (id) => {
// console.log(id);
this.setState({
list: this.state.list.filter(item => item.id !== id)
})
}
// 发表评论功能
handleChange = (e) => {
const { name, value } = e.target;
this.setState({
[name]: value
})
}
add = () => {
const { name, content, list } = this.state;
// 当 name 或者 content 没有值
if(!name || !content){
return alert('信息不完整!');
}
// 添加评论
this.setState({
list: [{id: Date.now(), name: name, content: content} ,...list],
name: '',
content: ''
})
}
}
// 幽灵节点:节点不会渲染任何的内容,跟 vue 里面的 template 标签一样
const element = (
<React.Fragment>
<App></App>
</React.Fragment>
);
// 参数1:渲染的 react 元素即虚拟 DOM
// 参数2:需要渲染到哪个容器中
const root = ReactDom.createRoot(document.getElementById('root'));
root.render(element);
index.css
.app {
width: 400px;
padding: 10px;
border: 1px solid #999;
}
.user {
width: 100%;
box-sizing: border-box;
margin-bottom: 10px;
}
.content {
width: 100%;
box-sizing: border-box;
margin-bottom: 10px;
}
.no-comment {
text-align: center;
margin-top: 30px;
}
todoList 案例
components/todo.jsx
import React, { Component } from "react";
export default class Todo extends Component {
state = {
inputVal: "",
todo: [
{
id: 1,
todo: "吃饭",
done: false,
},
{
id: 2,
todo: "睡觉",
done: true,
},
],
liRef: null,
};
// 渲染函数
show = () => {
return this.state.todo.map((item, index) => (
<React.Fragment key={item.id}>
<div className="item" style={{ display: "flex" }}>
<li
style={
item.done ? { color: "red", textDecoration: "line-through" } : {}
}
onClick={() => this.liBtn(index)}
>
{item.todo}
</li>
<button
onClick={() => this.delBtn(item.id)}
style={{
marginLeft: "10px",
width: "60px",
height: "25px",
lineHeight: "25px",
}}
>
删除
</button>
</div>
</React.Fragment>
));
};
// 监听输入框的值变化
inputChange = (e) => {
let value = e.target.value;
this.setState({
inputVal: value,
});
};
// 点击 添加 按钮
addBtn = () => {
if (this.state.inputVal === "") {
alert("不能为空");
return;
}
this.state.todo.push({
id:
this.state.todo.length === 0
? 1
: this.state.todo[this.state.todo.length - 1].id + 1,
todo: this.state.inputVal,
done: false,
});
this.setState({
todo: this.state.todo,
});
this.setState({
inputVal: "",
});
};
// 点击 li 进行操作
liBtn = (i) => {
// 解构
let { todo } = this.state;
todo[i].done = !todo[i].done;
this.setState({
todo,
});
};
// 删除
delBtn = (i) => {
const arr = this.state.todo.filter((v) => v.id !== i);
this.setState({
todo: arr,
});
};
// 全部完成
changDone = (flag) => {
if (flag) {
this.state.todo.forEach((item) => {
item.done = true;
});
this.setState({
todo: this.state.todo,
});
} else {
this.state.todo.forEach((item) => {
item.done = false;
});
this.setState({
todo: this.state.todo,
});
}
};
// 全部未完成
changeAllNoDone = () => {};
render(data) {
return (
<div className="todo-container">
<div className="input">
<input
placeholder="请输入需要添加的任务"
type="text"
onChange={this.inputChange}
value={this.state.inputVal}
/>
<button onClick={this.addBtn}>添加</button>
</div>
<div className="todo-body">
<ul>{this.show()}</ul>
</div>
<footer className="todo-footer">
<button onClick={() => this.changDone(true)}>全部完成</button>
<button onClick={() => this.changDone(false)}>全部未完成</button>
</footer>
</div>
);
}
}
App.js
import './App.css';
import React from 'react';
import Todo from './components/todo.jsx'
function App() {
return (
<div className="App">
<Todo />
</div>
);
}
export default App;
简易购物车
版本一
import '../assets/css/shop1.css'
import React from "react";
class Food extends React.Component {
constructor() {
super();
this.state = {
goods: [
{
id: 1,
img: require("../assets/images/model1.jpg"),
big_title: "Java入门到放弃",
small_title: "精通Java的是个步骤",
price: 998
},
{
id: 2,
img: require("../assets/images/model2.jpg"),
big_title: "web入门到住院",
small_title: "前端性能优化的一个不会",
price: 19999
},
{
id: 3,
img: require("../assets/images/model3.jpg"),
big_title: "python爬虫实战",
small_title: "数据的抓取艺术",
price: 88888,
},
{
id: 4,
img: require("../assets/images/model4.jpg"),
big_title: "python爬虫实战",
small_title: "数据的抓取艺术",
price: 6666
},
],
cart: [],
}
}
table = () => {
return (<table>
<thead>
<tr>
<th>编号</th>
<th>图片</th>
<th>标题</th>
<th>价格</th>
<th>数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{this.show()}
</tbody>
</table>)
};
show = () => {
if (this.state.cart.length <= 0) {
return <tr><td colspan="6" style={{textAlign:"center"}}>还没有任何商品</td></tr>
} else {
return this.state.cart.map(v => <tr key={v.id}>
<td>{v.id}</td>
<td><img src={v.img} alt="" width="80px" /></td>
<td>{v.big_title}</td>
<td>{v.price}</td>
<td>{v.num}</td>
<td><button onClick={()=>this.del(v)}>删除</button></td>
</tr>)
}
};
// 删除
del=(item)=>{
item.num--
if(item.num===0){
this.state.cart=this.state.cart.filter(v=>item.id!==v.id)
}
this.setState({
cart:this.state.cart
})
}
// 添加
add = (item) => {
let { cart} = this.state
let obj=cart.filter(v => v.id === item.id)[0]
if (obj) {
obj.num++
} else {
obj = { ...item }
obj.num = 1
cart.push(obj)
}
this.setState({
cart
})
}
// 小计求和
sum(){
let sum=0
this.state.cart.forEach(v=>{
sum+=v.price*v.num
})
return sum
}
render() {
return (
<div className="container">
<h2>产品</h2>
<div className="list">
{this.state.goods.map(v => <div key={v.id} className="item"><img src={v.img} alt="" /><p>{v.big_title}</p><p>{v.small_title}</p><button onClick={() => this.add(v)}>加入购物车</button></div>)}
</div>
<h2>购物车</h2>
<div className='mytable'>
{this.table()}
</div>
<p>
总价:{this.sum()}
</p>
</div>
)
}
}
export default Food
版本二
import React, { Component } from 'react'
import './assets/css/shop.css'
export default class App extends Component {
state = {
productList: [{
id: 1,
img: require('./assets/images/model1.jpg'),
title: 'java入门到放弃',
desc: '精通java的是个步骤',
price: 1000
}, {
id: 2,
img: require('./assets/images/model2.jpg'),
title: 'java入门到放弃',
desc: '精通java的是个步骤',
price: 1100
}, {
id: 3,
img: require('./assets/images/model3.jpg'),
title: 'java入门到放弃',
desc: '精通java的是个步骤',
price: 1200
}, {
id: 4,
img: require('./assets/images/model4.jpg'),
title: 'java入门到放弃',
desc: '精通java的是个步骤',
price: 1300
}],
// tableList: [{
// id: 1,
// img: require('./assets/images/model1.jpg'),
// title: 'java入门到放弃',
// desc: '精通java的是个步骤',
// price: 1000,
// count: 1
// }]
tableList: []
}
// 增加
addCart = (index) => {
let { tableList, productList } = this.state;
// 判断表格中是否有当前这条数据
if (tableList.some(item => item.id === productList[index].id)) {
let currIndex = tableList.findIndex(item => item.id === productList[index].id);
tableList[currIndex].count += 1;
// 方式2
// let current = tableList.filter(item => item.id === productList[index].id);
// current[0].count += 1;
} else {
// 表格中没有这条数据,那么就push进去
tableList.push({
...this.state.productList[index],
count: 1
});
}
this.setState({
tableList
})
}
// 删除
del = (index) => {
let { tableList } = this.state;
if (tableList[index].count > 1) {
tableList[index].count -= 1;
} else {
tableList.splice(index, 1);
}
this.setState({
tableList
})
}
// 小计
// 方式1(推荐)
get totalPrice() {
return this.state.tableList.reduce((sum, next) => {
return sum + (next.price * next.count)
}, 0)
}
// 方式2
getTotalPrice() {
return this.state.tableList.reduce((sum, next) => {
return sum + (next.price * next.count)
}, 0)
}
render() {
return (
<div className='container'>
<h3>产品</h3>
<div className='list'>
{
this.state.productList.map((item, index) => {
return (
<div className='item' key={item.id}>
<img src={item.img} />
<p>{item.title}</p>
<p>{item.desc}</p>
<button onClick={() => this.addCart(index)}>加入购物车</button>
</div>
)
})
}
</div>
<h3>购物车</h3>
<div className='mytable'>
<table>
<thead>
<tr>
<th>编号</th>
<th>图片</th>
<th>标题</th>
<th>价格</th>
<th>数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{
this.state.tableList.map((item, index) => {
return (
<tr key={item.id}>
<td>{item.id}</td>
<td>
<img width={50} src={item.img} />
</td>
<td>{item.title}</td>
<td>{item.price}</td>
<td>{item.count}</td>
<td>
<button onClick={() => this.del(index)}>删除</button>
</td>
</tr>
)
})
}
</tbody>
</table>
</div>
<p>总价:{this.getTotalPrice()}元</p>
</div>
)
}
}