React基础
写在前面:这是我学习B站黑马React视频前75p所作的笔记。
后续的学习也会整理出笔记,供自己之后翻阅,也希望能给你带来一些帮助。
(1)概述
用于构建用户界面的JavaScript库
(2)特点
- 基于组件
- 学习一次,随处使用
(3)基本安装与使用
-
安装命令
npm i react react-dom
-
使用
// 1.导入react和react-dom文件 import React from 'react' import ReactDOM from 'react-dom' // 2.创建react元素 const title = React.createElement('h1', null, 'hello world!') // 3.渲染react元素 ReactDOM.render(title, document.getElementById('root'))
(4)脚手架安装与使用
-
意义
- 脚手架是开发现代Web应用的必备
- 充分利用Webpack、Babel、ESLint等工具辅助项目开发
- 零配置,无需手动配置繁琐的工具即可使用
- 关注业务,而不是工具配置
-
初始化
npx create-react-app my-app npx 是npm v5.2.0 引入的一条命令 目的是提升包内提供的命令行工具的使用体验; 原来需要先安装脚手架包,再使用这个包中提供的命令;现在无需安装脚手架包,可以直接使用这个包中提供的命令。
-
使用
同上。
启动命令 yarn start 或 npm start
(5)React Developer Tools 相关问题
这是我个人遇到的一些相关问题:
- 安装:不能科学上网时chrome商店打不开,就度娘找找资源,下载然后拖到浏览器扩展应用里会自动安装。
- 不能动态显示数据:我的原因是自己在 component里的Hide components where…加了过滤,去掉过滤就好
- 控制台可能有警告(强迫症难受):把浏览器的迅雷下载组件删了,或者是把浏览器F12的setting下的Preferences里的 Enable JavaScript source maps 和 Enable CSS source maps取消勾选即可
- 插件不可用:在扩展应用的React Developer Tools 的详情里,打开 “允许访问文件网址”
一、基础
(1)JSX的基本使用
用React.createElement()繁琐,不直观,不优雅。
JSX简介
JSX是React的核心内容。是JavaScript XML简写。优势是声明式的语法,更加直观,与HTML结构相同,降低学习成本,提高开发效率。
1.使用步骤
/*
使用JSX创建React元素
*/
const title = (
<h1 className="title">
hello JSX!
<span>这是span</span>
</h1>
)
// 渲染react元素
const root = document.getElementById('root')
ReactDOM.render(title, root)
为什么脚手架中可以使用JSX语法?
-
JSX不是标准的ECMAScript语法,是ECMAScript的语法扩展
-
需要使用babel编译处理后才能在浏览器环境使用
-
create-react-app脚手架中已经默认有该配置,无需手动配置
-
编译JSX语法的包:@babel/preset-react
2.注意点
- React元素的属性使用驼峰命名法
- 特殊属性名: class -> className; for -> htmlFor; tabindex -> tabIndex
- 没有子节点的React元素可以用 /> 结尾
- 推荐:使用小括号包裹JSX,从而避免JS中自动插入分号陷阱
3.JSX中使用JavaScript表达式
/*
JSX中使用JavaScript表达式
*/
const name = 'Tom'
const age = 19
const title = (
<h1>
hello {name} ! 年龄: {age}
</h1>
)
ReactDOM.render(title, document.getElementById('root'))
/*
条件渲染
*/
const isLoading = false
const loadData = () => {
if (isLoading) {
return (<div>loading...</div>)
}
return (<div>数据加载完成,此处显示加载后的数据</div>)
}
// 三元表达式
const loadData = () => {
return isLoading ? (<div>
loading...
</div>) : (<div>
数据加载完成
</div>)
}
// 逻辑运算符
const loadData = () => {
return isLoading && (<div>
loading...
</div>)
}
const load = (
<h1>
条件渲染:
{loadData()}
</h1>
)
ReactDOM.render(load, document.getElementById('root'))
4.JSX列表渲染
/*
列表渲染
*/
const songs = [
{ id: 1, name: '痴心绝对'},
{ id: 2, name: '阿拉斯加的海湾'},
{ id: 3, name: '长安姑娘'},
]
const list = (
<ul>
{songs.map(item => {
return (
<li key={item.id}>{item.name}</li>
)
})}
</ul>
)
ReactDOM.render(list, document.getElementById('root'))
5.JSX样式处理
// 类名,外部引入
import './index.css'
// 行内样式
const list = (
<h1 className="title" style={{color: 'skyblue', backgroundColor: 'pink'}}>
JSX的样式处理
</h1>
)
ReactDOM.render(list, document.getElementById('root'))
总结
- JSX是React的核心内容
- JSX表示在JS代码中写HTML结构,是React声明式的体现
- 使用JSX配合嵌入的JS表达式、条件渲染、列表渲染、可以描述任意UI结构
- 推荐使用类名给JSX添加样式
- React完全利用JS语言自身的能力来编写UI,而不是造轮子增强HTML功能
(2)React组件
组件介绍
- 组件是React的一等公民,使用React就是在用组件
- 组件表示页面中的部分功能
- 组合多个组件实现完整的页面功能
- 特点:可复用、独立、可组合
1.创建方式
- 使用函数创建
- 使用类创建
- 抽离为独立的JS文件
2.React事件处理
import React from 'react'
/*
React事件处理
*/
// 使用类组件形式
class App extends React.Component {
// 事件处理程序
handleClick() {
console.log('单击事件触发了')
}
render() {
return (
<button type="" onClick={this.handleClick}>点我</button>
)
}
}
// 使用函数组件形式
function App() {
function handleClick() {
console.log('函数组件的点击事件被触发了')
}
return (
// 和类组件相比,就没有this,可以直接通过函数名
<button type="" onClick={handleClick}>函数组件-点我</button>
)
}
export default App
3.React事件对象
import React from 'react'
/*
React事件对象 (合成事件)
*/
// 使用类组件形式
class App extends React.Component {
// 事件处理程序
handleClick(e) {
// 阻止默认行为
e.preventDefault()
console.log('a标签单击事件触发了')
console.log('事件对象:', e)
}
render() {
return (
<a href="https://www.bilibili.com" onClick={this.handleClick}>B站</a>
)
}}
export default App
4.有状态组件/无状态组件
import React from 'react'
/*
React有无状态组件
*/
// 函数组件又叫做无状态组件,类组件又叫做有状态组件
// 状态(state)即数据
// 函数组件没有自己的状态,只负责数据展示(静)
// 类组件有自己的状态,负责更新UI,让页面“动起来”
/*
state的基本使用
*/
// 状态是可以改变的,通过setState({要修改的数据})来修改数据;不能直接修改state中的值
// setState() 1.修改状态 2.更新UI
// 数据驱动的思想
// 使用类组件形式
class App extends React.Component {
// constructor() {
// super()
// // 初始化状态
// this.state = {
// count: 0
// }
// }
// 简化语法初始化state
state = {
count: 0,
test: 'a'
}
// render() {
// return (
// <div>
// <h1>计数器:{this.state.count}</h1>
// <button type="" onClick={
// () => {
// 这里的this指向外部环境,即render()方法,就是当前组件的实例,所以可以正常执行
// this.setState({
// count: this.state.count + 1
// })
// }
// }>+1</button>
// </div>
// )
// }
// 抽离逻辑代码
// 解决this指向问题
// 1.箭头函数
// 2.Function.prototype.bind()
// 3.class的实例方法(也是利用箭头函数this指向的特点)
// 利用ES5的bind方法,将事件处理程序中的this与组件实例绑定到一起
// constructor() {
// constructor里的this也是指向实例
// super()
// state也可以放在这里
// this.state = {
// count: 0
// }
// this.onIncrease = this.onIncrease.bind(this)
// }
// 事件处理程序
// onIncrease() {
// this.setState({
// count: this.state.count + 1
// })
// }
// 第三种改变this指向的方法
onIncrease = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<h1>计数器:{this.state.count}</h1>
{/*箭头函数本身不绑定this,这里的this指向外部环境,即render()方法,就是当前组件的实例,所以可以正常执行 */}
{/* <button type="" onClick={() => this.onIncrease()}>点我+1</button> */}
<button type="" onClick={this.onIncrease}>+1</button>
</div>
)
}
}
export default App
5.表单处理-受控组件
import React from 'react'
/*
React表单处理
*/
/*
1.受控组件:受到React控制的表单元素
HTML中表单元素是可输入的,也就是有自己可变的状态,而React中可变状态通常保存在state中,并且只能通过setState()方法来修改;React将state与表单元素值value绑定到一起,由state的值来控制表单元素的值
*/
// 步骤
// 1.在state中添加一个状态,作为表单元素的value值
// 2.给表单元素绑定change事件,将表单元素的值设置为state的值(控制表单元素值的变化)
// 问题:每个表单元素都有一个单独的事件处理程序
// 优化步骤
// 1.给表单元素添加name属性,名称与state相同
// 2.根据表单元素类型获取对应值(有些表单是value,有的是checked)
// 3.在change事件处理程序中通过[name]来修改对应的state
// 使用类组件形式
class App extends React.Component {
// 简化语法初始化state
state = {
txt: '',
content: '',
city: 'bj',
isChecked: false
}
handleChange = (e) => {
this.setState({
txt: e.target.value
})
}
// 处理富文本框
handleContent = e => {
this.setState({
content: e.target.value
})
}
// 处理下拉框
handleCity = e => {
this.setState({
city: e.target.value
})
}
// 处理复选框
// handleCheckBox = e => {
// this.setState({
// isChecked: e.target.checked
// })
// }
/*
事件处理程序优化
*/
handleAllChange = e => {
// 获取当前DOM对象
const target = e.target
// 根据类型判断获取值
const value = target.type === 'checkbox' ? target.checked : target.value
// 获取name
const name = target.name
this.setState({
[name]: value
})
}
render() {
return (
<div>
{/* 文本框 */}
{/* <input type="text" name="txt" value={this.state.txt} onChange={this.handleChange} /> */}
<input type="text" name="txt" value={this.state.txt} onChange={this.handleAllChange} />
{/* 富文本框 */}
{/* <textarea rows="" cols="" name="content" value={this.state.content} onChange={this.handleContent}></textarea> */}
<textarea rows="" cols="" name="content" value={this.state.content} onChange={this.handleAllChange}></textarea>
{/* 下拉框 */}
{/* <select name="city" value={this.state.city} onChange={this.handleCity}> */}
<select name="city" value={this.state.city} onChange={this.handleAllChange}>
<option value="sh">上海</option>
<option value="bj">北京</option>
<option value="wh">武汉</option>
</select>
<br/>
{/* 复选框 */}
{/* <input name="isChecked" type="checkbox" checked={this.state.isChecked} onChange={this.handleCheckBox}/> */}
<input name="isChecked" type="checkbox" checked={this.state.isChecked} onChange={this.handleAllChange}/>
</div>
)
}
}
export default App
6.表单处理-非受控组件
import React from 'react'
/*
2.非受控组件(不推荐)
借助ref,使用原生DOM方式来获取表单元素值
ref的作用:获取DOM或组件
*/
// 使用步骤
// 1.使用React.createRef()方法创建一个ref对象
// 2.将创建好的ref对象添加到文本框中(关联表单元素)
// 3.通过ref对象获取到文本框的值
// 使用类组件形式
class App extends React.Component {
constructor() {
super()
// 创建ref
this.txtRef = React.createRef()
}
// 获取文本框的值
getTxt = () => {
console.log('文本框的值为:', this.txtRef.current.value)
}
render() {
return (
<div>
<input type="text" ref={this.txtRef} />
<button type="" onClick={this.getTxt}>获取文本框的值</button>
</div>
)
}
}
export default App
总结
- 组件的两种创建方式:函数组件和类组件
- 无状态(函数)组件负责静态结构展示
- 有状态(类)组件负责更新UI,让页面动起来
- 绑定事件注意this指向问题
- 推荐使用受控组件来处理表单
- 完全利用JS语言的能力创建组件,这是React的思想
二、进阶
(1)React组件进阶
组件通讯介绍
组件是独立且封闭的单元,默认情况下只能使用组件自己的数据。组件化过程中多个组件之间不可避免的要共享某些数据。为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯。
1.组件的props
- props作用:接收传递给组件的数据
- 传递数据:给组件标签添加属性
- 接收数据:函数组件通过参数props接收数据,类组件通过this.props接收数据
import React from 'react'
// import ReactDOM from 'react-dom'
/*
props
*/
// 2.函数组件接收数据
// const App = (props) => {
// // props是一个对象
// console.log(props)
// return (
// <div>
// <h1>props: {props.name}</h1>
// </div>
// )
// }
// 类组件接收数据
class App extends React.Component {
// 推荐使用props作为构造函数参数
constructor(props) {
super(props)
console.log(props)
}
render() {
return (
<div>
<h1>props: {this.props.name}</h1>
</div>
)
}
}
// 1.传递数据
// ReactDOM.render(<App name="Tom" age={19}></App>, document.getElementById('root'))
特点
- 可以给组件传递任意类型的数据(字符串“ ”,非字符串直接用 { } 包裹;可以传数字、数组、函数、jsx)
- props是只读的对象,只能读取属性的值,无法修改对象
- 使用类组件时,如果写了构造函数,应该将props传递给super(),否则无法在构造函数中获取到props
2.组件通讯的三种方式
2.1 父组件传给子组件
-
父组件提供要传递的state数据
-
给子组件标签添加属性,值为state中的数据
-
子组件中通过props接收父组件中传递的数据
import React from 'react'
/*
组件通讯的三种方式
*/
// 父组件传给子组件
// 父组件
class Parent extends React.Component {
state = {
lastName: "wang"
}
render() {
return (
<div className="parent">
父组件:
<Child name={this.state.lastName}></Child>
</div>
)
}
}
// 子组件
const Child = (props) => {
return (
<div className="child">
<p>子组件接收到父组件的数据:{props.name}</p>
</div>
)
}
export default Parent
2.2 子组件传给父组件
**思路:**利用回调函数,父组件提供回调函数,子组件调用,将要传递的数据作为回调函数的参数。(和Vue是一样的)
**注意:**回调函数this指向的问题
- 父组件提供一个回调函数(用于接收数据)
- 将该函数作为属性的值,传递给子组件
- 子组件通过props调用回调函数
- 将子组件的数据作为参数传递给回调函数
import React from 'react'
/*
组件通讯的三种方式
*/
// 子组件传给父组件
// 父组件
class Parent extends React.Component {
state = {
parentMsg: ''
}
// 提供回调函数,用来接收数据
getChildMsg = (data) => {
console.log('接收到子组件中传递过来的数据:', data)
this.setState({
parentMsg: data
})
}
render() {
return (
<div className="parent">
父组件:{this.state.parentMsg}
<Child getMsg={this.getChildMsg}></Child>
</div>
)
}
}
// 子组件
class Child extends React.Component {
state = {
msg: "呷哺呷哺"
}
handleClick = () => {
// 子组件调用父组件传递过来的回调函数
this.props.getMsg(this.state.msg)
}
render() {
return (
<div className="child">
子组件:<button onClick={this.handleClick}>点我,给父组件传递数据</button>
</div>
)
}
}
export default Parent
2.3 兄弟组件之间通讯
**思路:**将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态(状态提升)
- 公共父组件职责:1.提供共享状态;2.提供操作共享状态的方法
- 要通讯的子组件只要通过props接收状态或操作状态的方法
import React from 'react'
/*
组件通讯的三种方式
*/
/*
兄弟组件之间通讯
*/
// 公共父组件
class Parent extends React.Component {
// 提供共享状态
state = {
count: 0
}
// 提供修改共享状态的方法
onIncrease = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<Child1 count={this.state.count}/>
<Child2 onIncrease={this.onIncrease}/>
</div>
)
}
}
// 子组件1
const Child1 = (props) => {
return (
<h1>计数器:{props.count}</h1>
)
}
// 子组件2
const Child2 = (props) => {
return (
<button onClick={() => props.onIncrease()}>点我+1</button>
)
}
export default Parent
3.Context
**作用:**跨组件传递数据(比如:主题、语言等)
使用步骤:
-
调用 React.createContext() 创建 Provider(提供数据)和 Consumer(消费数据)两个组件
-
使用 Provider 组件作为父节点
-
设置value,表示要传递的数据
-
使用Consumer组件接收数据
import React from 'react'
/*
Context
*/
// 1.创建context得到两个组件
const {Provider, Consumer} = React.createContext()
class App extends React.Component {
render() {
return (
// 2.使用 Provider 组件作为父节点
// 3.设置value,表示要传递的数据
<Provider value="pink">
<div>
<Node/>
</div>
</Provider>
)
}
}
const Node = (props) => {
return (
<div>
我是Node节点
<SubNode/>
</div>
)
}
const SubNode = (props) => {
return (
<div>
我是SubNode节点
<Child/>
</div>
)
}
const Child = (props) => {
return (
<div>
我是Child节点
{/* 4.使用Consumer组件接收数据 */}
<Consumer>
{
data => <h1>传递来的值 -- {data}</h1>
}
</Consumer>
</div>
)
}
export default App
总结:
- 如果两个组件是”远房亲戚“(比如多层嵌套)可以使用Context实现组件通讯
- Context提供了两个组件:Provider和Consumer
- Provider组件:用来提供数据
- Consumer组件:用来消费数据
4.props深入
4.1 children属性
-
children属性:表示组件标签的子节点。当组件标签有子节点时,props就会有该属性
-
children属性与普通的props一样,值可以是任意值(文本、React元素、组件,甚至是函数)
/*
children属性
*/
/* children为文本节点 */
// const App = (props) => {
// console.log(props)
// return (
// <div>
// <h1>组件标签的子节点:</h1>
// {props.children}
// </div>
// )
// }
// ReactDOM.render(<App>我是子节点</App>, document.getElementById('root'))
/* children为JSX或组件 */
// const App = (props) => {
// console.log(props)
// return (
// <div>
// <h1>组件标签的子节点:</h1>
// {props.children}
// </div>
// )
// }
// ReactDOM.render(<App><p>我是子节点,是一个p标签</p></App>, document.getElementById('root'))
// const Test = () => <button>我是按钮组件</button>
// ReactDOM.render(<App><Test/></App>, document.getElementById('root'))
/* children为函数 */
const App = (props) => {
console.log(props)
return (
<div>
<h1>组件标签的子节点:</h1>
{props.children()}
</div>
)
}
// ReactDOM.render(<App>{ () => console.log('这是一个函数子节点')}</App>, document.getElementById('root'))
4.2 props校验
-
对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据
-
如果传入的数据格式不对,可能会导致组件内部报错
-
关键问题:组件的使用者不知道明确的错误原因
-
props校验:允许在创建组件的时候,就指定props的类型、格式等
-
作用:捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性
使用步骤:
-
安装包:prop-types (yarn add prop-types / npm i prop-types)
-
导入prop-types包
-
使用 组件名.propTypes = {} 来给组件的props添加校验规则
-
校验规则通过 PropTypes 对象来指定
/* props校验 */ // 导入 import PropTypes from 'prop-types' const App = (props) => { const arr = props.colors const lis = arr.map((item, index) => { return ( <li key={index}>{item}</li> ) }) return ( <ul> {lis} </ul> ) } // 添加props校验 App.propTypes = { colors: PropTypes.array } // ReactDOM.render(<App colors={19}></App>, document.getElementById('root')) // 会有明确的错误提示 // Warning: Failed prop type: Invalid prop `colors` of type `number` supplied to `App`, expected `array`. // ReactDOM.render(<App colors={['red', 'blue']}></App>, document.getElementById('root'))
约束规则:
-
常见类型:array、bool、func、number、object、string
-
React元素类型:element
-
必填项:isRequired
-
特定结构的对象:shape({ })
-
import React from 'react'
/*
props校验-约束规则
*/
// 导入
import PropTypes from 'prop-types'
const App = (props) => {
return (
<div>
<h1>props校验:</h1>
</div>
)
}
/*
添加props校验
属性 a 的类型:数值(number)
属性 fn 的类型:函数(func)并且为必填项
属性 tag 的类型:React元素(element)
属性 filter 的类型:对象({ area:'上海', price: 1999})
*/
App.propTypes = {
a: PropTypes.number,
fn: PropTypes.func.isRequired,
tag: PropTypes.element,
filter: PropTypes.shape({
area: PropTypes.string,
price: PropTypes.number
})
}
export default App
```
###### 4.3 props的默认值
- 场景:分页组件 → 每页显示条数
- 作用:给props设置默认值,在未传入props时生效
```jsx
import React from 'react'
/*
props的默认值
*/
const App = (props) => {
console.log(props)
return (
<div>
<h1>props的默认值:{props.pageSize}</h1>
</div>
)
}
// 添加props默认值
App.defaultProps = {
pageSize: 10
}
// ReactDOM.render(<App pageSize={20}></App>, document.getElementById('root'))
// 这是显示的就是 20 而不是 10
export default App
组件的生命周期
1. 概述
- 意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能,分析组件错误原因等
- 组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程
- 生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数
- 钩子函数作用:为开发人员在不同阶段操作组件提供了时机
- 只有类组件才有生命周期
2. 生命周期的三个阶段
- 每个阶段的执行时机
- 每个阶段钩子函数的执行顺序
- 每个阶段钩子函数的作用
-
创建时(挂载阶段)
-
执行时机:组件创建时(页面加载时)
-
执行顺序:constructor() → render() → componentDidMount()
钩子函数 触发时机 作用 constructor 创建组件时,最先执行 1.初始化state;2.为事件处理程序绑定this render 每次组件渲染都会触发 渲染UI(注意:不能调用setState() ) componentDidMount 组件挂载(完成DOM渲染后) 1.发送网络请求;2.DOM操作
-
import React from 'react'
/*
组件生命周期
*/
// 创建时(挂载阶段)
class App extends React.Component {
constructor() {
super()
// 初始化state
this.state = {
count: 0
}
// 处理this绑定问题
// const title = document.getElementById('title')
// 此时DOM还没渲染,拿不到DOM
// console.log(title) 结果:null
console.warn('生命周期钩子函数: 1.constructor')
}
componentDidMount() {
// const title = document.getElementById('title')
// 1.此时已经完成DOM渲染,可以拿到DOM,进行DOM操作
// console.log(title)
// 2.发送AJAX请求,获取远程数据
// axios.get('http://api....')
console.warn('生命周期钩子函数: 3.componentDidMount')
}
render() {
// 在render()里调用this.setState()会造成递归更新
console.warn('生命周期钩子函数: 2.render')
// 渲染返回内容
return (
<div>
<h1 id="title">统计豆豆被打的次数:</h1>
<button id="btn">打豆豆</button>
</div>
)
}
}
export default App
-
更新时(更新阶段)
-
执行时机:1.setState(); 2.forceUpdate(); 3.组件接收到新的props
-
说明:以上三者任意一种变化,组件都会重新渲染
-
执行顺序:render() → componentDidUpdate()
钩子函数 触发时机 作用 render 每次组件渲染都会触发 渲染UI(与 挂载阶段 是同一个render) componentDidUpdate 组件更新(完成DOM渲染)后 1.发送网络请求;2.DOM操作;注意:如果要setState()必须放在一个if条件中 import React from 'react' /* 组件生命周期 */ // 更新时(更新阶段) class App extends React.Component { constructor(props) { super(props) // 初始化state this.state = { count: 0 } } handleClick = () => { // 1.调用setState,此时父组件会重新渲染 this.setState({ count: this.state.count + 1 }) // 3.演示强制更新,即使属性没有更新也会强制更新 // this.forceUpdate() } render() { return ( <div> {/* 2.接收到新属性,子组件也会更新 */} <Counter count={this.state.count}></Counter> <button id="btn" onClick={this.handleClick}>打豆豆</button> </div> ) } } class Counter extends React.Component { render() { // 2.接收到新属性,子组件也会更新 console.warn('子组件——生命周期钩子函数: render') return ( <h1 id="title">统计豆豆被打的次数:{this.props.count}</h1> ) } componentDidUpdate(prevProps) { console.warn('子组件——生命周期钩子函数: componentDidUpdate') // 可以获取DOM // const title = document.getElementById('title') // console.log(title.innerHTML) // 注意:如果要调用setState()更新状态,必须要放在if条件中。否则也会造成递归更新 // 原因:如果直接调用setState()更新状态,会再去执行render(),render()执行完会立即去执行componentDidUpdate(),从而造成递归调用。 // 做法:比较更新前后的props是否相同,来决定是否重新渲染组件 console.log('prevProps: ', prevProps, '--currentProps: ', this.props) if (prevProps !== this.props) { this.setState({}) // 发送ajax请求 } /* 这里点击“打豆豆”按钮,会打印两次 结果: prevProps: {count: 0} --currentProps: {count: 1} prevProps: {count: 1} --currentProps: {count: 1} 原因:第一次是点击了按钮,父组件状态更新,子组件props发生变化,钩子函数顺序执行到componentDidUpdate(),第二次是因为走了if里面的语句,调用完setState后,重新render后又要立即去执行componentDidUpdate(),只不过此时没有改变父组件状态,所以前后props是一样的 */ } } export default App
-
-
卸载时(卸载阶段)
-
执行时机:组件从页面中消失
-
钩子函数componentWillUnmount()
钩子函数 触发时机 作用 componentWillUnmount 组件卸载(从页面中消失) 执行清理工作(比如:清理定时器等) import React from 'react' /* 组件生命周期 */ // 卸载时(卸载阶段) class App extends React.Component { constructor(props) { super(props) // 初始化state this.state = { count: 0 } } handleClick = () => { this.setState({ count: this.state.count + 1 }) } render() { return ( <div> { this.state.count > 3 ? <p>豆豆被打西内了</p> : <Counter count={this.state.count}></Counter> } <button id="btn" onClick={this.handleClick}>打豆豆</button> </div> ) } } class Counter extends React.Component { componentDidMount() { // 开启定时器 this.timerId = setInterval(() => { console.log('定时器正在执行~') }, 1000) } render() { return ( <h1 id="title">统计豆豆被打的次数:{this.props.count}</h1> ) } componentWillUnmount() { console.warn('生命周期钩子函数:componentWillUnmount') // 清理定时器 clearInterval(this.timerId) } } export default App
-
3. 不常用钩子函数
**介绍:**react库不断更新,新老不匹配,移除旧的不常用钩子函数,加入新的钩子函数
旧版的生命周期,创建和更新的componentWillxxx不用了
新版完整的生命周期,这两个新的getxxxx也不是很推荐使用
render-props和高阶组件
1. React组件复用
-
概述
- 思考:如果两个组件中的部分功能相似或相同,该如何处理?
- 处理方式:复用相似的功能(联想函数封装)
- 复用什么?1.state 2.操作state的方法(组件状态逻辑)
- 两种方式:1.render props模式 2.高阶组件(HOC:high order component)
- 注意:这两种模式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)
2. render props模式
(只是个名字,不一定就是使用 render)
context其实用的就是render props模式,而且是children属性
思路分析
- 思路:将要复用的state和操作state的方法封装到一个组件中
- 问题1:如何拿到该组件中复用的state?
- 在使用组件时,添加一个值为函数的prop,通过函数参数来获取(需要组件内部实现)
- 问题2:如何渲染任意的UI?
- 使用该函数的返回值作为要渲染的UI内容(需要组件内部实现)
使用步骤
-
创建Mouse组件,在组件中提供复用的状态逻辑代码(1.状态;2.操作状态的方法)
-
将要复用的状态作为props.render(state)方法的参数,暴露到组件外部
-
使用props.render()的返回值作为要渲染的内容
import React from 'react' import PropTypes from 'prop-types' /* render props模式 */ // 导入图片资源 const catImgSrc = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.qiantucdn.com%2F58pic%2F27%2F51%2F73%2F30V58PICg4M_1024.jpg%21%2Ffw%2F780%2Fwatermark%2Furl%2FL3dhdGVybWFyay12MS4zLnBuZw%3D%3D%2Falign%2Fcenter%2Fcrop%2F0x1009a0a0&refer=http%3A%2F%2Fpic.qiantucdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1634716498&t=8cf797a6f75a9a570b1c760dc682c9e5" // 创建Mouse组件 class Mouse extends React.Component { // 鼠标位置state(状态) state = { x: 0, y: 0 } // 鼠标移动处理程序(操作状态的方法) handleMouseMove = (e) => { this.setState({ x: e.clientX, y: e.clientY }) } // 监听鼠标移动事件 componentDidMount() { window.addEventListener('mousemove', this.handleMouseMove) } // 在组件卸载时移除事件绑定 componentWillUnmount() { window.removeEventListener('mousemove', this.handleMouseMove) } render() { // return null // 没有渲染任何ui结构 // return this.props.render(this.state) // 用children属性替换render return this.props.children(this.state) } } // 添加props校验 Mouse.propTypes = { children: PropTypes.func.isRequired } class App extends React.Component { constructor(props) { super(props) console.log(props) } render() { return ( <div> <h1>render props 模式</h1> {/* 这里的mouse形参,拿到的就是上面Mouse组件的this.state */} {/* <Mouse render={(mouse) => { // 可以拿到状态,具体展示形式就可以在使用组件的地方自行定义 return <p>鼠标位置:{mouse.x},{mouse.y}</p> }}></Mouse> */} {/* 猫捉老鼠 */} {/* <Mouse render={(mouse) => { return <img src={catImgSrc} alt="猫" width={100} style={{ position: 'absolute', top: mouse.y - 50, left: mouse.x - 50 }}/> }}></Mouse> */} {/* children属性代替render */} <Mouse> { (mouse) => { return <p>children拿到鼠标位置:{mouse.x},{mouse.y}</p> } } </Mouse> {/* children属性代替render */} {/* 猫捉老鼠 */} <Mouse> { (mouse) => { return <img src={catImgSrc} alt="猫" width={100} style={{ position: 'absolute', top: mouse.y - 50, left: mouse.x - 50 }}/> } } </Mouse> </div> ) } } export default App
代码优化
- 推荐:给render props模式添加 props校验
- 应该在组件卸载时解除mousemove时间绑定
3. 高阶组件
概述
- 目的:实现状态逻辑复用
- 采用包装(装饰)模式,比如说:手机壳
- 手机:获取保护功能
- 手机壳:提供保护功能
- 通过包装组件,增强组件功能
思路分析
- 高阶组件(HOC,Higher-Order Component)是一个函数,接收要包装的组件,返回增强后的组件。
- 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给包装组件WrappedComponent
使用步骤
- 创建一个函数,名称约定以with开头
- 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
- 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
- 在该组件中,渲染参数组件,同时将状态通过props传递给参数组件
- 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中。
设置displayName
- 使用高阶组件存在的问题: 得到的两个组件名称相同
- 原因:默认情况下,React使用组件名称作为displayName
- 解决方式:为高阶组件设置displayName便于调试时区分不同的组件
- displayName的作用:用于设置调试信息(React Developer Tools信息)
传递props
- 问题:props丢失
- 原因:高阶组件没有往下传递props
- 解决方式:渲染WrappedComponent时,将state和this.props一起传递给组件
import React from 'react'
/*
高阶组件
*/
// 导入图片资源
const catImgSrc = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.qiantucdn.com%2F58pic%2F27%2F51%2F73%2F30V58PICg4M_1024.jpg%21%2Ffw%2F780%2Fwatermark%2Furl%2FL3dhdGVybWFyay12MS4zLnBuZw%3D%3D%2Falign%2Fcenter%2Fcrop%2F0x1009a0a0&refer=http%3A%2F%2Fpic.qiantucdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1634716498&t=8cf797a6f75a9a570b1c760dc682c9e5"
// 创建高阶组件函数
function withMouse(WrappedComponent) {
// 该组件提供复用的状态逻辑
class Mouse extends React.Component {
// 鼠标状态
state = {
x: 0,
y: 0
}
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 控制鼠标状态逻辑
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
console.log('Mouse: ', this.props)
// 将state和this.props一起传递给组件
return <WrappedComponent {...this.state} {...this.props}></WrappedComponent>
}
}
// 设置displayName
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
return Mouse
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// 用来测试高阶组件
const Position = props => {
console.log('Position: ', props)
return (
<p>
鼠标当前位置:(x: {props.x}, y: {props.y})
</p>
)
}
// 猫捉老鼠组件
const Cat = props => (
<img
src={catImgSrc}
alt="cat"
width={100}
style={{
position: 'absolute',
top: props.y - 50,
left: props.x - 50
}}
/>
)
// 获取增强后的组件
const MousePosition = withMouse(Position)
const MouseCat = withMouse(Cat)
class App extends React.Component {
constructor(props) {
super(props)
console.log(props)
}
render() {
return (
<div>
<h1>高阶组件</h1>
{/* 渲染高阶组件 */}
{/* 这里的a="1"相当于是传给了高阶函数里的Mouse组件 */}
<MousePosition a="1"></MousePosition>
<MouseCat></MouseCat>
</div>
)
}
}
export default App
总结
- 组件通讯是构建React应用必不可少的一环
- props的灵活性让组件更加强大
- 状态提升是React组件的常用模式
- 组件生命周期有助于理解组件的运行过程
- 钩子函数让开发者可以在特定的时机执行某些功能
- render props模式和高阶组件都可以实现组件状态逻辑复用
- 组件极简模型:(state, props)=> UI