React 全家桶:React-Router 路由库、PubSub 消息管理库、Redux 集中状态管理库、Ant-Design UI
React 是 Facebook 开发的,用于构建用户界面的开源 JavaScript 库(是一个将数据渲染为HTML视图的开源 JavaScript 库)
页面渲染步骤:
原生 JavaScript 与 React 比较:
React Developer Tools
一定要注意区分:【JS表达式】 与 【JS语句(代码)】
JS 表达式: 一个表达式会产生一个值,可以放在任何一个需要值的地方:a、a+b、demo(1)、arr.map、function demo(){}
JS 代码: 控制代码走向,没有返回值:if(){}、 for(){}、switch(){case:xx}
<div id="root"></div>
<script type="text/babel">
const catName = '花姐'
// 用 JSX 创建虚拟 DOM
const VDOM = (
<div>
<h2 className="name">
<span style={{color: "orange", fontSize: "20px"}}>{catName}</span>
</h2>
</div>
)
ReactDOM.render(VDOM, document.getElementById('root'))
</script>
模块(JS模块)
理解:向外提供特定功能的 js 程序,一般就是一个 js 文件
作用:复用 js ,简化 js 的编写,提高 js 的运行效率
组件
理解:用来实现局部功能效果的代码和资源的集合(html、css、js、image等)
作用:复用编码、简化项目编码、提高运行效率
模块化:当应用的 js 都以模块来编写的,这个应用就是一个 模块化 的应用
组件化:当应用都是以组件的方式实现,这个应用就是一个 组件化 的应用
// 创建一个 cat 类
class Cat{
// 构造器方法
constructor(name){
// 构造器中的 this 指向类的实例对象: 谁调用了new, this 指向谁
this.name = name
}
// 一般方法:放在了类的原型对象(_proto_)上, 供实例使用
// 通过 Cat 实例调用一般方法时,this 指向 Cat 实例
sayName(){
console.log(`我叫${this.name}`)
}
}
// 创建一个 cat 的实例对象
const HJ = new Cat('花姐')
// 创建一个 Minicat 类,继承于 Cat 类
class Minicat extends Cat {
constructor(name, age){
// super 作用:调用父类的构造器函数;super 必须写在其他参数之前
super(name);
this.age = age
}
}
// 创建一个 Minicat 的实例对象
const mini = new Minicat('橘宝', 2)
/*
总结:
1、类中的构造器不是必须写的,要对实例进行一些初始化操作的时候,如添加属性才写;
2、如果 A 类继承了 B 类,且 A 类写了构造器,那 A 类中构造器的 super 是必须调用的;
3、类中的方法,都是放在类的原型对象上(_proto_),供实例使用
*/
适用于 简单组件(无state) 的定义
// 创建函数式组件
function Demo(){
// babel 编译后开启了严格模式,严格模式禁止自定义的函数 this 指向 widow
console.log(this); // undefined
return <h2>我是函数式组件</h2>
}
// 渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('root'));
/*
执行了 ReactDOM.render(( .... 之后:
1、React 解析组件标签,找到了 Demo 组件;
2、发现是函数式组件的,随后调用该函数,将返回的虚拟 DOM 转为真实 DOM ,随后呈现在页面中
*/
适用于 复杂组件(有state) 的定义
/*
创建类式组件
1、React.Component:React 内置类
2、内部 render(){} 必须写,放在 Demo 原型对象上,供实例使用
*/
class Demo extends React.Component {
render(){
// render 中的 this 指向 Demo 组件实例对象
console.log(this);
return <h2>我是类式组件</h2>
}
}
// 渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('root'))
/*
执行了 ReactDOM.render(( .... 之后:
1、React 解析组件标签,找到 Demo 组件;
2、发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用到原型上的 render 方法
3、将 render 返回的虚拟 DOM 转化为真实的 DOM, 随后呈现在页面中
*/
// state 复杂写法
class Weather extends React.Component {
constructor(props){
super(props);
this.state = { isHot: false };
// .bind(this)用来解决 this 指向问题
this.changeWeather = this.changeWeather.bind(this);
}
/*
changeWeather 放在 Weather 原型对象上,供实例使用
由于 changeWeather 是作为 onClick 回调,而不是通过实例调用的,是直接调用
类中的方法默认开启了举报的严格模式,所以方法中的 this 为 undefined
*/
changeWeather(){ this.setState({isHot: !this.state.isHot}) }
render(){
const { isHot } = this.state;
return <p onClick={this.changeWeather} >今天天气{isHot}</p>
}
}
// state 简写方式: 省略类构造器,函数修改 this 指向
class Weather extends React.Component {
// 类中可以直接写赋值语句,给类添加属性(不能写var\let\const)
state = { isHot: fasle };
/*
赋值语句的形式 + 箭头函数,修改函数 this 指向
箭头函数没有自己的this, 声明时会向外寻找 this,始终指向函数声明时所在的作用域下的 this 的值
*/
changeWeather = () => this.setState({isHot: !this.state.isHot})
render(){
const { isHot } = this.state;
return <p onClick={this.changeWeather} >今天天气{isHot}</p>
}
}
/*
总结:
1、组件中 render 方法中的 this 为组件实例对象
2、组件自定义的方法中 this 为undefined, 解决方法:
通过函数对象的 bind(), 强制绑定this
箭头函数 + 赋值语句,声明方法
3、状态数据,通过 setState 修改
this.state.isHot = true 修改 isHot 的值会成功,但是不会触发渲染
需要通过 setState 修改才会触发重新渲染
4、标签绑定事件 onClick 要大写
*/
理解:每个组件都有一个 props(properties) 属性,组件标签的所有属性都保存在 props 中
作用:通过标签属性从组件外部向组件内部传递变化的数据
注意:组件内部不要修改 props 数据,会报错
let obj ={name: '花姐'}
// 1、函数组件使用 props,参数形式
function Demofn(props){
const {name} = props
return(
<p>{name}</p>
)
}
// 对标签属性进行类型、必要性限制
Demofn.propTypes = {
name: PropTypes.string.isRequired, // string 表示为字符串,isRequired 表示必传
}
// 指定默认标签属性值
Demofn.defaultProps = {
name: '喵'
}
ReactDOM.render(<Demofn {...obj}/>, document.getElementById('root'))
// 2、类式组件中的构造器 与 props
class Democlass extends React.Component {
// 构造器是都接收 props,是否传递给 super 取决于是否希望在构造器中通过 this 访问 props
// 开发过程中一般不使用构造器
constructor(props){
super(props);
console.log(this.props); // 需要调用super(props),才能在构造器中使用 this.props,否则为 undefined
}
// 对标签属性进行类型、必要性限制
static propTypes = {
name: PropTypes.string.isRequired, // string 表示为字符串,isRequired 表示必传
}
// 指定默认标签属性值
static defaultProps = {
name: '喵'
}
render(){
let {name} = this.props
return(
<p>{name}</p>
)
}
}
// {...obj} 扩展运算符批量传递
ReactDOM.render(<Democlass {...obj}/>, document.getElementById('root2'))
class Demo extends React.Component {
getRef = () => {
// this.refs.btn 是真实 DOM
const { btn } = this.refs
}
render(){
return(
<button ref="btn" onClick={this.getRef}>字符串形式 ref (不推荐使用)</button>
)
}
}
ReactDOM.render(<Demo/>, document.getElementById('root'))
class Demo extends React.Component {
getRef = () => {
const { btn } = this
}
render(){
return(
<button ref={c=> this.btn = c} onClick={this.getRef}>回调形式 ref, 内联写法</button>
)
}
}
ReactDOM.render(<Demo/>, document.getElementById('root'))
回调 ref 在页面更新过程中调用次数的问题(可以忽略,影响不大):
第一次传入 null,第二次传入 DOM,每次更新渲染时会创建一个新的函数实例,React 会清空就的 ref 并设置新的 ref
解决:通过 ref 回调函数定义成 class 的绑定函数可以解决
class Demo extends React.Component {
saveBtn = (c) => { this.btn = c }
getRef = () => { const { btn } = this }
render(){
return(
<button ref={this.saveBtn} onClick={this.getRef}>回调形式 ref, 内联写法</button>
)
}
}
ReactDOM.render(<Demo/>, document.getElementById('root'))
class Demo extends React.Component {
myRefs = React.createRef()
getRef = () => {
console.log(this.myRefs.current)
}
render(){
return(
<button ref={this.myRefs} onClick={this.getRef}>createRef 形式</button>
)
}
}
ReactDOM.render(<Demo/>, document.getElementById('root'))
事件的执行顺序为原生事件先执行,合成事件后执行
合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用
如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到 document 上合成事件才会执行
react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用 event.preventDefault() 来阻止默认行
受控组件:页面所有输入都由 state 控制
非受控组件: 现取现用 ref
高阶函数
如果一个函数符合下面2个规范中的任何一个,那么函数就是高阶函数
1、若A函数,接收的参数是一个函数,A就可以称为高阶函数
2、若A函数,函数调用的返回值依然是一个函数,A就可以称为高阶函数
例:promise(() => {})、setTimeout(() => {})、arr.map(() => {}) (数据常见的方法)等
函数柯里化
通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
function sum(a){
return b => {
return c =>{
return a + b + c
}
}
}
sum(1)(2)(3)
react 表单柯里化案例:
class Login extends React.Component {
state = {
username: '',
password: ''
}
handleSubmit = () => {
event.preventDefault();
const {username, password} = this.state
}
// 柯里化实现
saveFormData = (type) => {
return (event) => {
this.setState({[type]: event.target.value})
}
}
// 不使用柯里化实现
saveTypeData = (type, event) => {
this.setState({[type]: event.target.value})
}
render(){
return(
<form onSubmit={this.handleSubmit}>
{/* 柯里化实现 */}
用户名:<input type="text" name="username" onChange={this.saveFormData('username')} />
{/* 不使用柯里化实现 */}
密码:<input type="password" name="password" onChange={event => this.saveTypeData('password', event)} />
<button>登录</button>
</form>
)
}
}
componentWillMount :组件将要挂载(17 废弃)
componentWillUpdate :组件将要更新(17 废弃)(this.forceUpdate() 触发)
componentWillReceiveProp s:子组件将要接收新的 props,首次不调用(17 废弃)
旧生命周期总结:
初始化阶段(挂载时):由 ReactDOM.render() 触发初次渲染
constructor()
componentWillMount()
render()
componentDidMount()
更新阶段:由组件内部 this.setState() 或 父组件 或 render 触发
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
卸载组件:由ReactDOM.unmountComponentAtNode() 触发
componentWillUnmount()
新生命周期总结:
初始化阶段(挂载时):由 ReactDOM.render() 触发初次渲染
constructor()
getDerivedStateFromProps()
render()
componentDidMount()
更新阶段:由组件内部 this.setState() 或 父组件 或 render 触发
getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
卸载组件:由ReactDOM.unmountComponentAtNode() 触发
componentWillUnmount()
常用:
componentDidMount() 在此做初始化:开启定时器,发送网络请求,订阅信息
componentWillUnmount() 在此做收尾工作:关闭定时器,取消订阅信息
getSnapshotBeforeUpdate 使用场景
// 数据不停新增,固定滚动条位置
class NewList extends React.Component {
state = { newArr: [] }
componentDidMount() {
setInterval(() => {
const { newArr } = this.state
const news = '新闻' + (newArr.length + 1)
this.setState({newArr: [...news, newArr]})
}, 1000)
}
// getSnapshotBeforeUpdate 在最近渲染输出(提交到DOM节点)之前调用,能在组件发生更改之前从 DOM 中捕获一些信息
// 返回值将作为参数传递给 componentDidUpdate
getSnapshotBeforeUpdate(){
return this.refs.list.scrollHeight
}
componentDidUpdate(preProps, preState, scrollHeight) {
// preProps: 先前props, preState: 先前状态
this.refs.list.scrollTop += this.refs.list.scrollHeight - scrollHeight
}
render(){
return (
<div>
{
this.state.newArr.map((n, index) => {
return <div key={index}> {n} </div>
})
}
</div>
)
}
}
虚拟 DOM 中 key 的作用
key 是虚拟 DOM 对象的表示
当状态中的数据发生变化后,react 会根据【新数据】 生成 【新的虚拟DOM】,随后React进行【新虚拟 DOM】与【旧虚拟DOM】的 diff 比较,规则:
1)、旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key
- 若虚拟 DOM 中的内容没变,直接使用之前的真实 DOM
- 若虚拟 DOM 中的内容变了,则生成新的真实 DOM ,随后替换掉页面之前的真实 DOM
2)、旧虚拟 DOM 中未找到新虚拟 DOM 相同的 key,根据数据创建新的 DOM , 随后渲染到页面
用index 作为 key 可能会引发的问题
1)、若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没必要的DOM更新,界面没问题,效率低
2)、如果结构中包含输入类的 DOM(input 等):会产生错误的DOM更新,界面有问题(输入类的 DOM 内容会保留)
3)、如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于展示,使用index 作为 key 是没有问题的
开发中如何选择key
1)、使用每条数据的唯一标识作为 key,例如id、手机号、学号等
2)、如果确定只是简单的展示数据,则用 index 也是没关系的
vacode 的 react 扩展插件:ES7+ React/Redux/React-Native snippets
public ----- 静态文件
index.html ----- 应用主页面
manifest.json ----- 应用加壳时的相关配置文件
robots.txt ----- 爬虫规则文件
src ----- 静态文件
App.js ----- App组件
App.test.js ----- 用于给 App 做测试
index.js ----- 入口文件
reportWebVitals.js ----- 页面性能分析文件(需要 web-vitals 库的支持)
setupTests.js ----- 用于组件测试
创建App组件:两种引入 React.Component 的写法
import React from 'react'
// 创建并暴露app组件
export default class App extends React.Component{
render() {
return(
<div>hello,react!</div>
)
}
}
// 能直接拿到 Component 不是因为解构赋值, 而是因为虽然 Component 在 react 的原型上,
// react 默认暴露了React,并且分别暴露(export)了 Component 方法
import React, {Component} from 'react'
// 创建并暴露app组件
export default class App extends Component{
render() {
return(
<div>hello,react!</div>
)
}
}
创建入口文件 index.js
import React from 'react'
// 18版本的写法 react-dom/client
import ReactDOM from 'react-dom/client'
import App from './App'
// 18版本的写法 ReactDOM.createRoot
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
React 的样式是全局化,需做处理避免样式冲突,解决方法:
假设项目端口是:http://localhost:3000
方式一:在 package.json 中配置 proxy
优点:配置简单,前端请求资源时可以不加任何前缀
缺点:只能配置一个代理,不能配置多个代理
工作方式:上述方式配置代理,当请求了 3000 不存在的资源时,那么请求会转发给 5000 (优先匹配前端资源)
{
"proxy": "http://localhost:5000",
}
方式二:在项目根目录处创建 setupProxy.js (名字不可改)
优点:可以配置多个代理,可以灵活的控制请求是否走代理
缺点:配置繁琐,前端请求资源时必须加前缀
// 注意区分 http-proxy-middleware 版本不同写法
const proxy = require('http-proxy-middleware')
module.exports = function(app) {
app.use(
// 1、http-proxy-middleware低版本(2以下) proxy
proxy('/api', { // 遇见/api前缀请求,就会触发该代理配置
target: 'http://localhost:5000', // 请求转发给谁
changeOrigin: true, // changeOrigin 控制服务器收到的请求头中 HOST 字段
pathRewrite: {'^/pai': ''} // 重写请求路径(必须),去除请求前缀,保证交给服务器的是正常请求地址
}),
// 2、http-proxy-middleware高版本(2以上) proxy.createProxyMiddleware
proxy.createProxyMiddleware('/api2',{
target:'http://localhost:5001',
changeOrigin:true,
pathRewrite:{'^/api2':''}
}),
)
}
/**
* changeOrigin 设置为 true 时,服务器收到的请求头 host 为:localhost:5000
* changeOrigin 设置为 false 时,服务器收到的请求头 host 为:localhost:3000
* changeOrigin 默认为 false, 一般会设置为 true
*/
方法一 或 方法二 添加后,需重启项目才生效
请求接口:http://localhost:5000/api/index/index
假设项目端口是:http://localhost:3000
实际请求写法可以是:
// 此时 url 两种值均可 "http://localhost:3000/api/index/index" 或 "/api/index/index"
let url = "/api/index/index"
axios.get(url).then(
response => {console.log('成功了',response.data);},
error => {console.log('失败了',error);}
)
1、先订阅,再发布;在需要数据的组件订阅,在发送数据的组件发布
订阅: let 订阅名 = PubSub.subscribe(消息名, function(消息名, 数据){})
发布: PubSub.publish(消息名,数据)
2、适用于任意组件间的通讯
3、要在组件的 componentWillUnmount 中取消订阅
取消订阅:PubSub.unsubscribe(订阅名)
4、react 使用 PubSub 实现搜索例子(先订阅,再发布)
List.jsx
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
export default class List extends Component {
state = {
users:[],
isFirst: true, // 记录是否是第一次请求
isLoading: false, // 标识是否处于加载中
err: "", // 储存请求错误信息
}
componentDidMount() {
// 订阅消息
this.searchPubSub = PubSub.subscribe('getsearchData', (msgName, data) => {
console.log(msgName); // getsearchData
this.setState(data)
})
}
componentWillUnmount() {
// 页面销毁,取消消息订阅
PubSub.unsubscribe(this.searchPubSub)
}
render() {
const {users, isFirst, isLoading, err} = this.state
return (
<div className='flex-wrap'>
{
isFirst? <h2>欢迎使用!</h2> :
isLoading ? <h2>loading....</h2> :
err ? <h2>{err}</h2> :
users.map((userObj) => {
return (
<div className='box' key={userObj.id}>
<img src={ userObj.avatar_url } alt="head_portrait"></img>
<span>{ userObj.login }</span>
</div>
)
})
}
</div>
)
}
}
Search.jsx
// Search.jsx 设置发布
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import axios from 'axios'
export default class Search extends Component {
search = () => {
const { keyWordElement: {value: keyword} } = this
// 发送请求前通知 list 更新状态
PubSub.publish('getsearchData', {isFirst: false, isLoading: true})
axios.get(`https:api.github.com/search/users?q=${keyword}`).then(response => {
// 请求成功通知 list 更新状态
PubSub.publish('getsearchData', {isLoading: false, users: response.data.items, err: ""})
},err => {
// 请求失败通知 list 更新状态
PubSub.publish('getsearchData', {isLoading: false, err: err.message})
})
}
render() {
return (
<div>
<section>
<h3> Search 用户</h3>
<input ref={c => this.keyWordElement = c} type="text"></input>
<button onClick={this.search}>搜索</button>
</section>
</div>
)
}
}
App.jsx
import React, { Component } from 'react'
import Search from './components/Search'
import List from './components/List'
export default class App extends Component {
render() {
return (
<div>
<Search />
<List />
</div>
)
}
}
react-router 的理解:react 专门用来实现一个 SPA 应用的一个插件库: 分web(用于网页:react-router-dom) 、native(原生应用开发用)、any(任何平台都可用)
一个路由就是一个映射关系(key对应value),key 为路径,value 可能是function 或 component。
路由分类(后端路由 与 前端路由):
1)、 后端路由:
value 是 function 用来处理客户端提交的请求(后端路由即接口)
注册路由:router.get(path, function()=>{})
工作过程:node 接收到一个提交,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据
2)、前端路由:
浏览器端路由 value 是component,用于展示页面内容
注册路由:
工作过程:当浏览器的 path 变为 /test 时,当前路由组件就会变为Test 组件
路由原理:
基石是浏览器 BOM 身上的 history(window.history), 浏览器的 history 数据结构是栈:先进后出; 用于管理浏览器历史记录等; 可用 history.listen 监听路由变化
history 分两种:
history: H5 推出的 History Interface,利于 pushState() 和 replaceState()方法 - 兼容性差
hash: 利用的是 hash 值,类似锚点,链接带#,兼容性强
hash 模式和 history 模式都属于浏览器自身的特性
vue 的 history 模式需要后端配合,如果后端不配合,刷新页面会出现404
hash不需要url 中的 # 后的参数,不会作为资源发送给服务器
路由安装:
5版: npm i react-router-dem@5
6版: npm i react-router-dem
BrowserRouter、hashRouter、Route、Link
路由器 Router 管理着路由Route, Route 要包在路由器 Router(BrowserRouter or HashRouter) 之中;整个应用只能用一个路由器管理(注意区分 路由器Router 与 路由 Route)。
1、BrowserRouter、hashRouter 区别:
1、底层原理不一样
BrowserRouter 使用的是 H5 的 history API,不兼容 IE9 及以下版本
hashRouter 使用的是 URL 的哈希值
2、 path 表现形式不一样
BrowserRouter 的路径中没有#,例如:http://localhost:3000/home
HashRouter 的路径有#,例如:http://localhost:3000/#/home
3、刷新后对路由 state 参数的影响
BrowserRouter 没有任何影响,因为 state 保存在 history 对象中
hashRouter 刷新后会导致路由 state 参数丢失
4、hasRouter 可以用于解决一些路径错误相关的问题
2、路由的基本使用:
导航区用 Link 或 NavLink,展示区用 Route, 最外侧要包裹一个 BrowserRouter 或 HashRouter
index.js 文件 设置路由器(BrowserRouter or HashRouter):
import {BrowserRouter} from 'react-router-dom'
ReactDOM.render(
<BrowserRouter>
<App/>
</BrowserRouter>
,document.getElementById('root'))
App.js 编写路由链接、注册路由:
import React, { Component } from 'react'
import {Link, Route} from 'react-router-dom'
import Home from './components/Home'
export default class App extends Component {
render() {
return (
<div className='main'>
<div className='left'>
{/* 编写路由连接 - react 中靠路由连接实现切换组件*/}
<Link to="/home">home</Link>
</div>
<div className='right'>
{/* 注册路由 - 组件展示区 */}
<Route path="/home" component={Home}/>
</div>
</div>
)
}
}
Route
1、Route 模糊匹配 和 严格匹配(exact)
路由匹配遵循 - 最左侧匹配原则
模糊匹配:
输入的路径 必须要包含 匹配的路径,且顺序要一致,默认使用
例子:
home 可以匹配到
严格匹配:
输入的路径与匹配的路径必须一致 exact={true}
开启严格匹配:
严格匹配不要随便开启,有时候开启会导致无法继续匹配多级路由
2、Route 嵌套路由(多级路由)
1、注册子路由要写上父级路由的 path 值
2、路由的匹配时按照注册路由的顺序进行的
多级路由匹配的步骤:
首先路由匹配第一批注册的路由,例如 /home/tab 当匹配到 /home 时,就会先显示 home 组件
随后在home 组件中继续匹配路由/home/tab, 匹配到则展示,匹配不到则跳转到 Redirect 指定的路由
3、解决多级路径刷新页面样式丢失的问题
public 中的 index.html 引入样式时:
1、 './' 改为 '%PUBLIC_URL%/' (常用, PUBLIC_URL react 特有)
2、 路径 './' 改为 '/'
3、 使用 HashRouter
4、路由( Link、NavLink )的两种跳转方式 push、replace
1、默认是 push 模式,会留下痕迹
2、replace 不留下痕迹
开启 replace:
5、编程式路由导航
不写 Link 和 NavLink,借助this.props.history对象身上的 API 对路由进行跳转、前进、后退
利用: go、goBack、goForward、push、replace 实现路由跳转
this.props.history.go(n)
this.props.history.goBack()
this.props.history.goForward()
replace:
this.props.history.replace(`/home/tabTow/detail/${id}`)
this.props.history.replace(`/home/tabTow/detail?id=${item.id}`)
this.props.history.replace(`/home/tabTow/detail`,{id})
push:
this.props.history.push(`/home/tabTow/detail/${id}`)
this.props.history.push(`/home/tabTow/detail?id=${item.id}`)
this.props.history.push(`/home/tabTow/detail`,{id})
6、向路由组件传参
1、params 参数
路由连接(携带参数):{item.title }
注册路由(声明接收):
组件接收参数:this.props.match.params
2、search 参数
路由连接(携带参数):{item.title}
注册路由(无需声明,正常注册即可):
组件接收参数:this.props.location
备注:获取到的 search 是 urlencode 编码字符串,需要借助 querystring 解析
import qs from 'qs'
const {id, title} = qs.parse(search.slice(1))
3、state 参数
路由连接(携带参数):{item.title}
注册路由(无需声明,正常注册即可):
组件接收参数:this.props.location.state || {}
备注:此处的 state 是路由身上的 state,不是组件的 state, 刷新也可以保留住参数
7、路由组件 与 一般组件
1、写法不同:
路由组件:
一般组件:
2、存放位置不同:
路由组件:pages
一般路由:components
3、接受到的 props 不同:
一般组件: 组件标签传递了什么,子组件 props 就收到什么
路由组件:接受到三个属性
histor:{ go, goBack, goForward, push, replace }
location: { pathname, search, state }
match: { isExact, params, path, url }
isExact: 模糊匹配 or 精准匹配
Link 和 NavLink
Link 没有高亮效果
NavLink 标签可以通过 activeClassName 设置高亮类名,默认值是 active
// NavLink 封装
import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'
export default class MyNavLink extends Component {
render() {
return (
<NavLink activeClassName='linkactive' {...this.props} />
)
}
}
// 封装使用
<MyNavLink to="/home" >home</MyNavLink>
// 标签体内容是一个特殊的标签属性 children
// children 可以通过 this.props.children 获取标签体内容,再利用{...this.props}绑定到组件上
Switch
通常情况下,path 和 component 是一一对应关系,不使用 Switch, 路由匹配后还会继续往下匹配
Siwtch 作用:实现单一匹配 - 路由匹配到后,终止往下匹配,提高路由匹配效率
// 语法
<Switch>
<Route path='/demo' component={Demo} />
</Siwtch>
Redirect 重定向
一般写在所有路由注册的最下方,当所有路由都匹配不上的时候,跳转到 Redirect 指定的路由
// 语法
<Switch>
<Route path='/demo' component={Demo} />
<Redirect to="/demo" />
</Switch>
withRouter
只有路由组件才有 history, 一般组件没有 history,无法使用 this.props.history 的AP方法
withRouter 可以加工一般组件,让一般组件具备路由组件所特有的 API
withRouter 返回值是一个新组件
语法: export default withRouter(一般组件)
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
class Header extends Component {
back = () => {
this.props.history.back()
}
render() {
return (
<div>
<button onClick={this.back}>回退</button>
</div>
)
}
}
// 暴露 withRouter 加工过的一般组件
export default withRouter(Header)
// 使用组件
<Header />
与 react-router 5 版本小相比,改变了什么?
1、内置组件的变化:移除 ,新增了 等
2、语法的变化:component = {Home} 变为 element = { }等
3、新增多个hook:useParams、useNavigate、useMatch 等
4、官方明确推荐函数式组件
Route
语法变化 component 变为 element,属性值为标签形式
caseSensitive属性用于指定,匹配时是否区分大小写(默认false)
Route 可以嵌套使用,且可配合 useRoutes() 配置“路由表”,当需要通过
// React-router 5
<Route path="/home" component={Home} />
// React-router 6
<Routes>
// path属性用于定义路径,element 属性用于定义当前路径所对应的组件
<Route path="/home" element={<Home />} />
// 定义嵌套路由,home是以及路由,对应的路径 /home*/
<Route path="home" element={<Home />} >
<Route path="news" element={<news />} ></Route>
<Route path="news2" element={<news2/>} ></Route>
</Route>
// Router 也可以不写 element 属性,这是就是用于展示嵌套的路由,对应的路径是 /home/xxx
<Route path="home">
<Route path="xxx" element={<xxx/>} />
</Route>
</Routes>
Routes 代替 Switch
React-router 6 移除了 Switch,新增了Routes, Routes 跟 Switch的规则一样,匹配到一个,就不会继续向下匹配,
Routes 要和 Route 配合使用,且必须用 Routes 包裹 Route:React-router 5 不写 Switch 不会报错,React-router 6 Route 不用 Routes 包裹会报错
// React-router 5
<Switch>
<Route path="/home" component={Home} />
</Switch>
// React-router 6
<Routes>
<Route path="/home" element={<Home />} />
</Routes>
Navigate 代替 Redirect 重定向
Navigate 只要渲染就会引起试图的切换,to 的属性必须写,replace 属性可选
// React-router 5
<Switch>
<Route path="/home" component={Home} />
<Redirect to="/about" />
</Switch>
// React-router 6
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/" element={<Navigate to="/about" />} />
</Routes>
页面跳转例子:
import React,{useState} from 'react'
import {Navigate} from 'react-router-dom'
export default function Demo(){
const [sum, setSum] = useState(1)
return (
<div>
{sum === 2? <Navigate to="/home" replace={true}/> : <h3>当前的sum的值是:{sum}</h3> }
<button onClick={()=>setSum(2)}>点击变2</button>
</div>
)
}
NavLink
React-router 6指定高亮的类名语法修改了
// React-router 5
<NavLink activeClassName='linkactive' {...this.props} />
// React-router 6
<NavLink className={isActive => isActive ? 'linkactive' : 'link'} {...this.props} />
// 优化写法
function computedClassName(isActive){
return isActive ? 'linkactive' : 'link'
}
<NavLink className={computedClassName} {...this.props} />
useRoutes路由表 与 Outlet嵌套路由
定义路由表:创建routes文件夹,生成inde.jsx文件
import Home from './pages/Home'
import About from './pages/About'
import TabOne from './pages/Home/TabOne'
import TabTow from './pages/Home/TabTow'
import {Navigator} from 'react-router-dom'
export default[
{
path: '/home',
element: <Home />,
// 定义子路由
children:[{
path: 'tabOne',
element: <TabOne />
},{
path: 'tabTow',
element: <TabTow />
}]
},{
path: '/about',
element: <About />
},{
path: '/',
element: <Navigator to="/home" />
}
]
使用路由表 uesRoutes
import React from 'react'
import {NavLink, uesRoutes} from 'react-router-dom'
import routes from './routes'
export default function App() {
// 根据路由表生成对应的规则
const element = uesRoutes(routes)
render() {
return (
<div className='containt'>
{/* 路由连接 */}
<NavLink to="/home">home</NavLink>
<NavLink to="/about">about</NavLink>
{/* 注册路由 */}
{element}
</div>
)
}
}
Outlet嵌套路由:Home 组件展示子路由,因为App已经引入了routes.js,所以 Home作为App组件不需要引入
import React from 'react'
import { NavLink, Outlet } from 'react-router-dom'
export default function Home() {
return (
<div className='containt'>
{/* 路由连接-注意子路由不写/ */}
<NavLink to="tabOne">home</NavLink>
<NavLink to="tabTow">about</NavLink>
{/* 指定路由组件呈现的位置 */}
{Outlet}
</div>
)
}
路由传参
1、传递 Prarams 参数,使用useParams 或 useMatch 获取
// 路由表
import Home from './pages/Home'
import {Navigator} from 'react-router-dom'
export default[
{
// Prarams 参数需要占位
path: '/home/:id/:title',
element: <Home />,
}{
path: '/',
element: <Navigator to="/home" />
}
]
// 使用路由表uesRoutes
import React from 'react'
import {NavLink, uesRoutes} from 'react-router-dom'
import routes from './routes'
export default function App() {
// 根据路由表生成对应的规则
const element = uesRoutes(routes)
render() {
return (
<div className='containt'>
{/* 路由连接- 定义参数 */}
<NavLink to={`/home/${id}/${title}`}>home</NavLink>
{/* 注册路由 */}
{element}
</div>
)
}
}
Home 组件 接收参数
import React from 'react'
import { useParams, useMatch } from 'react-router-dom'
export default function Home() {
// useParams获取 路由参数
const {id, title} = useParams()
// useMatch 获取 路由参数
const x = useMatch('/home/:id/:title')
return (
<div className='containt'> </div>
)
}
2、传递 search参数,使用useSearchParams 或 useLocation获取
import Home from './pages/Home'
import {Navigator} from 'react-router-dom'
export default[
{
path: '/home',
element: <Home />,
}{
path: '/',
element: <Navigator to="/home" />
}
]
// 使用路由表uesRoutes
import React from 'react'
import {NavLink, uesRoutes} from 'react-router-dom'
import routes from './routes'
export default function App() {
// 根据路由表生成对应的规则
const element = uesRoutes(routes)
render() {
return (
<div className='containt'>
{/* 路由连接- 定义参数 */}
<NavLink to={`/home?id=${id}&title=${title}`}>home</NavLink>
{/* 注册路由 */}
{element}
</div>
)
}
}
Home 组件 接收参数
import React from 'react'
import { useSearchParams, useLocation } from 'react-router-dom'
export default function Home() {
// useSearchParams获取路由参数,search.get()获取参数值
const [search, setSearch] = useSearchParams()
console.log(search.get('id'))
// useLocation 获取参数 - 获取的是对象
const x = useLocation()
return (
<div className='containt'>
// setSearch 用于更新参数值
<button onClick={()=>setSearch('id=1&title=花姐')}>更新链接参数</button>
</div>
)
}
3、传递 state 参数,使用useLocation获取
import Home from './pages/Home'
import {Navigator} from 'react-router-dom'
export default[
{
path: '/home',
element: <Home />,
}{
path: '/',
element: <Navigator to="/home" />
}
]
// 使用路由表uesRoutes
import React from 'react'
import {NavLink, uesRoutes} from 'react-router-dom'
import routes from './routes'
export default function App() {
// 根据路由表生成对应的规则
const element = uesRoutes(routes)
render() {
return (
<div className='containt'>
{/* 路由连接- 定义参数 */}
<NavLink to="/home" state={{ id, title }}>home</NavLink>
{/* 注册路由 */}
{element}
</div>
)
}
}
Home 组件 接收参数
import React from 'react'
import { useLocation } from 'react-router-dom'
export default function Home() {
// useLocation 获取参数 - 获取的是对象
const state = useLocation()
return (
<div className='containt'></div>
)
}
编程式路由导航 useNavigate
import React from 'react'
import {NavLink, uesRoutes, useNavigate} from 'react-router-dom'
import routes from './routes'
export default function App() {
const navigate = userNavigate()
// 根据路由表生成对应的规则
const element = uesRoutes(routes)
function goToDetail(id){
// 子路由不能写 /
navigate('tabOne', {
repalce: false, // 不写默认false
// 如果传递params参数与search参数,则拼接在链接上,不写state
state:{id}
})
// navigate(-1), navigate(1) 实现前进后退
}
render() {
return (
<div className='containt'>
{/* 路由连接- 定义参数 */}
<NavLink to="/home" state={{ id, title }}>home</NavLink>
<button onClick="goToDetail">点击跳转</button>
{/* 注册路由 */}
{element}
</div>
)
}
}
useInRouterContext()、useNavigation()返回当前的导航类型、userOutlet()呈现当前组件中渲染的嵌套路由、useResolvePath()用于解析path,search,hash值
官网: https://ant.design/index-cn
安装: npm i antd
antd 提倡按需加载模块,使用相关 api 时,需要注意引入的文件名
antd 适用于搭建后台管理系统
按需引入样式:官方配置文档-高级配置
redux 是一个专门用于做状态管理的JS库,可以用在 react、angular、 vue等项目中,但是基本与 react 配合使用
官网: https://www.redux.org.cn/
安装:npm i redux
redux开发者工具:谷歌安装扩展 redux-devtools,还要在项目安装 redux-devtools-extension 库,并在 store.js 引用:
import {composeWithDevTools} from 'redux-dextools-extension'
export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
作用: 集中式管理 react 应用中多个组件共享的状态(独立于所有组件之外)
使用场景:
某个组件的状态,需要让其他组件可以随时拿到(共享)
一个组件需要改变另外一个组件的状态(通信)
总体原则:能不用就不用,如果不用比较吃力才考虑使用
React Components: 触发动作
三个核心概念 action、reducer、Store
action(动作对象):
1、同步 action
当 action 的值为一般对象是为同步 action,包含两个属性:
type:标识属性,值为字符串,唯一,必要属性
data:数据属性,值任意属性,可选属性
2、异步 action
当 action 的值为函数时,为异步 action,异步 action 中一般都会调用同步 action
reducer:
reducer 的本质是一个函数,接收 preState, action, 返回加工后的新 state
reducer 有两个作用:初始化状态,加工状态
reducer 被第一次调用时,是 store 自动触发的,传递的 preState 是 undefined
Store:
将state、action、reducer 联系在一起的对象(不做数据处理)
如何得到此对象?
import { createStore } from 'redux'
import reducer from './reducers'
const store = createStore (reducer )
此对象的功能:
getState():得到 state
dispatch(action):分发 action,触发 reducer 调用。产生新的 state
subscribe(listener):注册监听,当产生了新的 state 时,自动调用
在组件的 componentDidMount 中或在 index.js 中通过 store.subscribe() 检测 store 中状态的改变
一旦改变重新调用 this.setState({}) 或 重新渲染
合并Reducers
每个reducer只负责管理全局state中它负责的一部分。每个reducer的state参数都不同,分别对应它管理的那部分state数据
Redux 提供了combineReducers()来合并所有的reducer
/**
* 该文件用于汇总所有的reducer为一个总的reducer
* combineReducers 用于汇总多个reducer
*/
import { combineReducers} from 'redux'
// 引入为 count 组件服务的 reducer
import count from './count'
import personObj from './person'
// 汇总所有的reducer 变为一个总的reducer
export default combineReducers({
count,
personObj
})
redux 实践:实现运算:
store.js
引入 redux 中的 createStore 函数,创建一个 store, createStore 调用时要传入一个为其服务的 reducer
记得暴露 store 对象
/**
* 该文件专门用于暴露一个 store 对象,整个应用只有一个 store 对象
*/
// 引入 createStore, 专门用于创建 redux 最核心的 store 对象, applyMiddleware 用于执行中间件
import {createStore, applyMiddleware} from 'redux'
// 引入为 count 组件服务的 reducer
import countReducer from './count_reducer'
// 引入 redux-thunk 用于支持异步 action
import thunk from 'redux-thunk'
export default createStore(countReducer, applyMiddleware(thunk))
count_reducer.js
reducer 的本质是一个函数,接收 preState, action,返回加工后的状态
reducer 有两个作用:初始化状态,加工状态
reducer 被第一次调用时,是 store 自动触发的,传递的 preState 是 undefined
/**
* 该文件是用于创建一个 Count 组件服务的 reducer, reducer 的本质就是一个函数
* reducer 是一个纯函数,相同输入得到相同结果
* reducer 函数会接到两个参数,分别为:之前的状态(preState)、动作对象(action)
*/
import {INCREMENT} from './constant'
export default function countReducer(preState = 0, action) {
const {type, data} = action
// 根据 type 决定如何加工数据
switch (type) {
case INCREMENT:
return preState + data
default:
return preState
}
}
count_action.js
专门用于创建 action 对象
/**
* 该文件专门为 Count 组件生成 action 对象
* 当 action 的值为对象时,为同步 action, action({type,data})
* 当 action 的值为函数时,为异步 action, action(function()) 异步 action 中一般都会调用同步 action,异步 action 不是必要的
*/
import {INCREMENT} from './constant'
// 同步 action
export const createIncrementAction = data => ({type: INCREMENT, data})
// 异步 action : 注意异步action 里面调用 dispatch 的写法
export const createIncrementAsyncAction = (data, time) => {
return (dispatch)=>{
setTimeout(()=>{
dispatch(createIncrementAction(data))
}, time)
}
}
constant.js
放置容易写错 action 中的 type 值
/**
* 该模块用于定义action 对象中 type 类型的常量值
*/
export const INCREMENT = 'increment'
CountRedux.jsx
使用 action 的组件,注意 store.subscribe 的写法
在组件的 componentDidMount 中或在 index.js 中通过 store.subscribe() 检测 store 中状态的改变
一旦改变重新调用 this.setState({}) 或 重新渲染
import React, { Component } from 'react'
// 引入 store 用于获取 redux 中保存的状态
import store from '../redux/store'
// 引入 actionCreator, 用于创建 action 对象
import {createIncrementAction, createIncrementAsyncAction} from '../redux/count_action'
export default class CountRedux extends Component {
componentDidMount(){
// 检测 redux 中状态的变化,只要变化,就调用 setState 触发 render 刷新
store.subscribe(()=> {
this.setState({})
})
/**
store.subscribe 也可以写在入口文件 index.js - 一劳永逸
store.subscribe(()=> {
root.render( );
})
*/
}
increment = ()=> {
let {value} = this.selectNumber
// 通知 redux 操作 同步
store.dispatch(createIncrementAction(value*1))
}
incrementAsync = ()=> {
let {value} = this.selectNumber
// setTimeout(()=>{
// store.dispatch(createIncrementAction(value*1))
// }, 1000)
// 异步 action,与上面的 setTimeout 效果一致
store.dispatch(createIncrementAsyncAction(value*1, 1000))
}
render() {
return (
<div>
<div>当前求和为: { store.getState() }</div>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.incrementAsync }>异步+</button>
</div>
)
}
}
react-redux 是由 react 官方提供,redux不是 react 官方提供
react-redux 实际也是通过 redux 库创建 store ,再由 react-redux 的 connect 函数创建容器组件,并由组件容器与store交互,而不是ui组件自身去触发
注意: React-redux 的store.js、action.js、reducer.js、constant.js 创建,以及合并多个redux都与 redux 一样
1、所有的 UI 组件都应该包裹一个容器组件,他们是父子关系
2、容器组件是真正和 redux 打交道的,里面可以随意的使用 redux 的 api
3、UI 组件不能使用任何 redux 的 api
4、容器组件会传给 UI 组件:1)、redux 中所保存的状态,2)、用于操作状态的方法
5、UI 组件放在 component,容器组件放在 containers
6、备注:容易给 UI 组件传递:状态、操作状态的方法,均通过 props 传递
基本使用:
1、容器组件和ui组件整合一个文件
UI 组件:不能使用任何 redux 的 api,只负责页面的呈现、交互等,在 UI 组件中通过 this.props.key 读取和操作状态
容器组件: 由 react-redux 的 connect 函数创建,负责和 redux 通信,将结果与操作状态的方法传递给 UI 组件
创建一个容器组件:
connect(mapStateToProps,mapDispatchToProps)(UI 组件)
mapStateToProps:映射状态,返回值是一个对象
mapDispatchToProps: 映射操作状态的方法,返回值是一个对象, 可以简写成一个对象
简写语法:
connect(
state => {key: value}, // 映射状态
{key: xxxAction} // 映射操作状态的方法
)(UI组件)
// 整合容器组件和ui组件示例代码
import React, { Component } from 'react'
// 引入 connect 用于连接 ui 组件 与 redux
import {connect} from 'react-redux'
// 引入 action 方法
import {increment, incrementAsync} from '../../redux/action/count'
// 此处的 ui 组件不暴露
class Count extends Component {
increment = ()=> {
let {value} = this.selectNumber
this.props.increment(value*1)
}
incrementAsync = ()=> {
let {value} = this.selectNumber
this.props.incrementAsync(value*1, 2000)
}
render() {
return (
<div>
<h2>我是Count组件</h2>
<div>总人数为: {this.props.presonCount},当前求和为: { this.props.count }</div>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
/** 引入 connect 生成一个 Count 容器组件,并暴露 */
export default connect(
// mapStateToProps 的简写
state => ({ count: state.count, presonCount: state.personObj.length }),
// mapDispatchToProps 一般写法
// dispatch => ({
// increment: number => dispatch(increment(number)),
// incrementAsync: (number, time) => dispatch(incrementAsync(number, time))
// })
// mapDispatchToProps 的简写
{
increment,
incrementAsync
})(Count)
2、无需自己给容器组件传递 store, 给 包裹一个 即可
使用react-redux 后也不用再自己检测redux 中状态的改变,容器组件可以自动完成这个工作
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// Provider 用于批量给组件传递 store
import store from './redux/store';
import {Provider} from 'react-redux'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
{/* 此处需要用 Provider 包裹 App, 目的是让 App 的所有后代容器组件都能接收到 store */}
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
打包后可以借助serve库运行打包好的文件
安装: npm i serve -g
到打包好的index目录运行:serve
setState 更新状态的2种写法:
1、setState(stateChange, [callback]) 对象式的 setState
1)、stateChange 为状态改变对象(该对象可以体现出状态的更改)
2)、callback 是可选的回调函数,它在状态更新完毕,界面也更新后(render调用后)才被调用
2、setState(updater, [callback]) 函数式的 setState
1)、updater 为返回 stateChange 对象的函数
2)、updater 可以接收到 state 和 props
3)、callback 是可选的回调函数,它在状态更新、界面也更新后(render调用后)才被调用
总结:
对象式的 setSate 是函数式的 setState 的简写方式(语法糖)
使用原则:
如果新状态不依赖原状态,使用对象方式
如果新状态依赖原状态,使用函数方式
如果需要在 setState() 执行后获取最新的状态数据,要在第二个 callback 函数中读取
路由组件的 lazyLoad
1、通过react的lazy函数配合import()函数动态加载路由组件 ===》 路由组件代码会被分开打包
const Login = Lazy(()=>import('@/pages/Login'))
2、通过指定在加载得到路由打包文件前显示一个自定义 loading 界面
<Suspense fallback={<h1>loading...</h1>}>
<Switch>
<Route path='/xxx' componet={Xxx}>
<Redirect to="login">
</Switch>
</Suspense>
3、 标签的 fallback 可以传入一个组件
// 注意写在 Suspense fallback 中的 Loading 组件不能使用懒加载
import Loading from '../Loading'
<Suspense fallback={<Loading/>}>
<Switch>
<Route path='/xxx' componet={Xxx}>
<Redirect to="login">
</Switch>
</Suspense>
Hook是React 16.8.0版本增加的新特性/新语法,可以在函数组件中使用 state 以及其他的 React 特性
三个常用的 Hooks
State Hook:React.useState()
Effect Hook: React.useEffect()
Ref Hook: React.useRef()
State Hook
State Hook 能让函数组件也可以有 state 状态,并进行状态数据的读写操作
语法:const [xxx, setXxxx] = React.useState(initValue)
useState()说明:
参数:第一次初始化指定的值在内部作缓存
返回值:包含2个元素的数组,第1个为内部当前状态值,第2个为更新状态值的函数
setXxx 2种写法:
setXxx(newValue):参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值
setXxx(value => newValue): 参数为函数,接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值
import React from 'react'
function Demo(){
const [count, setCount] = React.useState(0)
const [name, setName] = React.useState('橘宝')
function add() {
// 第一种写法
// setCount(count + 1)
// 第二种写法
setCount(count => count+1)
}
function changeName() {
setName('花姐')
}
return (
<div>
<h2>当前数字为{count}, 名字:{name}</h2>
<button onClick={add}>加1</button>
<button onClick={changeName}>改名</button>
</div>
)
}
export default Demo
Effect Hook
Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
React 中的副作用操作:发送ajax请求、设置订阅/启动定时器、手动更改真实DOM
语法和说明:
React.useEffect(()=>{
// 在此处可以执行任何带副作用操作, 相当于 componentDidMount 与 componentDidUpdate比如页面初始化,加载数据等
return()=>{
// 在组件卸载前执行,相当于 componentWillUnmount,可以做一些收尾工作,比如清除定时器,取消订阅等
}
}, [stateVlaue])
// 如果指定的是[],回调函数只会在第一次render() 后执行
// 如果不指定,则会监听所有的 state 更新,并每次更新都执行,如果传对应的state,则监听对应的state
可以把 useEffect Hook 看做是三个函数的结合:
componentDidMount()、
componentDidUpdate()、
componentWillUnmount()
import React from 'react'
import root from '../../index'
function Demo(){
const [count, setCount] = React.useState(0)
React.useEffect(()=> {
// 此处返回的函数体相当于 componentDidMount 与 componentDidUpdate
let timer = setInterval(()=>{
// 注意此处的setCount需要写函数形式,否则拿不到count
setCount(count => count+1)
}, 1000)
// 此处返回的函数相当于 componentWillUnmount
return () => {
clearInterval(timer)
}
}, [])
function unmount() {
// 注意 18 版本的卸载,与之前的区别,需要在入口文件中,将 root 暴露
root.unmount()
}
return (
<div>
<h2>当前数字为{count}</h2>
<button onClick={unmount}>卸载组件</button>
</div>
)
}
export default Demo
Ref Hook
Ref Hook 可以在含糊组件中储存/查找组件内的标签或任意其他数据
语法:const refContainer = useRef()
作用:保存标签对象,功能与 React.createRef() 一样
import React from 'react'
function Demo(){
const myRef = React.useRef()
function show() {
console.log(myRef.current.value)
}
return (
<div>
<input type="text" ref={myRef}/>
<button onClick={show}>打印数据</button>
</div>
)
}
export default Demo
import React, { Component, Fragment } from 'react'
export default class Demo extends Component {
render() {
return (
<Fragment key={1}>
<div>Fragment 标签可以代表外层根节点,并且会被解析,页面渲染时不显示</div>
<div>Fragment 可以设置key值</div>
</Fragment>
)
}
}
用于组件间通信方式,常用于【祖组件】与【后代组件】间通信
注意:在应用开发中,一般不用context,一般用来封装 react 插件
1)、创建 Context 容器对象
const XxxContext = React.createContext()
2)、渲染子组件时,外面包裹 XXXContext.Provider,通过 Value 属性给后代组件传递数据:
子组件
3)、后代组件读取数据:
// 第一种方式:适用于类组件
static contextType = XxxContext // 声明接收 context
this.context // 读取context的value数据
// 第二种方式:函数组件与类组件都可以
{
value => { 要显示的内容 } // value 就是context中的value数据
}
import React, { Component } from 'react'
const MyContext = React.createContext()
const {Provider, Consumer} = MyContext
export default class A extends Component {
state = {username:'花姐'}
render() {
let {username} = this.state
return (
<div>
<p>我是A组件,我的用户名是: {username}</p>
<Provider value={{username, age: 5}}>
<B/>
</Provider>
</div>
)
}
}
class B extends Component {
render() {
return (
<div>
<p>我是B组件</p>
<C/>
<D/>
</div>
)
}
}
// 类式接收context
class C extends Component {
// 声明接收context
static contextType = MyContext
render() {
const {username, age} = this.context
return (
<div>
<p>我是类式 C 组件,A组件的用户名是:{username}, {age}岁</p>
</div>
)
}
}
// 函数接收context
function D() {
return (
<div>
<p>我是函数式 D 组件,A组件的用户名是:
<Consumer>
{
value => `${value.username}, ${value.age}`
}
</Consumer>
岁</p>
</div>
)
}
Component 的2个问题
1)、只要执行了setState,即使不改变状态数据,组件也会重新render()
2)、即使子组件没有用到父组件的任何数据,只要父组件重新render(),就会自动重新render子组件 ==> 效率低
原因:
Component中的 shouldComponentUpdate()总是返回 true
效率高的做法:
只有当组件的 state 或 props 数据发生改变时才重新render()
解决:
方法1:重写 shouldComponentUpdate() 方法,比较新旧 state 或 props 数据,如果有变化才返回 true,如果没有返回false
import React, { Component } from 'react'
export default class Parent extends Component {
state = {catName: '花姐'}
changeCat = ()=> {
this.setState({catName: '橘宝'})
}
shouldComponentUpdate(nextProps, nextState){
if(this.state.catName === nextState.catName) return false
return true
}
render() {
console.log('Parent---Chils')
return (
<div>
<h3>Parent,{this.state.catName}</h3>
<button onClick={this.changeCat}>改名</button>
<Chils catName="橘宝"/>
</div>
)
}
}
class Chils extends Component {
shouldComponentUpdate(nextProps, nextState){
if(this.props.catName === nextProps.catName) return false
return true
}
render() {
console.log('render---Chils')
return (
<div>
<h3>Chils</h3>
</div>
)
}
}
方法2:,使用PureComponent,PureComponent 重写了 shouldComponentUpdate(),只有sate或props数据有变化才返回true
注意:
只是进行 state 与 props 数据的浅比较,如果只是数据对象内部变了,返回false
不要直接修改 state 数据,而是要产生新数据
项目中一般用 PureComponent 来优化
import React, { PureComponent } from 'react'
export default class Parent extends PureComponent {
state = {catName: '花姐'}
changeCat = ()=> {
this.setState({catName: '橘宝'})
}
render() {
console.log('Parent---Chils')
return (
<div>
<h3>Parent,{this.state.catName}</h3>
<button onClick={this.changeCat}>改名</button>
<Chils catName="橘宝"/>
</div>
)
}
}
class Chils extends PureComponent {
render() {
console.log('render---Chils')
return (
<div>
<h3>Chils</h3>
</div>
)
}
}
向组件内部动态传入带内容的结构
vue 中:
使用slot 技术,通过组件标签体传入结构
React中:
使用 children props:通过组件标签体传入结构
使用 render props:通过组件标签属性传入结构,一般用 render 函数属性
children Props:
语法:
A组件渲染:{this.props.children }
问题:如果B组件需要 A组件内的数据,无法传递
render Props:
语法: }
A组件:{this.props.render(内部state数据)}
B组件:读取A组件传入的数据显示 {this.props.data}
import React, { Component } from 'react'
export default class Parent extends Component {
render() {
return (
<div>
<p>我是Parent组件</p>
// 组件的标签属性 render 返回一个组件
<ChilsA render={name => <ChilsB name={name} />} />
</div>
)
}
}
class ChilsA extends Component {
state = {name: 'ChilsA'}
render() {
return (
<div>
<p>我是ChilsA组件</p>
// 使用props.render 渲染传入的组件
{this.props.render(this.state.name)}
</div>
)
}
}
class ChilsB extends Component {
render() {
return (
<div>
<p>我是ChilsB组件, 是{this.props.name}的子组件</p>
</div>
)
}
}
错误边界 Error boundary :用来捕获后代组件错误,渲染出备用页面
特点:
只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
使用方式:
getDerivedStateFromError 配合 componentDidCatch
// 当Parent的子组件出现报错稍后,会触发 getDerivedStateFromError 调用,并携带错误信息
static getDerivedStateFromError(error){
return {hasError: error}
}
// 子组件渲染错误会调用-给后台做出反馈
componentDidCatch(){
console.log('统计错误此处,反馈给服务器,用于通知编码人员进行bug的解决')
}
import React, { Component } from 'react'
export default class Parent extends Component {
state = {
hasError:'', // 用于标识子组件是否产生错误
}
// 当Parent的子组件出现报错稍后,会触发 getDerivedStateFromError 调用,并携带错误信息
static getDerivedStateFromError(error){
return {hasError: error}
}
// 子组件渲染错误会调用-给后台做出反馈
componentDidCatch(){
console.log('统计错误此处,反馈给服务器,用于通知编码人员进行bug的解决')
}
render() {
return (
<div>
<p>我是Parent组件</p>
{this.state.hasError? <h2>当前网络不稳定,请稍后再试</h2>: <ChilsA />}
</div>
)
}
}
class ChilsA extends Component {
state = {list:'123'}
render() {
return (
<div>
<p>我是ChilsA组件</p>
{
this.state.list.map((item,index) => {
return <div key={index}>{item.name}</div>
})
}
</div>
)
}
}
组件间的关系: 父子组件、兄弟组件(非嵌套组件)、祖孙组件(跨级组件)
几种通信方式:
1、props:
1)、children props
父传子: 通过标签属性传递,子组件通过 props 获取
子传父:props+回调的⽅式,父组件通过 props 传递给子组件一个函数,子组件调用这个函数
2)、render props
父传子: 通过组件标签属性 render 传入结构, 父组件通过 {this.props.render(内部state数据)} 渲染
子组件: {this.props.data} 读取数据
状态提升:某些组件的 state 放在它们共同的父组件 state 中
2、消息订阅 - 发布:
发布者发布事件(PubSub.publish()),订阅者监听事件(PubSub.subscribe())并做出反应,我们可以通过引⼊event 模块进⾏通信 -- 适用于任意组件间的通讯
3、集中式管理:
借助 Redux 或者 Mobx 等全局状态管理⼯具进⾏通信,这种⼯具会维护⼀个全局状态中⼼Store,并根据不同的事件产⽣新的状态。
4、conText :
生产者-消费者模式
Context 设计⽬的是为了共享那些对于⼀个组件树⽽⾔是“全局”的数据,例如当前认证的户、主题或⾸选语⾔,对于跨越多层的全局数据通过 Context 通信再适合不过
比较好的搭配方式:
父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅发布、集中式管理、conText(开发用的少,封装插件用的多)