Days 02.React
高级语法
一、课程大纲
1、脚手架:构建SPA
应用的工具
2、组件传值:父子组件传值、子父组件传值、组件之间传值
3、表单处理:单向数据流模式下,完成数据的双向绑定
4、ref
:受控组件和非受控组件
5、生命周期:组件的从创建、加载、运行到销毁的过程
6、核心API
:Fragment、PureComponent
7、综合案例:品牌管理案例,通过弹窗完成数据的新增、编辑
二、课程内容
1、脚手架
官方文档:
https://zh-hans.reactjs.org/docs/create-a-new-react-app.html
create-react-app
:(掌握)创建SPA(Singleton Page Application)
单页面应用
// ① 老版本,安装全局模块
npm install create-react-app
// 创建项目
create-react-app app01
// ② 当前版本:不再支持直接使用create-react-app构建项目
// npx:指定执行某个操作命令,如果指定的命令模块不存在就联网下载
npx create-react-app app01
Next.js
:(了解)创建SSR(Server Side Render)
服务端渲染项目Gatsby
:(了解)创建静态网站项目的脚手架
2、创建项目
① 创建项目
打开命令行,执行命令创建指定的react
项目
-- 过时:不再使用
$ npm install create-react-app // 安装模块
$ create-react-app app01 // 创建项目
-- 推荐使用命令
$ npx create-react-app app01 // 创建指定名称的项目
执行命令创建项目:联网下载create-react-app/react/react-dom...
依赖模块
项目创建成功,并且提示了一些可操作命令
② 启动项目
进入项目目录,执行命令启动项目
$ cd app01
$ npm start
3、项目降级
React
当前市场主流版本@16/@17
版本,脚手架默认添加最新@18
版本
(1) 项目目录结构
项目目录中,了解所有的文件本身的含义,按照需要进行删减和修改
项目目录
|-- app01/
|-- node_modules/
|-- public/
|-- index.html
|-- src/
|-- views/ // 创建,用于存放页面组件的目录
|-- App.jsx // 重命名,App.js -> App.jsx,便于代码提示
|-- App.css // App组件样式文件
|-- index.js // 项目运行入口模块
|-- index.css // 全局样式文件
|-- package-lock.json
|-- package.json
(2) 项目降级
已经完善了文件结构的项目中,需要将@18
版本降级到@17
版本
① 依赖降级,命令行进入项目,安装相关版本的依赖
$ cd app01
// 安装依赖的时候添加--force选项,解决安装依赖模块时出现的冲突问题
$ npm install --force react@17 react-dom@17 -S
② 完善启动模块,@18 / @17
启动模块不兼容
编辑src/index.js
文件,修改启动模块
import React from 'react';
// @18
// import ReactDOM from 'react-dom/client';
// @17
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// @18
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
// <React.StrictMode>
// <App />
// </React.StrictMode>
// );
// @17
ReactDOM.render(
<React.StrictMode>
<App/>
</React.StrictMode>,
document.querySelector("#root")
)
4、组件传值
(1) 认识组件
React SPA
应用中,主要包含的是函数式组件或者类组件,以.js
文件的形式出现,需要注意的是开发期间为了保障开发工具对编写的代码的良好支持,建议修改组件文件.js
为.jsx
创建单页面组件:src/views/HelloComp.jsx
import { Component } from 'react'
export default class HelloComp extends Component {
render() {
return <div>
<h4>单页面(类)组件</h4>
</div>
}
}
编辑入口组件:src/App.jsx
import './App.css';
import HelloComp from './views/HelloComp'
function App() {
return (
<div className="App">
<h2>项目入口</h2>
<h3>1、使用单页面组件</h3>
<!-- 单页组件的使用 -->
<HelloComp></HelloComp>
</div>
);
}
export default App;
(2) 父子组件传值
创建父组件:src/views/Parent/index.jsx
import {Component} from "react"
import Child from "../Child"
export default Parent extends Component {
state = {
parentData: "parent data"
}
render() {
return <div>
<h2>父组件</h2>
<p>父组件数据:{this.state.parentData}</p>
<!-- 可以将数据当成自定义属性,传递给子组件 -->
<Child pmsg={this.state.parentData}/>
</div>
}
}
创建一个子组件:src/views/Child/index.jsx
import {Component} from "react"
export default Child extends Component {
render() {
// 解构父组件传递的数据
let {pmsg} = this.props
return <div>
<h3>子组件</h3>
<p>接受父组件的数据:{pmsg}</p>
</div>
}
}
(3) 子父组件传值
父组件src/views/Parent/index.jsx
中,声明接受数据的变量和接受数据的函数
import {Component} from "react"
import Child from "../Child"
export default Parent extends Component {
state = {
parentData: "parent data",
dataFromChild: '等待接受数据' // 接受数据的变量
}
getData(dat) { // 接受数据的函数
this.setState({
dataFromChild: dat
})
}
render() {
return <div>
<!-- 将接受数据的函数声明,当成自定义属性传递给子组件 -->
<!-- 子组件内部就可以调用执行getData()函数-->
<Child getData={this.getData.bind(this)}/>
</div>
}
}
子组件src/views/Child/index.jsx
中声明数据并完成数据的发送
import {Component} from "react"
export default Child extends Component {
state = {
childData: "child data" // 子组件数据
}
sendData() {
// 调用父组件传递的函数,完成数据传递
this.props.getData(this.state.childData)
}
render() {
return <div>
<p>子组件数据:{this.state.childData}</p>
<button onClick={this.sendData.bind(this)}>发送数据 给父组件</button>
</div>
}
}
(4) 组件之间传值
同一个项目中的不同组件之间,肯定会有直接或者间接的父子嵌套关系,但是相隔较远的组件之间通过父子/子父组件传值方式非常反锁,借助一个事件中心/消息中心的模块,完成数据传递
安装第三方事件模块:
$ npm install events -S
创建消息中心:src/plugins/bus.js
/** 消息中心 */
import Event from 'events'
export default new Event()
创建发送数据的其他组件:src/views/Other/index.jsx
import {Copmonent} from "react"
import events from '../../plugins/bus'
export default class Other extends Component {
state = {
otherData: "其他组件的数据"
}
sendData() {
// 触发自定义事件,完成数据发送
events.emit('dfo-event', this.state.otherData)
}
render() {
return <div>
<button onClick={this.sendData.bind(this)}>发送数据给其他组件</button>
</div>
}
}
负责接受数据的组件:src/views/Child/index.jsx
import {Copmonent} from "react"
import events from '../../plugins/bus'
export default class Child extends Component {
state = {
dataFromOther: "等待接受数据"
}
render() {
// 监听自定义事件,接受数据
events.on('dfo-event', dat=>{
this.setState({
dataFromOther: dat
})
})
return <div>
<p>来自其他组件的数据:{this.state.dataFromOther}</p>
</div>
}
}
5、表单数据处理
React
数据流特征是单向数据流,对表单元素的操作非常不友好,必须结合表单标签的特性,通过value
属性和onChange
事件共同实现表单数据双向绑定的特性
(1) 数据双向绑定
结合表单元素的value
属性和onChange
事件,实现数据在js
和视图view
之间的双向同步
创建表单处理组件:src/views/Forms/index.jsx
import React, { Component } from 'react'
export default class FormsComp extends Component {
state = {
username: '' // 账号数据
}
render() {
return (
<div className="form-container">
<h3>表单处理组件</h3>
<div>
<label htmlFor="username">账号:
</label>
<input type="text" id="username"
value={this.state.username}
onChange={e=> this.setState({username: e.target.value})}/>
<span>record: { this.state.username }</span>
</div>
</div>
)
}
}
(2) 表单元素数据双向绑定
常规的表单元素的数据绑定方式,代码如下:
import React, { Component } from 'react'
export default class FormsComp extends Component {
state = {
username: '', // 账号数据
password: '', // 密码输入框
gender: '男', // 单选框
fav: [], // 复选框
address: '', // 下拉列表框
intro: '', // 文本域
img: '', // 文件域
}
// 复选框 数据双向绑定辅助函数
checkboxFav(value) {
// 某个选项是否存在,存在-> 删除;不存在-> 添加
let newFav = ''
if(this.state.fav.includes(value)) {
newFav = this.state.fav.filter(item => item !== value)
} else {
this.state.fav.push(value)
newFav = [...this.state.fav]
}
// 刷新页面
this.setState({
fav: newFav
})
}
render() {
return (
<div className="form-container">
<h3>表单处理组件</h3>
<div>
<label htmlFor="username">账号:
</label>
<input type="text" id="username"
value={this.state.username}
onChange={e=> this.setState({username: e.target.value})}/>
<span>record: { this.state.username }</span>
</div>
<div>
<label htmlFor="password">密码:
</label>
<input type="password" id="password"
value={this.state.password}
onChange={e=> this.setState({password: e.target.value})}/>
<span>record: { this.state.password }</span>
</div>
<div>
<label htmlFor="gender">性别:
</label>
<input type="radio" id="gender"
value="男" name="gender"
onChange={e=> this.setState({gender: e.target.value})}/>男
<input type="radio" id="gender"
value="女" name="gender"
onChange={e=> this.setState({gender: e.target.value})}/>女
<span>record: { this.state.gender }</span>
</div>
<div>
<label htmlFor="fav">爱好:
</label>
<input type="checkbox" id="fav"
value="编程"
onChange={e=> this.checkboxFav(e.target.value)}/>编程
<input type="checkbox"
value="音乐"
onChange={e=> this.checkboxFav(e.target.value)}/>音乐
<input type="checkbox"
value="电影"
onChange={e=> this.checkboxFav(e.target.value)}/>电影
<input type="checkbox"
value="旅游"
onChange={e=> this.checkboxFav(e.target.value)}/>旅游
<span>record: { this.state.fav }</span>
</div>
<div>
<label htmlFor="addr">地址:
</label>
<select id="addr" onChange={e=> this.setState({address: e.target.value})}>
<option value='郑州'>郑州</option>
<option value='北京'>北京</option>
<option value='杭州'>杭州</option>
<option value='上海'>上海</option>
</select>
<span>record: { this.state.address }</span>
</div>
<div>
<label htmlFor="intro">个人介绍:</label>
<textarea id="intro" cols="30" rows="10"
value={this.state.intro}
onChange={e => this.setState({intro: e.target.value})}></textarea>
<span>record: { this.state.intro }</span>
</div>
{/** 默认提供的value/onChange的形式,无法直接操作文件域中的二进制文件 */}
<div>
<label htmlFor="img">头像:</label>
<input type="file" id="img"
value={this.state.img}
onChange={e => this.setState({img: e.target.value})}/>
<span>record: {this.state.img}</span>
</div>
</div>
)
}
}
6、ref
引用对象
React
提供了一种可以直接获取目标元素对象的操作语法:ref
引用,通过这样的引用对象可以避开React
内建操作函数直接操作数据;语法上提供了三种不同的形式:
- 字符串赋值
- 箭头函数
- 引用组件对象(推荐)
- 官方文档:
https://zh-hans.reactjs.org/docs/refs-and-the-dom.html
代码操作:src/views/RefComp/index.jsx
import React, { Component } from 'react'
export default class RefComp extends Component {
// 创建一个Refs对象
imgRefs = React.createRef()
render() {
return (
<div>
<h3>ref引用组件</h3>
<div>
{/* 1、字符串获取Refs对象的语法,注意-已经声明过时,新项目中尽量避免使用 */}
{/* <input type="file" ref="imgFile"/> */}
{/* 2、回调函数的形式获取Refs对象 */}
{/* <input type="file" ref={dom => this.imgFile = dom}/> */}
{/* 3、推荐- ref属性直接关联一个Refs组件,通过组件访问该标签对象 */}
<input type="file" id="imgFile" ref={this.imgRefs}/>
</div>
<button onClick={this.getRefs.bind(this)}>获取ref对象</button>
</div>
)
}
getRefs() {
// 1、获取字符串形式Refs对象
// console.log(this.refs, "this.refs")
// console.log(this.refs.imgFile.files[0], "Refs获取文件域 文件对象")
// 2、获取回调函数(箭头函数实现)Refs对象
// console.log(this.imgFile)
// console.log(this.imgFile.files[0], 'Refs回调 获取 文件对象')
// 3、Refs组件方式,操作页面标签
console.log(this.imgRefs.current, "imgRefs")
console.log(this.imgRefs.current.files[0], 'Refs组件 获取文件域 文件对象')
}
}
备注:关于ref
的使用情况
1、可以应用在普通标签元素上
2、可以应用在类组件上
3、不可以直接使用在函数式组件上~需要结合辅助函数
HOOKS
来完成ref
对象的操作
- 函数式组件没有实例,所以不能直接使用
ref
获取实例对象
7、受控组件
使用前端框架开发应用时,一般都会要求组件中所有的数据都应该通过框架内建操作函数完成数据的更新,数据的任何变化都是可预测可控制的,方便数据的管理;很少推荐使用DOM
操作直接处理数据,很容易造成数据不可控的修改;
实际项目操作时不可避免的会出现必须通过DOM
操作才能实现数据获取和更新的目的,此时框架就会将数据的不同更新方式区分为两种形式:
- 受控数据:按照内建函数,可预测、可控制的完成数据的赋值、更新、删除等操作
React
中,将任意的元素都封装成组件进行管理,受控数据的操作称为受控组件,一般为了实现数据双向同步,通过value/onChange
内建语法实现订阅模式(可预测、可控制数据更新)
- 非受控数据:对状态数据在视图中的显示标签,进行
DOM
操作实现数据的直接更新React
中,通过ref
管理的组件数据,称为非受控组件
面试题:请简单描述一下什么是受控组件?什么是非受控组件?我们在项目中如何使用他们的?
8、生命周期
生命周期LifeCycle
,描述了一个组件从创建、加载、运行及销毁的整个流程
(1) 早期生命周期图谱
(2) 当前生命周期图谱
(3) 生命周期钩子函数
生命周期钩子函数中,区分挂载、运行和销毁
① 挂载
constructor()
:组件实例构造方法,实例创建的生命周期(掌握)static getDerivedStateFromProps()
:组件状态数据和属性数据准备生命周期UNSAFE_componentWillMount()
:组件DOM更新前生命周期,和getDerivedStateFromProps
互斥render()
:DOM渲染生命周期(掌握)componentDidMount()
:DOM加载完成的生命周期(掌握)
② 运行
static getDerivedStateFromProps()
:组件更新属性数据和状态数据shouldComponentUpdate()
:组件验证更新数据,返回(true:继续更新;false:不需更新)(掌握)UNSAFE_componentWillUpdate()
:DOM数据渲染前执行,和getDerivedStateFromProps
互斥render()
:渲染更新DOM数据(掌握)getSnapshotBeforeUpdate()
:数据更新完成,DOM尚未刷新时调用,一般获取DOM旧记录componentDidUpdate()
:DOM数据渲染完成(掌握)
③ 销毁
componentWillUnmount()
:卸载组件时调用
扩展:容错的生命周期,主要在组件中出现错误时,保障组件的正常运行,避免程序崩溃退出
static getDerivedStateFromError()
componentDidCatch()
:了解
扩展:其他辅助API
setState()
:一般操作state
数据后,需要调用更新页面DOM
数据forceUpdate()
:通过更新的state
数据,对页面DOM
执行强制刷新
扩展:类组件扩展
defaultProps
:给当前组件使用的自定义属性添加默认值,避免组件在使用时父组件未传递数据而导致程序崩溃
代码操作:src/views/Lifecycle/index.jsx
import React, { Component } from 'react'
export default class LifeCycle extends Component {
constructor() { // beforeCreate() / created()
super()
this.state = {
msg: "lifecycle"
}
console.log("1、生命周期constructor执行(掌握)....")
}
// static getDerivedStateFromProps(props, state){
// console.log("2、生命周期getDerivedStateFromProps执行(了解)...")
// console.log('[更新/运行]5、getDerivedStateFromProps生命周期(了解)...')
// return state
// }
shouldComponentUpdate(nextProps, nextState){
console.log('[更新/运行]6、shouldComponentUpdate生命周期...', )
console.log(nextProps, nextState, "shouldComponentUpdate")
if(nextState.msg === this.state.msg) {
console.log('数据未发生改变,无需更新')
return false
}
return true
}
changeMsg() {
this.setState({
msg: "新数据"
})
}
// 说明:getDerivedStateFromProps()一旦声明,下面的生命周期就不能使用;互斥!
// UNSAFE_componentWillMount() { // beforeMount()
// console.log("2、生命周期componentWillMount执行了...")
// }
render() { // beforeMount()
console.log("3、生命周期render 渲染函数执行了(掌握)....")
console.log('[更新/运行]7、render()生命周期...', )
return (
<div>
<h3>生命周期测试</h3>
<p>数据:{this.state.msg}</p>
<button onClick={this.changeMsg.bind(this)}>更新数据</button>
</div>
)
}
// 数据已经改变,但是DOM尚未更新的情况下;获取DOM更新前的特殊数据如滚动条位置
// 需要和compoentDidUpdate()配合使用
// getSnapshotBeforeUpdate(prevProps, prevState){
// console.log('[更新/运行]8、getSnapshotBeforeUpdate()生命周期...', )
// return prevState
// }
// 页面DOM元素更新渲染完成
componentDidUpdate() {
console.log('[更新/运行]9、componentDidUpdate()生命周期...', )
}
// 类似beforeUpdate()
UNSAFE_componentWillUpdate() {
console.log('[更新/运行]UNSAFE_componentWillUpdate()生命周期...', )
}
componentWillReceiveProps(nextProps) {
console.log('[更新/运行]UNSAFE_componentWillReceiveProps()生命周期...', )
return nextProps
}
componentDidMount() { // mounted()
console.log("4、生命周期componentDidMount执行了(掌握)...")
}
}
9、核心api
(1) Fragment
React
组件中,要求返回的JSX
代码片段必须包含一个唯一的根标签
疑问:如果我们将表格中的一部分内容封装成功能组件,如何实现?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sJEi2fax-1657072438887)(assets/image-20220705171134691.png)]
代码操作:
import React, { Component } from 'react'
export default class CoreApi extends Component {
render() {
return (
{/* <React.Fragment >*/}
<>
{/** 需要唯一的一个根标签保障语法正确性,但是这个根标签不能产生任何实际标签对象 */}
{/** React.Fragment 属于一个虚拟根标签 */}
{/** 提供了虚拟根标签简洁语法,直接写标签括号即可,不需要填写任何内容 */}
<h3>核心API</h3>
</>
{/* </React.Fragment >*/}
)
}
}
(2) PureComponent
生命周期中一个shouldComponentUpdate()
,用于判断数据是否需要更新的操作;React
针对一些页面数据经常需要更新的组件,提供了预处理父类PureComonent
,可以检测数据的变化情况
代码操作:src/views/CoreApi/index.jsx
import React, { PureComponent } from 'react'
export default class CoreApi extends PureComponent {
state = {
msg: "old message"
}
changeMsg() {
this.setState({
msg: "new message"
})
}
// shouldComponentUpdate(props, state) {
// if(state.msg === this.state.msg) {
// console.log("数据更新前后一致,不需要更新")
// return false
// }
// return true
// }
render() {
console.log("render() 数据更新,重新刷新DOM页面")
return (
<>
{/** 需要唯一的一个根标签保障语法正确性,但是这个根标签不能产生任何实际标签对象 */}
{/** React.Fragment 属于一个虚拟根标签 */}
{/** 提供了虚拟根标签简洁语法,直接写标签括号即可,不需要填写任何内容 */}
<h3>核心API</h3>
<p>数据:{this.state.msg}</p>
<button onClick={this.changeMsg.bind(this)}>更新数据</button>
</>
)
}
}
面试题:请简要描述
PureComponent
和shouldComponentUpdate()
的作用,说明一下在你的项目中是如何使用他们的?解析:
PureComponent
和shouldComponentUpdate()
都可以用于检测组件的数据是否发生内容更新,PureComponent
直接通过继承方式,让React
辅助完成数据的校验;shouldComponentUpdate()
通过自定义编码的方式针对性的完成数据校验;如果某个组件中大部分数据都需要更新校验一般让组件直接继承PureComponent
实现,如果组件中个别特殊数据需要针对性的处理,可以使用shouldComponentUpdate()
完成精确的数据校验!