携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
React组件进阶
- children属性
- props校验
- props默认值
- 生命周期与钩子函数
props校验-基本使用
目标
校验接收的props的数据类型,增加组件的健壮性
类型校验的必要性
对于子组件来说,props是外来的,无法保证组件使用者传入什么格式的数据,有了类型校验,我们的程序就更加健壮了。
步骤
-
导入
prop-types包 。这个包在脚手架创建项目时就自带了,无须额外安装import PropTypes from 'prop-types'这里的PropTypes可以改成其他的名字 -
使用
组件名.propTypes = {属性名1: 类型1, ...}来给组件的props添加校验规则这里的propTypes是固定写法
示例
import PropTypes from 'prop-types'
class App extends React.component {
render(){
return (
<h1>Hi, {props.colors}</h1>
)
}
}
App.propTypes = {
// 约定colors属性为array类型
// 如果类型不对,则报出明确错误,便于分析错误原因
colors: PropTypes.array
}
作用:规定接收的props的类型必须为数组,如果不是数组就会报错,报错的格式如下:
小结
-
为啥要对props进行校验?
-
步骤是:
- 导入 __ _包
- 设置格式: __ ___
props校验-常见规则
目标
了解react组件props校验的常见规则
内容:
- 常见类型:array、bool、func、number、object、string
- React元素类型:element
- 必填项:isRequired
- 特定结构的对象:shape({ })
示例
Com.propTypes = {
fn: PropTypes.func, // fn 是函数类型
isEdit: PropTypes:bool.isRequired // isEdit的类型是bool,且必须要传入
// 特定结构的对象
goodInfo: PropTypes.shape({ // goodInfo的格式一个对象,有 数值price属性 和 字符串name属性
price: PropTypes.number,
name: PropTypes.string
})
}
props默认值(了解)
目标
掌握给组件的props提供默认值的两种方法
默认值
定义: 没有赋值的情况下,给属性赋一个值
好处:简化调用组件时要传入的属性,更方便用户使用
两种方式
两种方式:
defaultProps- 解构赋值的默认值
方法1:defaultProps
通过defaultProps可以给组件的props设置默认值,在未传入props的时候生效
定义组件
class App extends React.Component{
return (
<div>
此处展示props的默认值:{ this.props.pageSize }
</div>
)
}
// 设置默认值
App.defaultProps = {
pageSize: 10
}
// 类型限制
App.propTypes = {
pageSize: PropTypes.number
}
使用组件
// 不传入pageSize属性
<App />
方法2:解构赋值的默认值
class App extends React.Component{
return (
// 解构赋值的默认值
const { pageSize = 10} = this.props
<div>
此处展示props的默认值:{ this.props.pageSize }
</div>
)
}
// 类型限制
App.propTypes = {
pageSize: PropTypes.number
}
小结
props校验和默认值简化-类的静态成员-static(了解)
目标
了解静态属性的概念和特点,能用它来简化props校验和默认值
什么是静态成员
-
静态成员:通过类或者构造函数本身才能访问的属性或者方法
-
实例成员: 通过实例调用的属性或者方法
示例
class Person { constructor(name){ this.name = name // 实例成员 } static n = 1 // 静态成员 有static修饰 }
静态成员的定义和访问
定义
class Person {
constructor(name){
this.name = name // 实例成员
}
// 方法1:在class内部 通过static来定义
static n = 1 // 静态成员 有static修饰
}
// 方法2 在class外部 通过类型.属性名来定义
Person.m = function() { console.log()}
访问
Person.n
Person.m()
简化props校验和默认值
把校验规则和默认值放入class内部
组件生命周期-概述
目标
能够理解什么是组件的生命周期
生命周期
生命周期:一个事物从创建到最后消亡经历的整个过程
组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程
意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等
钩子函数
在生命周期的不同阶段,会自动被调用执行的函数,为开发人员在不同阶段操作组件提供了时机。
注意:只有类组件 才有生命周期钩子函数
react类组件的生命周期钩子函数-整体说明
projects.wojtekmaj.pl/react-lifec…
小结
- __ _组件有生命周期,****组件没有生命周期的概念
- 类组件的生命周期有__个阶段, 5个钩子函数
组件生命周期-挂载阶段
目标
能够说出组件的挂载阶段的钩子函数以及执行时机
内容
- 执行时机:组件创建时(页面加载时)
- 执行顺序:constructor() -> render() -> componentDidMount()
| 钩子 函数 | 触发时机 | 作用 |
|---|---|---|
| constructor | 创建组件时,最先执行 | 1. 初始化state 2. 创建Ref等 |
| render | 每次组件渲染都会触发 | 渲染UI |
| componentDidMount | 组件挂载(完成DOM渲染)后 | 1. 发送网络请求 2.DOM操作 |
小结
- 挂载阶段有****个钩子? 分别是? 各个钩子函数中可以适合做什么事?
组件生命周期-更新阶段
目标
能够说出组件的更新阶段的钩子函数以及执行时机
更新阶段的钩子
更新阶段会执行两个钩子: render() -> componentDidUpdate()
三种操作可触发组件更新
- 调用setState。它能改数据&& 更新页面
- 调用forceUpdate()
- 组件接收到新的props
说明:以上三者任意一种发生,组件就会进入更新阶段
更新阶段能做的事情
| 钩子函数 | 触发时机 | 作用 |
|---|---|---|
| render | 每次组件渲染都会触发 | 渲染UI(与 挂载阶段 是同一个render) |
| componentDidUpdate | 组件更新(完成DOM渲染)后 | DOM操作,可以获取到更新后的DOM内容 |
如果有些事情是需要每次更新去做的,就可以写在componentDidUpdate中。例如:数据本地存储
小结
- 更新阶段有 __ __ __ __ 个钩子,分别可以做什么事情?
组件生命周期-卸载阶段
目标
能够说出组件的销毁阶段的钩子函数以及执行时机
内容
执行时机:组件销毁
| 钩子函数 | 触发时机 | 作用 |
|---|---|---|
| componentWillUnmount | 组件卸载(从页面中消失) | 执行清理工作(比如:清理定时器等) |
示例:清理定时器&&移除事件
/* eslint-disable react/prop-types */
import React, { Component, createRef } from 'react'
import ReactDOM from 'react-dom'
class Son extends Component {
constructor () {
super()
this.timer = setInterval(() => {
console.log('子组件:', Date.now())
}, 1000)
this.fn = () => {
console.log('鼠标移动...')
}
window.addEventListener('mousemove', this.fn)
}
render () {
return <div>子组件:{this.props.content}</div>
}
componentDidUpdate () {
console.log('子组件: componentDidUpdate')
}
componentWillUnmount () {
console.log('子组件 卸载: componentWillUnmount')
// 删除事件
window.removeEventListener('mousemove', this.fn)
// 删除定时器
clearInterval(this.timer)
}
}
export default class App extends Component {
constructor () {
super()
console.log('1. constructor')
this.state = {
content: '',
isShow: true
}
this.refTxt = createRef()
}
hChange = (e) => {
this.setState({ content: e.target.value })
}
click = () => {
this.forceUpdate()
}
render () {
console.log('2. render')
return (
<div>
<button onClick={this.click}>更新</button>
组件<input value={this.state.content} onChange={this.hChange} />
<br />
{this.state.isShow && <Son content={this.state.content} />}
</div>
)
}
componentDidMount () {
console.log('3. componentDidMount')
console.log(this.refTxt.current)
// axios.get()
}
componentDidUpdate () {
console.log('更新完成: componentDidUpdate')
}
}
ReactDOM.render(<App />, document.getElementById('root'))
综合案例-B站评论列表案例
本地持久化的功能
核心代码
componentDidUpdate() {
localStorage.setItem('list', JSON.stringify(this.state.list))
}
componentDidMount() {
const list = JSON.parse(localStorage.getItem('list')) || []
this.setState({ list })
}
setState进阶-掌握它的3个特点
目标
掌握setState的三个特点
问题导入
export default class App extends Component {
state = {
n: 1
}
hClick = () => {
this.setState({n: 100})
console.log(this.state.n)
console.log(document.getElementById('span').innerHTML) // 这会打印?
}
render () {
return (
<div>
<button onClick={this.hClick}>click</button>
<span id="span">
n: {this.state.n}
</span>
</div>
)
}
}
setState的特点
-
可以表现为异步。
setState调用之后,并不会立即去修改state的值,也不会立即去更新dom
-
多次调用会合并。setState({对象})会合并,再统一触发一次render()
-
使用不当会死循环。在componentDidUpdate, render 中调用setState()会导致死循环
小结
setState进阶-第二个参数
目标
能够掌握setState的完整语法格式
setState的第二个参数
格式1
this.setState({} [,回调函数])
回调函数是可选的,它的作用是:当本轮setState生效(state更新,页面ui更新)之后,会调用回调函数
格式2
this.setState((上一状态) => {
return 新状态
}[,回调函数])
回调函数是可选的,它的作用是:当本轮setState生效(state更新,页面ui更新)之后,会调用回调函数
示例1
state = {
n: 1
}
hClick = () => {
this.setState( preState => ({ n: preState.n + 1 }))
this.setState( preState => ({ n: preState.n + 2 }))
this.setState( preState => ({ n: preState.n + 3 }))
}
// 页面上的n会是多少?
state可以获取到叠加的状态,适用于需要调用多次setState
示例2
state = {
n: 1
}
hClick = () => {
this.setState( preState => ({ n: preState.n + 1 }), ()=>{console.log(this.state.n)})
this.setState( preState => ({ n: preState.n + 2 }), ()=>{console.log(this.state.n)})
this.setState( preState => ({ n: preState.n + 3 }), ()=>{console.log(this.state.n)})
}
// 1. 页面上的n会是多少?
// 2. 三个console.log会输出什么?
state可以获取到最新的状态,适用于获取state更新之后的状态。
setState练习
任务说明
初始初始隐藏textarea。
点击回复按钮之后:
- 显示textArea
- 让它获取焦点
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
export default class App extends Component {
state = {
isReplay: false
}
hClick = () => {
// 你的代码
}
render () {
return (
<div>
<p>要求:初始隐藏textarea。点击回复按钮,显示textArea,并让它获取焦点</p>
<button onClick={this.hClick}>回复</button>
<textarea />
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
完成:
import ReactDOM from 'react-dom'
import React, { Component, createRef } from 'react'
export default class App extends Component {
constructor () {
super()
this.txtRef = createRef()
}
state = {
isShow: false
}
hClick = () => {
this.setState({ isShow: true }, () => {
// 获取焦点
this.txtRef.current.focus()
})
}
render () {
return (
<div>
<button onClick={this.hClick}>回复</button>
{this.state.isShow && <textarea ref={this.txtRef} />}
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
setState进阶-同步or异步
目标
能够说出setState到底是同步的还是异步
内容
setState本身并不是一个异步(setTime, setInterval, ajax,Promise.then.....)方法,其之所以会表现出一种异步的形式,是因为react框架本身的性能优化机制
在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用 setState 不会同步更新 this.state,除此之外的setState调用会同步执行this.state。
所谓“除此之外”,指的是绕过React通过 addEventListener 直接添加的事件处理函数,还有通过setTimeout || setInterval 产生的异步调用。
简单一点说:
- 经过React 处理(事件回调,钩子函数)中的setState是
异步更新 - 没有经过React处理(通过
addEventListener||setTimeout/setInterval)中的setState是同步更新。
示例代码
import reactDom from 'react-dom'
import React, { Component } from 'react'
class App extends Component {
state = {
n: 1
}
// setTimeout(() => {
// this.setState({ n: 2 })
// console.log(this.state.n)
// console.log(document.getElementById('btn').innerHTML)
// }, 2000)
componentDidMount () {
// this.setState({ n: 2 })
// console.log(this.state.n)
// console.log(document.getElementById('btn').innerHTML)
document.getElementById('btn').addEventListener('click', () => {
this.setState({ n: 2 })
console.log(this.state.n)
console.log(document.getElementById('btn').innerHTML)
})
}
click = () => {
this.setState({ n: 2 })
console.log(this.state.n)
console.log(document.getElementById('btn').innerHTML)
}
render () {
return (
<div>
{/* <button id="btn" onClick={this.click}> */}
<button id="btn" onClick={this.click}>
{this.state.n}
</button>
</div>
)
}
}
reactDom.render(<App />, document.getElementById('root'))
总结
setState是同步的方法,但是react为了性能优化,所以setState在react的事件中表现得像异步。
参考链接:
json-server-安装及基本使用
参考:教程
官网: npm
作用
能json-server快速地根据已有json文件,生成接口
json ----json-server-----> 接口
使用步骤
全局安装json-server
以前安装过的全局包:
nodemon,nrm
它是依赖于nodejs的第三方包,它是一个独立的工具,并不限于某个项目,可以全局安装。
npm i json-server -g
如果是mac本,可能需要加sudo,即:sudo npm i json-server -g
准备空文件夹
在任意目录下,准备一个空文件夹,取名mock-server(可改成其它名字)
创建json文件
在文件夹中新建一个名为db.json文件(可改其它名称,注意名字是是英文)
初始化结构
在db.json文件中,按json格式的要求,去定义一个对象:
- 键值对格式
- 用双引号括起来
{
"assets": [
{ "id": 1, "name": "外套", "price": 99 },
{ "id": 2, "name": "裤子", "price": 34 },
{ "id": 3, "name": "鞋", "price": 25.4 },
{ "id": 4, "name": "头发", "price": 19900 }
]
}
启动接口服务
根据上面创建的db.json自动地生成接口。
进入到上一步创建的文件夹下,
在此文件夹下,打开命令行窗口,输入命令json-server db.json -p 8888 (json-server后有空格)
如果没有错误,则运行结果如下:
测试
在浏览器中访问上面地址
注意:
- 产生的接口地址中的
assets是与db.json中的属性名是对应的。 - db.json的内容必须是json格式。
- 属性名 ---> 接口的地址
- 属性值 ---> 接口返回数据 (数组|对象)
原理图
注意
- 小黑窗不要关!
- 小黑窗上不要用鼠标选中任何内容!
RESTful接口
目标
了解restFul风格的定义
问题导入
在写接口时,每个程序员都有自己的写法:取不同的名字,例如:实现添加资产
A同学: localhost:3000/addAsset | delAsset
B同学: localhost:3000/insertAsset | deleteAsset
C同学: localhost:3000/tjzj | sczj
RESTful接口
针对上述问题,提出一套约定。RESTful接口的规范是通过:
- 请求方式来决定操作类别(添加,删除,修改,查询)
- 用名词来表示要操作的目标
json-server提供的就是符合restful规则的接口。
测试
以上面启动的服务为例:一旦服务启动成功,就会自动生成如下接口地址。
| 接口地址 | 请求方式 | 操作类型 |
|---|---|---|
| localhost:3000/assets | GET | 获取全部数据 |
| localhost:3000/assets/1 | GET | 获取单个数据 |
| localhost:3000/assets | POST | 添加操作 。参数: {name,price} |
| localhost:3000/assets/1 | DELETE | 删除操作 |
| localhost:3000/assets/1 | PUT | 完整修改。参数:{name,price} |
| localhost:3000/assets/1 | PATCH | 局部修改。参数:{name} |
以上接口就是符合restful要求的接口规则。
案例-todos-准备接口
目标
用json-server实现接口
步骤
- 准备文件data.json
{
"todos": [
{
"id": 1,
"name": "读书",
"done": false
},
{
"name": "写字",
"done": true,
"id": 4
}
]
}
- 使用json-server启动接口
json-server data.json --port 8888
- 接口地址
http://localhost:8888/todos
增加: post
删除: delete
修改: patch / put
获取: get
案例-todos-引入素材拆分组件
目标
引入基本素材,并拆分组件
分析
目录结构
|-styles
|-------base.css
|-------index.css
|-components
|-------Footer.js
|-------Header.js
|-------Main.js
|-index.js
案例-todos-具体功能
index.js
import React, { Component } from 'react'
import Footer from './component/Footer'
import Main from './component/Main'
import Header from './component/Header'
import axios from 'axios'
export default class App extends Component {
state = {
list: []
}
componentDidMount () {
this.getTodos()
}
getTodos = async () => {
const res = await axios.get('http://localhost:8888/todos')
this.setState({
list: res.data
})
}
add = async (content) => {
await axios({
url: 'http://localhost:8888/todos',
method: 'post',
data: {
content,
isDone: false
}
})
this.getTodos()
}
del = async (id) => {
await axios({
url: 'http://localhost:8888/todos/' + id,
method: 'delete'
})
this.getTodos()
}
update = async (id, isDone) => {
await axios({
url: 'http://localhost:8888/todos/' + id,
method: 'patch',
data: {
isDone
}
})
this.getTodos()
}
render () {
const { list } = this.state
return (
<section className="todoapp">
<Header add={this.add} />
<Main update={this.update} list={list} del={this.del} />
<Footer />
</section>
)
}
}
main.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Main extends Component {
render () {
const { list } = this.props
return (
<section className="main">
<input id="toggle-all" className="toggle-all" type="checkbox" />
<label htmlFor="toggle-all">Mark all as complete</label>
<ul className="todo-list">
{list.map((it) => (
<li key={it.id} className={it.isDone ? 'completed' : ''}>
<div className="view">
<input
className="toggle"
onChange={() => {
this.props.update(it.id, !it.isDone)
}}
type="checkbox"
checked={it.isDone}
/>
<label>{it.content}</label>
<button
onClick={() => this.props.del(it.id)}
className="destroy"
/>
</div>
<input className="edit" value="Create a TodoMVC template" />
</li>
))}
{/* <li>
<div className="view">
<input className="toggle" type="checkbox" />
<label>Buy a unicorn</label>
<button className="destroy" />
</div>
<input className="edit" value="Rule the web" />
</li> */}
</ul>
</section>
)
}
}
Main.propTypes = {
list: PropTypes.array.isRequired,
del: PropTypes.func.isRequired,
update: PropTypes.func.isRequired
}
Header.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Header extends Component {
state = { content: '1' }
render () {
return (
<header className="header">
<h1>todos</h1>
<input
onKeyUp={this.add}
value={this.state.content}
onChange={({ target }) => {
console.log(target)
this.setState({ content: target.value.trim() })
}}
className="new-todo"
placeholder="What needs to be done?"
autoFocus
/>
</header>
)
}
add = async (e) => {
if (e.code === 'Enter') {
await this.props.add(this.state.content)
this.setState({ content: '' })
}
}
}
Header.propTypes = {
add: PropTypes.func.isRequired
}
Footer.js
import React, { Component } from 'react'
export default class Footer extends Component {
render () {
return (
<footer className="footer">
<span className="todo-count">
<strong>0</strong> item left
</span>
<ul className="filters">
<li>
<a className="selected" href="#/">
All
</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button className="clear-completed">Clear completed</button>
</footer>
)
}
}
列表渲染功能
核心步骤
- App组件中通过axios发送请求获取到列表
- 通过父传子把list传递给TodoMain组件
- TodoMain组件渲染任务列表