day-04
React 组件进阶
children属性
目标
掌握 props 中 children 属性的用法。
内容
- 组件的子节点会被当做是 children 属性传递到子组件内部。
- 在传递数据的时候 children 属性与普通的 prop 一样,值可以是任意类型例如数字、字符串、数组、JSX、函数等。
代码
function Hello(props) {
return <div>该组件的子节点:{props.children}</div>
}
;<Hello children='我是子节点' />
// children 是一个特殊的 prop,上面的写法和下面等价,当内容比较多的时候,下面的写法更加直观
;<Hello>我是子节点</Hello>
props 校验
目标
- 了解为什么需要对 props 进行校验。
- 掌握如何对传递过来的 prop 进行校验。
为什么需要对 props 进行校验
对于组件来说,props 是外来的,无法保证组件使用者传入数据的格式正确,如果传入的数据格式不对,可能会导致组件内部报错,而组件的使用者不能很明确的知道错误的原因。
演示 props 校验的意义
- 校验前
- 校验后
如何对 props 进行校验
- 安装并导入
prop-types
包。 - 使用
组件名.propTypes = {}
来给组件的 props 添加校验规则。 - 校验规则通过
PropTypes
对象来指定。
App.jsx
import React, { Component } from 'react'
import Test from './Test'
export default class App extends Component {
render() {
return (
<div>
<Test colors={'red'} />
</div>
)
}
}
Test.jsx
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class Test extends Component {
render() {
return (
<ul>
{this.props.colors.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}
}
Test.propTypes = {
colors: PropTypes.array,
}
export default Test
ES7中使用方法示例
Test.jsx
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class Test extends Component {
static propTypes = {
colors: PropTypes.array,
}
render() {
return (
<ul>
{this.props.colors.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}
}
export default Test
总结
-
为什么要对 props 进行校验?
-
如何对 props 进行校验?
常见校验规则
目标
了解常见的 props 校验规则。
内容
- 常见类型:number、string、bool、array、func、object。
- React 元素类型(JSX):element。
- 必填项:isRequired。
- 特定结构的对象:shape({})。
{
// 常见类型
fn1: PropTypes.func,
// 必选
fn2: PropTypes.func.isRequired,
// 特定结构的对象
obj: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
}
props 默认值
目标
- 了解指定默认值的好处。
- 掌握给组件的 props 提供默认值的 2 种方式。
内容
通过 defaultProps
可以给组件的 props 设置默认值,在未传入 props 的时候生效。
import React, { Component } from 'react'
class Test extends Component {
render() {
return <div>{this.props.age}</div>
}
}
Test.defaultProps = {
age: 18,
}
export default Test
// 没有传入 pageSize 属性
;<Test />
更建议写法(利用 JS 自身的能力来完成)。
import React, { Component } from 'react'
class Test extends Component {
render() {
//结构之后用等于号表示默认值
const { age = 18 } = this.props
return <div>{age}</div>
}
}
export default Test
小结
- 好处:即便外部不传递也不至于程序报错;简化代码(有可能就是有一些数据是很常用的,这样的话指定默认值外界不需要每次都传递啦)。
- 更建议使用
es6
的写法
类的静态属性
目标
能够通过类的 static 语法简化 props 校验和默认值的写法。
内容
- 实例成员:通过实例才能访问的成员(属性或者方法),叫做实例成员。
- 静态成员:通过类或者构造函数本身才能访问的成员(一般是直接挂载到类上的或者通过 static 关键字定义的)。
class Person {
// 实例成员(通过实例能访问的成员,挂载到实例自身上的)
name = 'zs',
// 静态(通过构造函数或类才能访问到的成员)
static age = 18
// 实例成员(通过实例能访问的成员,挂载到原型上的)
sayHi() {
console.log('哈哈')
}
}
简写
import React, { Component } from 'react'
class Test extends Component {
static defaultProps = {
age: 18,
}
render() {
return <div>{this.props.age}</div>
}
}
export default Test
生命周期概述
目标
- 能够理解什么是生命周期和生命周期函数。
- 能够说出 React 中组件的生命周期总共有几个大的阶段。
内容
- 生命周期:一个事物从创建到最后消亡的整个过程,而组件的生命周期说的就是组件从被创建到挂载到页面中运行,再到组件卸载的过程。
- 意义:学习组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件中问题产生的原因等。
- 生命周期钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。
- 只有类组件才有生命周期。
React 生命周期
小结
-
什么是生命周期?
组件从被创建到挂载到页面中运行,再到组件被卸载的过程。只有类组件有生命周期。
-
React 中生命周期有几个阶段?常用的有几个钩子函数?
- 挂载阶段
constructor
render
componentDidMount
- 更新阶段
render
componentDidUpdate
- 卸载阶段
componentWillUnmount
- 挂载阶段
挂载阶段
目标
- 能够说出挂载阶段的钩子函数有哪几个。
- 掌握执行时机和作用分别是什么。
内容
挂载阶段常用的生命周期函数有 3 个,执行顺序是 constructor => render => componentDidMount。
钩子函数 | 触发时机 | 作用 |
---|---|---|
constructor | 创建组件时,最先执行 | 1. 初始化 state 2. 创建 Ref 等 |
render | 每次组件渲染都会触发 | 渲染 UI(注意: 不能调用 setState() ) |
componentDidMount | 组件挂载(完成 DOM 渲染)后 | 1. 发送网络请求 2.DOM 操作 |
更新阶段
目标
- 能够说出更新阶段的钩子函数有哪几个。
- 掌握执行时机和作用分别是什么。
内容
- 更新阶段常用的生命周期函数有 2 个,执行顺序是 render => componentDidUpdate。
- 触发更新:
setState()
、forceUpdate()
、New props
(父组件进行了 render)。
钩子函数 | 触发时机 | 作用 |
---|---|---|
render | 每次组件渲染都会触发 | 渲染 UI(与挂载阶段是同一个 render) |
componentDidUpdate | 组件更新(完成 DOM 渲染)后 | DOM 操作,可以获取到更新后的 DOM 内容,不要调用 setState |
卸载阶段
目标
- 能够说出组件卸载阶段的钩子函数是什么。
- 明白在卸载阶段的钩子函数里面干什么。
内容
- 触发时机:组件从页面中消失。
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
钩子函数 | 触发时机 | 作用 |
---|---|---|
componentWillUnmount | 组件卸载(从页面中消失) | 执行清理工作(比如:清理定时器等、解绑事件等) |
可以用来清除定时器和解绑事件。
import React, { Component } from 'react'
export default class Test extends Component {
/* constructor() {
super()
this.timer = null
} */
// timer = null
render() {
return <div>Test</div>
}
handleClick() {
console.log(1)
}
componentDidMount() {
// 挂载完毕,指挥执行一次
this.timer = setInterval(() => {
console.log('hello')
}, 1000)
document.addEventListener('click', this.handleClick)
}
componentWillUnmount() {
// !干点啥?
console.log('componentWillUnmount')
// 清除定时器
clearInterval(this.timer)
// 解绑:事件类型一致,事件回调一致
document.removeEventListener('click', this.handleClick)
}
}
setState 更新数据的表现
目标
能够了解 setState 更新数据的表现,异步和同步的场景。
异步表现
- 一般情况下(常见的在生命周期或合成事件处理函数中),通过
setState()
方法来更新数据,表现是异步的。 - 当执行到 setState 这一行的时候,React 出于性能考虑,并不会马上进行调用来修改 state,而是先把这个以及后续的更新对象放到一个更新队列里面进行合并的操作,期间不影响后续代码的执行。
- 多次调用 setState(),只会触发一次重新渲染,所以无需担心多次进行
setState
会带来性能问题。
// 初始
state = { count: 1 }
// 更新
this.setState({
count: this.state.count + 1,
})
// 输出
console.log(this.state.count) // 1
// 通过 DOM 也是不能马上获取的
解释合并
this.setState({
count: this.state.count + 1,
})
this.setState({
count: this.state.count + 2,
})
this.setState({
count: this.state.count + 1,
})
执行过程:先排队,再把排队中的数据进行合并,最后执行 1 次 setState。
同步表现
如果是在 setTimeout/setInterval 或者原生事件的回调中,表现出来是同步的。
App.js
import React, { Component } from 'react'
export default class App extends Component {
state = {
count: 1,
}
componentDidMount() {
setTimeout(() => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 2
})
}
render() {
return <div>{this.state.count}</div>
}
}
原生事件
import React, { Component, createRef } from 'react'
export default class App extends Component {
state = {
count: 1,
}
btnRef = createRef()
componentDidMount() {
this.btnRef.current.onclick = () => {
this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 2
}
}
render() {
return (
<div>
<h2>{this.state.count}</h2>
<button ref={this.btnRef}>click</button>
</div>
)
}
}
另一种同步的表现写法,了解即可!
import React, { Component } from 'react'
export default class App extends Component {
state = {
count: 1,
}
async componentDidMount() {
await this.setState({
count: this.state.count + 1,
})
console.log(this.state.count) // 2
}
render() {
return (
<div>
<h2>{this.state.count}</h2>
</div>
)
}
}
总结
-
setState 更新数据的表现一般是异步的,目的是什么?在哪些场景下更新数据的表现是同步的?
多次调用setState(),只会触发一次重新渲染,可以节省性能。
如果是在setTimeout/setInterval 或者原生事件的回调中,表现出来是同步的。
-
问题/现象:不能立即拿到更新后的数据;多次进行 setState 会进行合并的操作。
setState 推荐语法
目标
- 能够解决上面的两个问题/现象。
- 能够掌握 setState 箭头函数的语法。
第一个问题
通过 setState 第二个参数可以立即拿到更新后的数据。
- 场景:在状态更新后,依靠更新后的状态立即执行某个操作。
- 语法:
setState(updater[, callback])
。
this.setState({}, () => {
console.log('这个回调函数会在状态更新后立即执行')
})
第二个问题
- 推荐:使用
setState((preState) => {})
语法。 - 参数 preState: 上一个
setState
的结果。
// 初始
state = { count: 1 }
// 更新
this.setState((preState) => {
return {
count: preState.count + 1,
}
})
this.setState((preState) => {
return {
count: preState.count + 2,
}
})
// 输出
console.log(this.state.count) // 依然是 1
这种语法依旧是异步的,不同的是通过 preState 可以获取到最新的状态