1. setState
setState更新状态其实有两种写法
setState这个方法在调用的时候是同步的,但是引起React的状态更新是异步的 【React状态更新是异步的】 为什么setState设计为异步的?
setState设计为异步,可以显著的提升性能:
- 如果每次调用setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;
- 如果同步更新了state,但是还没有执行render函数,而且peops依赖于state中的数据,那么state和props不能保持同步;pstate和props不能保持一致性,会在开发中产生很多的问题;
1.1 对象式
setState(stateChange, [callback])
- stateChange为状态改变对象 (该对象可以体现出状态的更改,是异步的)
- callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用【异步更新后调用,可以拿到更新后的状态的值】
1.2 函数式
setState(updater, [callback])
- updater为返回stateChange对象的函数【返回值是对象】
- updater可以接收到state和props
- callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用【和对象式是一样的】
1.3 代码
异步更新
函数式组件
import React, { Component } from 'react'
export default class Demo extends Component {
state = { count: 0 }
add = () => {
//对象式的setState
/* //1.获取原来的count值
const {count} = this.state
//2.更新状态
// 方式一: 获取异步更新后的数据
this.setState({count:count+1},()=>{
console.log(this.state.count); //1,异步的回调,能拿到setState更新后的值
})
//console.log('12行的输出',this.state.count); //0
setState对象状态的改变是异步的,先执行完代码,react才会帮你更改状态,不能拿到setState更新后的值
*/
//函数式的setState
this.setState((state, props) => { return { count: state.count + 1 } })
}
render() {
return (
<div>
<h1>当前求和为:{this.state.count}</h1>
<button onClick={this.add}>点我+1</button>
</div>
)
}
}
类式组件
import React, { Component } from 'react';
function Home(props) {
// Hello World
return <h1>{props.message}</h1>
}
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
message: "Hello World"
}
}
render() {
return (
<div>
<h2>当前计数: {this.state.message}</h2>
<button onClick={e => this.changeText()}>改变文本</button>
<Home message={this.state.message}/>
</div>
)
}
componentDidUpdate() {
// 方式二: 获取异步更新的state
console.log(this.state.message);
}
changeText() {
// 2.setState是异步更新
// this.setState({
// message: "你好啊,李银河"
// })
// console.log(this.state.message); // Hello World
// 方式一: 获取异步更新后的数据
// setState(更新的state, 回调函数)
this.setState({
message: "你好啊,李银河"
}, () => {
console.log(this.state.message);
})
}
}
同步更新
setState在正常状态下是异步更新的,但在如下情况下是同步更新的:
// 情况一: 将setState放入到定时器中
setTimeout(() => {
this.setState({
message: "你好啊,李银河"
})
console.log(this.state.message);
}, 0);
// 情况二: 原生dom事件中
componentDidMount() {
document.getElementById("btn").addEventListener("click", (e) => {
this.setState({
message: "你好啊,李银河"
})
console.log(this.state.message);
})
}
1.4 setState数据的合并与本身的合并
我通过setState去修改message,是不会对name产生影响的;源码中其实是有对原对象和新对象进行合并的:
数据合并代码:
通过Object.assign方法,将后两个复制到目标对象,其中第二个参数中如果存在和第三个参数相同的属性,则将其替换成第三个参数的内容
import React, { Component } from 'react';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
message: "Hello World",
name: "coderwhy"
}
}
render() {
return (
<div>
<h2>{this.state.message}</h2>
<h2>{this.state.name}</h2>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}
changeText() {
// 了解真相你才能获得真正的自由
this.setState({
message: "你好啊,李银河"
});
// Object.assign({}, this.state, {message: "你好啊,李银河"})
}
}
本身合并代码:
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
render() {
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
</div>
)
}
increment() {
// 1.setState本身被合并,不论调用几次都只能加1
// this.setState({
// counter: this.state.counter + 1
// });
// this.setState({
// counter: this.state.counter + 1
// });
// this.setState({
// counter: this.state.counter + 1
// });
// 2.setState合并时进行累加
// this.setState((上一次的state, props)
this.setState((prevState, props) => {
return {
counter: prevState.counter + 1
}
});
this.setState((prevState, props) => {
return {
counter: prevState.counter + 1
}
});
this.setState((prevState, props) => {
return {
counter: prevState.counter + 1
}
});
}
}
1.5 state的引用数据尽量不要直接修改
friends是数组,是引用类型,存储的数据为一个指向堆中内存的指针,
push一个对象是将一个对象的引用加入到原数组在堆中的内存,之后
setState{ friends: this.state.friends } 是将原指针赋值给friends,使用
PureComponent(shouldComponentUpdate会比较新旧state)会出现问题,
使用Component没有上述问题,但是也不推荐直接修改
import React, { PureComponent } from "react";
export default class App extends PureComponent {
// 引用类型
state = {
friends: [
{ name: "lilei", age: 20 },
{ name: "lily", age: 25 },
{ name: "lucy", age: 22 },
],
};
// shouldComponentUpdate(newProps, newState) {
// if (newState.friends !== this.state.friends) {
// return true;
// }
// return false;
// }
render() {
return (
<div>
<h2>好友列表:</h2>
<ul>
{this.state.friends.map((item, index) => {
return (
<li key={item.name}>
姓名: {item.name}
年龄: {item.age}
<button onClick={() => this.incrementAge(index)}>age+1</button>
</li>
);
})}
</ul>
<button onClick={() => this.insertData()}>添加数据</button>
</div>
);
}
insertData() {
// 1.在开发中不要这样来做
// const newData = {name: "tom", age: 30}
// this.state.friends.push(newData);
// this.setState({
// friends: this.state.friends
// });
// 2.推荐做法
const newFriends = [...this.state.friends];
newFriends.push({ name: "tom", age: 30 });
this.setState({
friends: newFriends,
});
}
incrementAge(index) {
const newFriends = [...this.state.friends];
newFriends[index].age += 1;
this.setState({
friends: newFriends,
});
}
}
1.6 总结
对象式的setState是函数式的setState的简写方式【语法糖】 使用原则
- 如果新状态不依赖于原状态【使用对象方式】
- 如果新状态依赖于原状态 【使用函数方式】
- 如果需要在setState()执行后,获取最新的状态数据,可以在第二个callback函数中读取到异步更新的最新值
- 在组件生命周期或React合成事件中,setState是异步;
- 在setTimeout或者原生dom事件中,setState是同步;
- 不要直接修改state中的引用数据
2. lazyLoad
路由组件的lazyLoad
//1.通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包
const Login = lazy(()=>import('@/pages/Login'))
//2.通过<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面
<Suspense fallback={<h1>loading.....</h1>}>
<Switch>
<Route path="/xxx" component={Xxxx}/>
<Redirect to="/login"/>
</Switch>
</Suspense>
案例
import React, { Component,lazy,Suspense} from 'react'
import {NavLink,Route} from 'react-router-dom'
// import Home from './Home'
// import About from './About'
//Loading组件用于懒加载未加载出来时展示的内容,不能也用懒加载,需要正常引入
import Loading from './Loading'
//懒加载引入Home,About组件,注意这里箭头函数不能用{}
const Home = lazy(()=> import('./Home') )
const About = lazy(()=> import('./About'))
export default class Demo extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 在React中靠路由链接实现切换组件--编写路由链接 */}
<NavLink className="list-group-item" to="/about">About</NavLink>
<NavLink className="list-group-item" to="/home">Home</NavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{通过<Suspense>包裹并返回一个自定义loading界面}
<Suspense fallback={<Loading/>}>
{/* 注册路由 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</Suspense>
</div>
</div>
</div>
</div>
</div>
)
}
}
3. Fragment
使用:
<Fragment></Fragment>
//短语法
<></>
注意:Fragment只用拥有key属性,<></>什么属性都不能拥有
作用:
可以不用必须有一个真实的DOM根标签了,用来代替最外层div
import React, { PureComponent, Fragment } from "react";
export default class App extends PureComponent {
state = {
counter: 0,
friends: [
{ name: "why", age: 18 },
{ name: "lilei", age: 20 },
{ name: "kobe", age: 25 },
],
};
render() {
return (
<>
{this.state.friends.map((item, index) => {
return (
<Fragment key={item.name}>
<div>{item.name}</div>
<p>{item.age}</p>
<hr />
</Fragment>
);
})}
</>
);
}
}
4. StrictMode
//index.jsx 为所有组件开启严格模式检查
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
- StrictMode 是一个用来突出显示应用程序中潜在问题的工具.
- 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI.
- 它为其后代元素触发额外的检查和警告.
- 严格模式检查仅在开发模式下运行;它们不会影响生产构建.
- 可以为应用程序的任何部分启用严格模式.
- 如下图案例: 不会对 Header 和 Footer 组件运行严格模式检查; 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查
严格模式检查的是什么?
- 识别不安全的生命周期:
- 使用过时的ref API
- 使用废弃的findDOMNode方法 :
在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
- 检查意外的副作用 :
严格模式下组件的constructor会被调用两次; 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用; 在生产环境中,是不会被调用两次的;
- 检测过时的context API :
早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的; 目前这种方式已经不推荐使用
5. Context
理解
一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信
Context相关API
React.createContext :
- 创建一个需要共享的Context对象:
- 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
- defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
Context.Provider: - 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
- Provider 接收一个 value 属性,传递给消费组件;
- 一个 Provider 可以和多个消费组件有对应关系;
- 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
- 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
Class.contextType - 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
- 这能让你使用 this.context 来消费最近 Context 上的那个值; 你可以在任何生命周期中访问到它,包括 render 函数中;
Context.Consumer - 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
- 这里需要 函数作为子元素(function as child)这种做法;
- 这个函数接收当前的 context 值,返回一个 React 节点;
使用
1) 创建Context容器对象:
const XxxContext = React.createContext()
2) 渲染子组件时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
<xxxContext.Provider value={数据}>
子组件
</xxxContext.Provider>
3) 后代组件读取数据:
//第一种方式:仅适用于类组件
static contextType = xxxContext // 声明接收context
this.context // 读取context中的value数据
//第二种方式: 函数组件与类组件都可以,但要是使用多个嵌套的话只能用这个(函数,类)
<xxxContext.Consumer>
{
value => ( // value就是context中的value数据
要显示的内容
)
}
</xxxContext.Consumer>
注意
在应用开发中一般不用context, 一般都它的封装react插件
案例
import React, { Component } from 'react'
import './index.css'
//创建Context对象
const MyContext = React.createContext(默认值,当value无效时使用)
// 解构赋值,取出Provider和Consumer,简化后续写法
const { Provider, Consumer } = MyContext
export default class A extends Component {
state = { username: 'tom', age: 18 }
render() {
const { username, age } = this.state
return (
<div className="parent">
<h3>我是A组件</h3>
<h4>我的用户名是:{username}</h4>
<Provider value={{ username: username, age: age }}>
<B />
</Provider>
</div>
)
}
}
// 不需要写任何东西
class B extends Component {
render() {
return (
<div className="child">
<h3>我是B组件</h3>
<C />
</div>
)
}
}
// 类式组件
/* class C extends Component {
//声明接收context
static contextType = MyContext
render() {
const {username,age} = this.context
return (
<div className="grand">
<h3>我是C组件</h3>
<h4>我从A组件接收到的用户名:{username},年龄是{age}</h4>
</div>
)
}
} */
// 函数式组件
function C() {
return (
<div className="grand">
<h3>我是C组件</h3>
<h4>我从A组件接收到的用户名:
<Consumer>
{
value => { return `${value.username},年龄是${value.age}` }
}
</Consumer>
</h4>
</div>
)
}
多层嵌套
import React, { Component } from 'react';
// 创建Context对象
const UserContext = React.createContext({
nickname: "aaaa",
level: -1
})
const ThemeContext = React.createContext({
color: "black"
})
function ProfileHeader() {
return (
<UserContext.Consumer>
{
value => {
return (
<ThemeContext.Consumer>
{
theme => {
return (
<div>
<h2 style={{color: theme.color}}>用户昵称: {value.nickname}</h2>
<h2>用户等级: {value.level}</h2>
<h2>颜色: {theme.color}</h2>
</div>
)
}
}
</ThemeContext.Consumer>
)
}
}
</UserContext.Consumer>
)
}
function Profile(props) {
return (
<div>
<ProfileHeader />
<ul>
<li>设置1</li>
<li>设置2</li>
<li>设置3</li>
<li>设置4</li>
</ul>
</div>
)
}
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
nickname: "kobe",
level: 99
}
}
render() {
return (
<div>
<UserContext.Provider value={this.state}>
<ThemeContext.Provider value={{ color: "red" }}>
<Profile />
</ThemeContext.Provider>
</UserContext.Provider>
</div>
)
}
}
6. 组件性能优化
Component的2个问题
只要执行setState(),即使不改变状态数据, 组件也会重新render() ==> 效率低
只当前组件重新render(), 就会自动重新render子组件,纵使子组件没有用到父组件的任何数据 ==> 效率低
效率高的做法
只有当组件的state或props数据发生改变时才重新render()
原因
Component中的生命周期钩子shouldComponentUpdate()总是返回true
React给我们提供了一个生命周期方法shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值
该方法有两个参数:
- 参数一:nextProps修改之后,最新的props属性
- 参数二:nextState修改之后,最新的state属性 该方法返回值是一个boolean类型
- 返回值为true,那么就需要调用render方法;
- 返回值为false,那么就不需要调用render方法; 默认返回的是true,也就是只要state发生改变,就会调用render方法;
解决
办法1:
重写shouldComponentUpdate()方法
比较新旧state或props数据, 如果有变化才返回true, 如果没有返回false
实例:
import React, {Component} from 'react';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
message: "Hello World"
}
}
render() {
console.log("App render函数被调用");
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}
shouldComponentUpdate(nextProps, nextState) {
//如果counter更新,则重新调用render方法,其他情况不重新调用render
if (this.state.counter !== nextState.counter) {
return true;
}
return false;
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
changeText() {
this.setState({
message: "你好啊,李银河"
})
}
}
如果所有的类,我们都需要手动来实现shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。 我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?
props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false; 事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?将class继承自PureComponent。
办法2:
使用PureComponent
PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true
注意:
只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false
不要直接修改state数据, 而是要产生新数据
项目中一般使用PureComponent来优化
实例:
import React, { PureComponent } from 'react' //项目中一般使用PureComponent
import './index.css'
export default class Parent extends PureComponent {
state = { carName: "奔驰c36", stus: ['小张', '小李', '小王'] }
addStu = () => {
/*错误写法,这样无效,因为还是同一个对象
const {stus} = this.state
stus.unshift('小刘')
this.setState({stus}) */
// 正确写法
const { stus } = this.state
this.setState({ stus: ['小刘', ...stus] })
}
changeCar = () => {
this.setState({ carName: '迈巴赫' })
}
/*
shouldComponentUpdate(nextProps, nextState) {
// console.log(this.props,this.state); //目前的props和state
// console.log(nextProps,nextState); //接下要变化的目标props,目标state
if (this.state.carName === nextState.carName) {
return false
} else {
return true
}
}
*/
render() {
console.log('Parent---render');
const { carName } = this.state
return (
<div className="parent">
<h3>我是Parent组件</h3>
{this.state.stus}
<span>我的车名字是:{carName}</span><br />
<button onClick={this.changeCar}>点我换车</button>
<button onClick={this.addStu}>添加一个小刘</button>
<Child carName="奥拓" />
</div>
)
}
}
class Child extends PureComponent {
/* shouldComponentUpdate(nextProps,nextState){
console.log(this.props,this.state); //目前的props和state
console.log(nextProps,nextState); //接下要变化的目标props,目标state
return !(this.props.carName === nextProps.carName)
} */
render() {
console.log('Child---render');
return (
<div className="child">
<h3>我是Child组件</h3>
<span>我接到的车是:{this.props.carName}</span>
</div>
)
}
}
但还是有一个问题PureComponent只能解决类式组件的性能优化问题,无法解决函数式组件的性能优化。函数式组件的性能优化要用高阶组件memo,详细用法可以看我的另一篇文章 React Hooks 全解
7. 用React实现插槽
React本身是没有插槽这个概念的,但可以React的灵活性可以让其实现类似插槽的功能 App.jsx
import React, { Component } from "react";
import NavBar from "./NavBar";
import NavBar2 from "./NavBar2";
export default class App extends Component {
render() {
const leftJsx = <span>aaa</span>;
return (
<div>
<NavBar name="" title="" className="">
<span>aaa</span>
<strong>bbb</strong>
<a href="/#">ccc</a>
</NavBar>
<NavBar2
leftSlot={leftJsx}
centerSlot={<strong>bbb</strong>}
rightSlot={<a href="/#">ccc</a>}
/>
</div>
);
}
}
方法一:NavBar.jsx
利用props.children属性实现插槽功能
import React, { Component } from "react";
export default class NavBar extends Component {
render() {
console.log(this.props.children[0]);
return (
<div className="nav-item nav-bar">
<div className="nav-left">{this.props.children[0]}</div>
<div className="nav-item nav-center">{this.props.children[1]}</div>
<div className="nav-item nav-right">{this.props.children[2]}</div>
</div>
);
}
}
方法二:NavBar2.jsx 利用props的属性实现插槽
import React, { Component } from "react";
export default class NavBar2 extends Component {
render() {
const { leftSlot, centerSlot, rightSlot } = this.props;
return (
<div className="nav-item nav-bar">
<div className="nav-left">{leftSlot}</div>
<div className="nav-item nav-center">{centerSlot}</div>
<div className="nav-item nav-right">{rightSlot}</div>
</div>
);
}
}
style.css
body {
padding: 0;
margin: 0;
}
.nav-bar {
display: flex;
}
.nav-item {
height: 44px;
line-height: 44px;
text-align: center;
}
.nav-left, .nav-right {
width: 70px;
background-color: red;
}
.nav-center {
flex: 1;
background-color: blue;
}
8. 错误边界
理解:
错误边界:用来捕获后代组件错误,渲染出备用页面
特点:
只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
使用方式:
getDerivedStateFromError配合componentDidCatch
// 生命周期函数,一旦后台组件报错,就会触发
static getDerivedStateFromError(error) {
console.log(error);
// 在render之前触发
// 返回新的state
return {
hasError: true,
};
}
componentDidCatch(error, info) {
// 统计页面的错误。发送请求发送到后台去
console.log(error, info);
}
Parent
import React, { Component } from 'react'
import Child from './Child'
export default class Parent extends Component {
state = {
hasError:'' //用于标识子组件是否产生错误
}
//当Parent的子组件出现报错时候,会触发getDerivedStateFromError调用,并携带错误信息
static getDerivedStateFromError(error){
console.log('@@@',error);
return {hasError:error}
}
componentDidCatch(){
console.log('此处统计错误,反馈给服务器,用于通知编码人员进行bug的解决');
}
render() {
return (
<div>
<h2>我是Parent组件</h2>
{this.state.hasError ? <h2>当前网络不稳定,稍后再试</h2> : <Child/>}
</div>
)
}
}
Child
import React, { Component } from 'react'
export default class Child extends Component {
state = {
users:[
{id:'001',name:'tom',age:18},
{id:'002',name:'jack',age:19},
{id:'003',name:'peiqi',age:20},
]
// users:'abc'
}
render() {
return (
<div>
<h2>我是Child组件</h2>
{
this.state.users.map((userObj)=>{
return <h4 key={userObj.id}>{userObj.name}----{userObj.age}</h4>
})
}
</div>
)
}
}
9. 组件通信方式总结
方式:
props:
(1).children props
(2).render props
消息订阅-发布:
pubs-sub、event等等
集中式管理:
redux、dva等等
conText:
生产者-消费者模式
组件间的关系:
父子组件:props
兄弟组件(非嵌套组件):消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(用的少)
10. 高阶函数与高阶组件
1. 高阶函数
如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
- 若A函数,接收的参数是一个或多个函数,那么A就可以称之为高阶函数。
- 若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
常见的高阶函数有:Promise、setTimeout、arr.map()等等 可以参考下面这一篇文章:
高阶函数,关于AOP,偏函数,柯里化都有不错的记录,感觉还是不错的 函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
function sum(a){
return(b)=>{
return (c)=>{
return a+b+c
}
}
}
const result = sum(1)(2)(3)
console.log(result);
案例1:
<script type="text/babel">
//创建组件
class Login extends React.Component{
//初始化状态
state = {
username:'', //用户名
password:'' //密码
}
//保存用户名到状态中
saveUsername = (event)=>{
this.setState({username:event.target.value})
}
//保存密码到状态中
savePassword = (event)=>{
this.setState({password:event.target.value})
}
//表单提交的回调
handleSubmit = (event)=>{
event.preventDefault() //阻止表单提交
const {username,password} = this.state
alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
}
render(){
return(
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" name="username"/>
密码:<input onChange={this.savePassword} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//渲染组件
ReactDOM.render(<Login/>,document.getElementById('test'))
</script>
上方受控组件的写法太过繁琐,可以用函数柯里化简写
<!-- 准备好一个“容器” -->
<div id="test"></div>
<script type="text/babel">
//创建组件
class Login extends React.Component{
//初始化状态
state = {
username:'', //用户名
password:'' //密码
}
//保存表单数据到状态中
saveFormData = (dataType)=>{
return (event)=>{
this.setState({[dataType]:event.target.value})
}
}
//表单提交的回调
handleSubmit = (event)=>{
event.preventDefault() //阻止表单提交
const {username,password} = this.state
alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
}
render(){
return(
<form onSubmit={this.handleSubmit}>
{/*事件中要求接收的是一个函数,即{}内是一个函数*/}
用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/>
密码:<input onChange={this.saveFormData('password')} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//渲染组件
ReactDOM.render(<Login/>,document.getElementById('test'))
</script>
<!-- 准备好一个“容器” -->
<div id="test"></div>
<script type="text/babel">
//创建组件
class Login extends React.Component{
//初始化状态
state = {
username:'', //用户名
password:'' //密码
}
//保存表单数据到状态中
saveFormData = (dataType,event)=>{
this.setState({[dataType]:event.target.value})
}
//表单提交的回调
handleSubmit = (event)=>{
event.preventDefault() //阻止表单提交
const {username,password} = this.state
alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
}
render(){
return(
<form onSubmit={this.handleSubmit}>
{/*事件中要求接收的是一个函数,即{}内是一个函数*/}
用户名:<input onChange={event => this.saveFormData('username',event) } type="text" name="username"/>
密码:<input onChange={event => this.saveFormData('password',event) } type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//渲染组件
ReactDOM.render(<Login/>,document.getElementById('test'))
</script>
2. 高阶组件
高阶组件的英文是 Higher-Order Components,简称为 HOC;
官方的定义:高阶组件是参数为组件,返回值为新组件的函数;
- 高阶组件 本身不是一个组件,而是一个函数;
- 这个函数的参数是一个组件,返回值也是一个组件;
- 高阶组件并不是React API的一部分,它是基于React的 组合特性而形成的设计模式;
- 高阶组件在一些React第三方库中非常常见:
- 比如redux中的connect;
- 比如react-router中的withRouter;
高阶组件的定义
高阶组件的调用过程类似于这样:
高阶函数的编写过程类似于这样:
组件的名称问题:
- 在ES6中,类表达式中类名是可以省略的;
- 组件的名称都可以通过displayName来修改;
//App.jsx
import React, { PureComponent } from "react";
//App组件想要使用props必须要从包裹他的高阶组件中得到
class App extends PureComponent {
render() {
return <div>App: {this.props.name}</div>;
}
}
// 类式组件
//function enhanceComponent(形参)
function enhanceComponent(WrappedComponent) {
class NewComponent extends PureComponent {
render() {
return <WrappedComponent {...this.props} />;
}
}
// 重新定义组件类名
NewComponent.displayName = "Kobe";
return NewComponent;
}
// 函数式组件
function enhanceComponent2(WrappedComponent) {
function NewComponent(props) {
return <WrappedComponent {...props} />;
}
// 重新定义函数组件名
NewComponent.displayName = "Kobe";
return NewComponent;
}
const EnhanceComponent = enhanceComponent2(App);
export default EnhanceComponent;
//index.js
//注意这里导出的App实际为EnhanceComponent,所以传递name属性传递给的是EnhanceComponent组件
import App from "./App.jsx";
ReactDOM.render(<App name="why" />, document.getElementById("root"));
应用一 – props的增强
为组件统一添加相同地区属性--不修改原有代码的情况下,添加新的props
import React, { PureComponent } from 'react';
// 定义一个高阶组件
function enhanceRegionProps(WrappedComponent) {
return props => {
return <WrappedComponent {...props} region="中国"/>
}
}
class Home extends PureComponent {
render() {
return <h2>Home: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
}
}
class About extends PureComponent {
render() {
return <h2>About: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2>
}
}
const EnhanceHome = enhanceRegionProps(Home);
const EnhanceAbout = enhanceRegionProps(About);
class App extends PureComponent {
render() {
return (
<div>
App
<EnhanceHome nickname="coderwhy" level={90}/>
<EnhanceAbout nickname="kobe" level={99}/>
</div>
)
}
}
export default App;
案例--利用高阶组件来共享Context:
原版:
import React, { PureComponent, createContext } from "react";
// 创建Context
const UserContext = createContext({
nickname: "默认",
level: -1,
区域: "中国",
});
class Home extends PureComponent {
render() {
return (
<UserContext.Consumer>
{(user) => {
return (
<h2>
Home:
{`昵称: ${user.nickname} 等级: ${user.level} 区域: ${user.region}`}
</h2>
);
}}
</UserContext.Consumer>
);
}
}
class About extends PureComponent {
render() {
return (
<UserContext.Consumer>
{(user) => {
return (
<h2>
About:
{`昵称: ${user.nickname} 等级: ${user.level} 区域: ${user.region}`}
</h2>
);
}}
</UserContext.Consumer>
);
}
}
class App extends PureComponent {
render() {
return (
<div>
App
<UserContext.Provider
value={{ nickname: "why", level: 90, region: "中国" }}
>
<Home />
<About />
</UserContext.Provider>
</div>
);
}
}
export default App;
高阶组件版:
import React, { PureComponent, createContext } from "react";
class Home extends PureComponent {
render() {
return (
<h2>
Home:
{`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region} 标识:${this.props.id}`}
</h2>
);
}
}
class About extends PureComponent {
render() {
return (
<h2>
About:
{`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region} 标识:${this.props.id}`}
</h2>
);
}
}
class Detail extends PureComponent {
render() {
return (
<ul>
<li>{this.props.nickname}</li>
<li>{this.props.level}</li>
<li>{this.props.region}</li>
<li>{this.props.id}</li>
</ul>
);
}
}
// 定义一个高阶组件
function withUser(WrappedComponent) {
return (props) => {
return (
<UserContext.Consumer>
{(user) => {
console.log(props, user);
return <WrappedComponent {...props} {...user} />;
}}
</UserContext.Consumer>
);
};
}
const UserHome = withUser(Home);
const UserAbout = withUser(About);
const UserDetail = withUser(Detail);
// 创建Context
const UserContext = createContext({
nickname: "默认",
level: -1,
区域: "中国",
});
class App extends PureComponent {
render() {
return (
<div>
App
<UserContext.Provider
value={{ nickname: "why", level: 90, region: "中国" }}
>
<UserHome id={1} />
<UserAbout id={2} />
<UserDetail id={3} />
</UserContext.Provider>
</div>
);
}
}
export default App;
应用二 – 渲染判断鉴权
在开发中,我们可能遇到这样的场景: 某些页面是必须用户登录成功才能进行进入; 如果用户没有登录成功,那么直接跳转到登录页面; 这个时候,我们就可以使用高阶组件来完成鉴权操作.
import React, { PureComponent } from 'react';
// 登录组件
class LoginPage extends PureComponent {
render() {
return <h2>LoginPage</h2>
}
}
// 购物车组件
class CartPage extends PureComponent {
render() {
return <h2>CartPage</h2>
}
}
// 高阶组件
function withAuth(WrappedComponent) {
const NewCpn = (props) => {
const {isLogin} = props;
if (isLogin) {
return <WrappedComponent {...props}/>
} else {
return <LoginPage/>
}
}
NewCpn.displayName = "AuthCpn"
return NewCpn;
}
const AuthCartPage = withAuth(CartPage);
export default class App extends PureComponent {
render() {
return (
<div>
<AuthCartPage isLogin={true}/>
</div>
)
}
}
应用三 – 生命周期劫持
我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:
默认版:
import React, { PureComponent } from 'react';
class Home extends PureComponent {
// 即将渲染获取一个时间 beginTime
UNSAFE_componentWillMount() {
this.beginTime = Date.now();
}
// 渲染完成再获取一个时间 endTime
componentDidMount() {
this.endTime = Date.now();
const interval = this.endTime - this.beginTime;
console.log(`Home渲染时间: ${interval}`)
}
render() {
return <h2>Home</h2>
}
}
class About extends PureComponent {
// 即将渲染获取一个时间 beginTime
UNSAFE_componentWillMount() {
this.beginTime = Date.now();
}
// 渲染完成再获取一个时间 endTime
componentDidMount() {
this.endTime = Date.now();
const interval = this.endTime - this.beginTime;
console.log(`About渲染时间: ${interval}`)
}
render() {
return <h2>About</h2>
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<Home />
<About />
</div>
)
}
}
高阶组件版:
import React, { PureComponent } from 'react';
//高阶组件
function withRenderTime(WrappedComponent) {
return class extends PureComponent {
// 即将渲染获取一个时间 beginTime
UNSAFE_componentWillMount() {
this.beginTime = Date.now();
}
// 渲染完成再获取一个时间 endTime
componentDidMount() {
this.endTime = Date.now();
const interval = this.endTime - this.beginTime;
console.log(`${WrappedComponent.name}渲染时间: ${interval}`)
}
render() {
return <WrappedComponent {...this.props}/>
}
}
}
class Home extends PureComponent {
render() {
return <h2>Home</h2>
}
}
class About extends PureComponent {
render() {
return <h2>About</h2>
}
}
const TimeHome = withRenderTime(Home);
const TimeAbout = withRenderTime(About);
export default class App extends PureComponent {
render() {
return (
<div>
<TimeHome />
<TimeAbout />
</div>
)
}
}
高阶函数的意义
我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理.
其实早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:
- Mixin 可能会相互依赖,相互耦合,不利于代码维护
- 不同的Mixin中的方法可能会相互冲突
- Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性 当然,HOC也有自己的一些缺陷: HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难; HOC可以劫持props,在不遵守约定的情况下也可能造成冲突; Hooks的出现,是开创性的,它解决了很多React之前的存在的问题 比如this指向问题、比如hoc的嵌套复杂度问题等等
11. ref的转发--forwardRef
在前面我们学习ref时讲过,ref不能应用于函数式组件: 因为函数式组件没有实例,所以不能获取到对应的组件对象 . 但是,在开发中我们可能想要获取函数式组件中某个元素的DOM,这个时候我们应该如何操作呢?
- 方式一:直接传入ref属性(错误的做法)
- 方式二:通过forwardRef高阶函数;
import React, { PureComponent, createRef, forwardRef } from "react";
class Home extends PureComponent {
render() {
return <h2>Home</h2>;
}
}
// function Profile(props) {
// return <h2>Profile</h2>;
// }
// 高阶组件forwardRef
const Profile = forwardRef(function (props, ref) {
return <p ref={ref}>Profile</p>;
});
export default class App extends PureComponent {
titleRef = createRef();
homeRef = createRef();
profileRef = createRef();
render() {
return (
<div>
<h2 ref={this.titleRef}>Hello World</h2>
<Home ref={this.homeRef} />
<Profile ref={this.profileRef} name={"why"} />
<button onClick={(e) => this.printRef()}>打印ref</button>
</div>
);
}
printRef() {
console.log(this.titleRef.current);
console.log(this.homeRef.current);
console.log(this.profileRef.current);
}
}
12. Portals的使用
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元 素上的).
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
- 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
- 第二个参数(container)是一个 DOM 元素;
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点.
然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的.
比如说,我们准备开发一个Modal组件,它可以将它的子组件渲染到屏幕的中间位置:
- 步骤一:修改index.html添加新的节点
- 步骤二:编写这个节点的样式
- 步骤三:编写组件代码
//App.jsx
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
class Modal extends PureComponent {
render() {
return ReactDOM.createPortal(
this.props.children,
document.getElementById("modal")
)
}
}
class Home extends PureComponent {
render() {
return (
<>
<h2>Home</h2>
<Modal>
<h2>Title</h2>
</Modal>
</>
)
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<Home/>
</div>
)
}
}
//index.html
<div id="root"></div>
<div id="modal"></div>
//index.css
#modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
//index.js
import App from "./App.jsx";
import "./css/index.css";
13. React过渡动画
1. react-transition-group介绍
- 在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验。
- 当然,我们可以通过原生的CSS来实现这些过渡动画,但是React社区为我们提供了react-transition-group用来完成过渡动画。
- React曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-group。
- react-transition-group本身非常小,不会为我们应用程序增加过多的负担。
- 这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装:
- 官网:reactcommunity.org/react-trans…
npm npm install react-transition-group --save
yarn yarn add react-transition-group
2. react-transition-group主要组件
react-transition-group主要包含四个组件:
- Transition
- 该组件是一个和平台无关的组件(不一定要结合CSS);
- 在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition;
- CSSTransition
- 在前端开发中,通常使用CSSTransition来完成过渡动画效果
- SwitchTransition
- 两个组件显示和隐藏切换时,使用该组件
- TransitionGroup
- 将多个动画组件包裹在其中,一般用于列表中元素的动画
3. CSSTransition
- CSSTransition是基于Transition组件构建的:
- CSSTransition执行过程中,有三个状态:appear、enter、exit;
- 它们有三种状态,需要定义对应的CSS样式:
- 第一类,开始状态:对于的类是-appear、-enter、exit;
- 第二类:执行动画:对应的类是-appear-active、-enter-active、-exit-active;
- 第三类:执行结束:对应的类是-appear-done、-enter-done、-exit-done;
- CSSTransition常见对应的属性:
- in:触发进入或者退出状态
- 如果添加了unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉;
- 当in为true时,触发进入状态,会添加-enter、-enter-acitve的class开始执行动画,当动画执行结束后,会移除两个class, 并且添加-enter-done的class;
- 当in为false时,触发退出状态,会添加-exit、-exit-active的class开始执行动画,当动画执行结束后,会移除两个class,并 且添加-exit-done的class; CSSTransition常见属性:
- in:触发进入或者退出状态
- classNames:动画class的名称 决定了在编写css时,对应的class名称:比如card-enter、card-enter-active、card-enter-done;
- timeout: 过渡动画的时间,到达设置时间后才会执行
onEntered与onExited - appear: 是否在初次进入添加动画(需要和in同时为true,然后再css中添加类xxx-appear设置初次进入动画)
- unmountOnExit:退出后卸载组件--为true则在退出时将dom元素卸载掉
- 其他属性可以参考官网来学习: reactcommunity.org/react-trans…
- CSSTransition对应的钩子函数:主要为了检测动画的执行过程,来完成一些JavaScript的操作
-
onEnter:在进入动画之前被触发;
-
onEntering:在应用进入动画时被触发;
-
onEntered:在应用进入动画结束后被触发
-
onExit:在退出动画之前被触发;
-
onExiting:在应用退出动画时被触发;
-
onExited:在应用退出动画结束后被触发
-
//CSSTransition.css
/* 进入开始/初次进入开始 */
.card-enter, .card-appear {
opacity: 0;
transform: scale(.6);
}
/* 进入过程中/初次进入过程中 */
.card-enter-active, .card-appear-active {
opacity: 1;
transform: scale(1);
transition: opacity 300ms, transform 300ms;
}
/* 进入完成/初次进入完成 */
.card-enter-done, .card-appear-done {
opacity: 1;
}
/* 退出开始 */
.card-exit {
opacity: 1;
transform: scale(1);
}
/* 退出过程中 */
.card-exit-active {
opacity: 0;
transform: scale(.6);
transition: all 300ms;
}
/* 退出完成 */
.card-exit-done {
opacity: 0;
}
//CSSTransition.js
import React, { PureComponent } from "react";
import { CSSTransition } from "react-transition-group";
import { Card, Avatar } from "antd";
import {
EditOutlined,
EllipsisOutlined,
SettingOutlined,
} from "@ant-design/icons";
const { Meta } = Card;
export default class CSSTransitionDemo extends PureComponent {
state = {
isShow: true,
};
render() {
const { isShow } = this.state;
return (
<div>
<button
onClick={(e) => {
this.setState({ isShow: !isShow });
}}
>
显示/隐藏
</button>
<CSSTransition
in={isShow}
classNames="card"
timeout={300}
unmountOnExit={true}
appear
onEnter={(el) => console.log("开始进入")}
onEntering={(el) => console.log("正在进入")}
onEntered={(el) => console.log("进入完成")}
onExit={(el) => console.log("开始退出")}
onExiting={(el) => console.log("退出状态")}
onExited={(el) => console.log("退出完成")}
>
<Card
style={{ width: 300 }}
cover={
<img
alt="example"
src="https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png"
/>
}
actions={[
<SettingOutlined key="setting" />,
<EditOutlined key="edit" />,
<EllipsisOutlined key="ellipsis" />,
]}
>
<Meta
avatar={
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
}
title="Card title"
description="This is the description"
/>
</Card>
</CSSTransition>
</div>
);
}
}
//App.js
import React, { PureComponent } from "react";
import CSSTransitionDemo from "./transition/CSSTransitionDemo";
export default class App extends PureComponent {
render() {
return (
<div style={{ textAlign: "center", padding: "30px" }}>
<CSSTransitionDemo />
</div>
);
}
}
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './transition/CSSTransition.css';
import 'antd/dist/antd.css';
ReactDOM.render(
<App />,
document.getElementById('root')
);
4. SwitchTransition
- SwitchTransition可以完成两个组件之间切换的炫酷动画:
- 比如我们有一个按钮需要在on和off之间切换,我们希望看到on先从左侧退出,off再从右侧进入;
- 这个动画在vue中被称之为 vue transition modes;
- react-transition-group中使用SwitchTransition来实现该动画;
- SwitchTransition中主要有一个属性:mode,有两个值
- in-out:表示新组件先进入,旧组件再移除;
- out-in:表示就组件先移除,新组建再进入;
- 如何使用SwitchTransition呢?
- SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件;
- SwitchTransition里面的CSSTransition或Transition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是 key属性;
//SwitchTransition.css
.btn-enter {
opacity: 0;
transform: translateX(100%);
}
.btn-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 1000ms, transform 1000ms;
}
.btn-exit {
opacity: 1;
transform: translateX(0);
}
.btn-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: all 1000ms;
}
//SwitchTransition.js
import React, { PureComponent } from "react";
import "./SwitchTransition.css";
import { SwitchTransition, CSSTransition } from "react-transition-group";
export default class SwitchTransitionDemo extends PureComponent {
state = {
isOn: true,
};
render() {
const { isOn } = this.state;
return (
<div>
<SwitchTransition mode="out-in">
<CSSTransition
key={isOn ? "on" : "off"}
classNames="btn"
timeout={1000}
>
<button onClick={(e) => this.setState({ isOn: !this.state.isOn })}>
{isOn ? "on" : "off"}
</button>
</CSSTransition>
</SwitchTransition>
</div>
);
}
}
5. TransitionGroup
当我们有一组动画时,需要将这些CSSTransition放入到一个TransitionGroup中来完成动画:
//TransitionGroup.css
.item-enter, .item-appear {
opacity: 0;
transform: scale(.6);
}
.item-enter-active ,.item-appear-active{
opacity: 1;
transform: scale(1);
transition: opacity 300ms, transform 300ms;
}
.item-enter-done ,.item-appear-done {
color: rgb(255, 136, 0);
}
.item-exit {
opacity: 1;
transform: scale(1);
}
.item-exit-active {
opacity: 0;
transform: scale(.6);
transition: opacity 300ms, transform 300ms;
}
.item-exit-done {
opacity: 0;
}
//TransitionGroup.js
import React, { PureComponent } from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import "./TransitionGroup.css";
export default class TransitionGroupDemo extends PureComponent {
state = {
names: ["coderwhy", "kobe", "lilei"],
};
render() {
return (
<div>
<TransitionGroup>
{this.state.names.map((item, index) => {
return (
<CSSTransition key={index} timeout={500} classNames="item" appear>
<div>
{item}
<button onClick={(e) => this.removeItem(index)}>-</button>
</div>
</CSSTransition>
);
})}
</TransitionGroup>
<button onClick={(e) => this.addName()}>+name</button>
</div>
);
}
addName() {
this.setState({
names: [...this.state.names, "zgc"],
});
}
removeItem(index) {
// index indey indez
this.setState({
names: this.state.names.filter((item, indey) => index !== indey),
});
}
}