章节介绍
小伙伴大家好,本章将学习React18核心概念与类组件使用 - 入门React18第一步。
本章学习目标
从本章开始将全面学习React18框架。核心知识点全面介绍,深入掌握各种高级特性及类组件的开发方式,并完成相关案例。
课程安排
- 02-虚拟DOM与React18新的渲染写法
- 03-什么是JSX及JSX详细使用方式(一)
- 04-什么是JSX及JSX详细使用方式(二)
- 05-如何进行条件渲染与列表渲染
- 06-类组件基本使用及组件通信
- 07-props细节详解及注意事项
- 08-类组件中事件的使用详解
- 09-类组件响应式视图实现与原理
- 10-state细节详解及React18的自动批处理
- 11-PureComponent与shouldComponentUpdate
- 12-immutable.js不可变数据集合
- 13-Refs操作DOM及操作类组件
- 14-详解受控组件及各种表单中的使用
- 15-详解非受控组件的实现方案
- 16-详解常见生命周期钩子函数
- 17-详解不常见生命周期钩子函数
- 18-组件内容的组合模式
- 19-复用组件功能之Render Props模式
- 20-复用组件功能之HOC高阶组件模式
- 21-组件跨层级通信方案Context
- 22-简易购物车的综合案例
- 23-章节总结
虚拟DOM与React18新的渲染写法
对虚拟DOM的理解
在学习React18之前,还是要对虚拟DOM进行深入了解。Vue和React框架都会自动控制DOM的更新,而直接操作真实DOM是非常耗性能的,所以才有了虚拟DOM的概念。
下面是一个多次触发DOM更新的例子和只触发一次DOM的一个对比。
<ul></ul>
<script>
// 多次触发DOM操作,非常耗时:1000ms左右
console.time(1)
let ul = document.querySelector('ul');
for(let i=0;i<1000;i++){
ul.innerHTML += `<li>${i}</li>`;
}
console.timeEnd(1)
</script>
<ul></ul>
<script>
// 只触发一次DOM操作,节约时间:1ms左右
console.time(1)
let ul = document.querySelector('ul');
let str = '';
for(let i=0;i<1000;i++){
str += `<li>${i}</li>`;
}
ul.innerHTML = str;
console.timeEnd(1)
</script>
所以在React18中,我们并不直接操作真实DOM,而是操作虚拟DOM,再一次性的更新真实DOM。
在React18中,需要使用两个文件来初始化框架:
- react.development.js 或 react模块 -> 生成虚拟DOM
- react-dom.development.js 或 react-dom/client模块 -> Diff算法 + 处理真实DOM
下面就是初始化React程序的代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../react.development.js"></script>
<script src="../react-dom.development.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// React对象 -> react.development.js
// ReactDOM对象 -> react-dom.development.js
let app = document.querySelector('#app');
// root根对象,react渲染的
let root = ReactDOM.createRoot(app);
// React.createElement() -> 创建虚拟DOM
let element = React.createElement('h2', {title: 'hi'}, 'hello world');
root.render(element);
</script>
</body>
</html>
这样在页面中就可以渲染一个h2
标签,并显示hello world
字样。
<div align=center>
<img src="./img/14-01-虚拟DOM渲染到页面.png" />
<div>虚拟DOM渲染到页面</div>
</div>
什么是JSX及JSX详细使用方式(一)
什么是JSX
在上一个小节中,我们利用React.createElement()
方法创建了虚拟DOM。但是这种方法非常的麻烦,如果结构非常复杂的情况下,那么是灾难性的,例如多添加一个span
标签,代码如下:
let element = React.createElement('h2', {title: 'hi'}, [
'hello world',
React.createElement('span', null, '!!!!!!')
]);
所以才有了JSX语法,即:这个有趣的标签语法既不是字符串也不是 HTML。它被称为 JSX,是一个 JavaScript 的语法扩展。
let element = <h2 title="hi">hello world<span>!!!!!!</span></h2>
JSX写起来就方便很多了,在内部会转换成React.createElement()
,然后再转换成对应的虚拟DOM,但是JSX语法浏览器不认识,所以需要利用babel插件进行转义处理。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../react.development.js"></script>
<script src="../react-dom.development.js"></script>
<script src="../babel.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
let element = (
<h2 title="hi">
hello world
<span>!!!!!!</span>
</h2>
);
root.render(element);
</script>
</body>
</html>
JSX语法详解
JSX 实际上等于 JavaScript + XML 的组合,那么就有很多结构限制,具体如下:
- 标签要小写
- 单标签要闭合
- class属性与for属性
- 多单词属性需驼峰,data-*不需要
- 唯一根节点
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
let element = (
<React.Fragment>
<h2 title="hi" className="box">
hello world
<span>!!!!!!</span>
<label htmlFor="elemInput">用户名:</label>
<input id="elemInput" type="text" tabIndex="3" data-userid="123" />
</h2>
<p>ppppppp</p>
</React.Fragment>
);
root.render(element);
什么是JSX及JSX详细使用方式(二)
JSX语法详解
在上一小节介绍了一些JSX的语法,本小节将继续学习JSX的语法,具体如下:
- { } 模板语法
- 添加注释
- 属性渲染变量
- 事件渲染函数
- style渲染对象
- { } 渲染 JSX
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
let myClass = 'box';
let handleClick = () => {
console.log(123);
}
let myStyle = {
color: 'red'
};
let element = (
<div>
{ /* <p>{ 1 + 1 }</p> */ }
<p className={myClass}>{ 'hello' }</p>
<p onClick={handleClick}>{ 'hello'.repeat(3) }</p>
<p style={myStyle}>{ true ? 123 : 456 }</p>
<p>{ <span>span111</span> }</p>
<p><span>span222</span></p>
</div>
);
root.render(element);
这里可以看到react中的模板语法采用的是单大括号,这一点跟Vue不太一样,Vue采用的是双大括号。
在React模板中,可以直接渲染JSX进去,是非常强大的,后面也会经常利用这一点特性去进行操作。
如何进行条件渲染与列表渲染
在React中是没有指令这个概念的,所以条件渲染和列表渲染都需要通过命令式编程来实现(也就是JS本身的能力)。
条件渲染
既然没有相关的指令,那么就只能通过原生JS来实现条件渲染了,具体方案可采用:
- 条件语句
- 条件运算符
- 逻辑运算符
// 方案一,条件语句
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
let isShow = false;
let element;
if(isShow){
element = (
<div>
hello world
</div>
);
}
else{
element = (
<div>
hi react
</div>
);
}
root.render(element);
// 方案二,条件运算符
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
let isShow = true;
let element = (
<div>
{
isShow ? 'hello world' : 'hi react'
}
</div>
);
root.render(element);
// 方案三,逻辑运算符
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
// JSX中不会渲染的值:false null undefined ''
let isShow = 0;
let element = (
<div>
{
isShow !== 0 && 'hello world'
}
</div>
);
root.render(element);
列表渲染
列表渲染也是需要通过原生JS来实现,具体方案:
- 循环语句
- map()方法
这里还需要注意一点,就是循环结构的时候还是需要给每一项添加唯一的key属性,这一点跟Vue非常相似。
// 方案一,循环语句
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
let data = [
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' }
];
let ret = [];
for(let i=0;i<data.length;i++){
ret.push(<li key={data[i].id}>{data[i].text}</li>);
}
// ['a', 'b', 'c'] -> 'a,b,c'
// { ['a', 'b', 'c'] } -> 'abc'
let element = (
<ul>
{ ret }
</ul>
);
root.render(element);
// 方案二,map()方法
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
let data = [
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' }
];
let element = (
<ul>
{
data.map(v=><li key={v.id}>{v.text}</li>)
}
</ul>
);
root.render(element);
类组件基本使用及组件通信
类组件基本使用
实际上我们的JSX是包含两部分的:
- React元素
- React组件
// React元素
const element = <div />
// React组件
const element = <Welcome name="Sara" />
定义一个组件,就是标签名首字母要大写,在React18中有两种定义组件的写法:
- 函数组件
- 类组件
下面分别给大家演示一下,如何定义一个React组件,代码如下:
// 函数组件
function Welcome(props){
return (
<div>hello world, {props.msg}</div>
);
}
let element = (
<Welcome msg="hi react" />
);
// 类组件
class Welcome extends React.Component {
render(){
return (
<div>hello world, {this.props.msg}</div>
);
}
}
let element = (
<Welcome msg="hi react" />
);
在上面组件中的msg
就是组件通信的数据,可以实现父子传递数值的操作。还可以传递函数给组件内部来实现子父通信操作。代码如下:
// 子组件
class Head extends React.Component {
render(){
this.props.getData('子组件的问候~~~')
return (
<div>Head Component</div>
);
}
}
// 父组件
class Welcome extends React.Component {
getData = (data) => {
console.log(data)
}
render(){
return (
<div>
hello world, {this.props.msg}
<br />
<Head getData={this.getData} />
</div>
);
}
}
props细节详解及注意事项
构造器中获取props数据
props是我们React父子组件之间通信的对象,那么这个对象在构造器constructor
中是获取不到的。
class Welcome extends React.Component {
constructor(){
super();
console.log( this.props.msg ) // undefined
}
render(){
return (
<div>hello world, {this.props.msg}</div>
);
}
}
let element = (
<Welcome msg="hi react" />
);
可以通过给super()
传递props参数是可以做到的,代码如下:
constructor(props){
super(props);
console.log( this.props.msg ) // hi react
}
那么React类组件是如何设计的呢?就要对面向对象非常的熟悉,原理分析如下:
class Foo {
constructor(props){
this.props = props;
}
}
class Bar extends Foo {
constructor(props){
super(props);
console.log(this.props);
}
render(){
console.log(this.props);
return '';
}
}
let props = {
msg: 'hello world'
};
let b = new Bar(props);
b.props = props;
b.render();
多属性的传递
当有非常多的属性要传递的时候,那么会比较麻烦,所以可通过扩展运算形式进行简写。
class Welcome extends React.Component {
render(){
let { msg, username, age } = this.props;
console.log( isChecked );
return (
<div>hello world, {msg}, {username}, {age}</div>
);
}
}
let info = {
msg: 'hi react',
username: 'xiaoming',
age: 20
};
let element = (
<Welcome {...info} />
);
给属性添加默认值与类型
import PropTypes from 'prop-types'
class Welcome extends React.Component {
static defaultProps = {
age: 0
}
static propTypes = {
age: PropTypes.number
}
...
}
这里的类型需要引入第三方模块才可以生效。
当父子通信的时候,如果只写属性,不写值的话,那么对应的值就是布尔值true。
类组件中事件的使用详解
首先React中的事件都是采用事件委托的形式,所有的事件都挂载到组件容器上,其次event对象是合成处理过的。一般情况下这些都是内部完成的,我们在使用的时候并不会有什么影响,作为了解即可。
事件中this的处理
在事件中最重要的就是处理this指向问题了,这里我们推荐采用面向对象中的public class fields
语法。
class Welcome extends React.Component {
handleClick = (ev) => { //推荐 public class fields
console.log(this); //对象
}
handleClick(){ //不推荐 要注意修正指向
console.log(this); //按钮
}
render(){
return (
<div>
<button onClick={this.handleClick}>点击</button>
hello world
</div>
);
}
}
let element = (
<Welcome />
);
事件传参处理
推荐采用函数的高阶方式,具体代码如下:
class Welcome extends React.Component {
handleClick = (num) => { // 高阶函数
return (ev) => {
console.log(num);
}
}
render(){
return (
<div>
<button onClick={this.handleClick(123)}>点击</button>
hello world
</div>
);
}
}
let element = (
<Welcome />
);
类组件响应式视图实现与原理
类组件响应式视图
通过state设置响应式视图,他是组件内私有的,受控于当前组件。通过state的变化,就可以影响到视图的变化。
class Welcome extends React.Component {
state = {
msg: 'hello',
count: 0
}
render(){
return (
<div>
{this.state.msg}, {this.state.count}
</div>
);
}
}
let element = (
<Welcome />
);
这样就可以在页面中渲染msg
和count
这两个字段了,那么怎么才能让state修改后视图跟着发生变化呢,首先不能像Vue那样直接对数据进行修改,在React中是不行的。
React类组件中式通过一个具体的方法setState()
进行state数据的更新,从而触发render()
方法的重渲染操作。
class Welcome extends React.Component {
state = {
msg: 'hello',
count: 0
}
handleClick = () => {
//this.state.msg = 'hi' //永远不要这样去操作
this.setState({
msg: 'hi'
});
}
render(){
console.log('render');
return (
<div>
<button onClick={this.handleClick}>点击</button>
{this.state.msg}, {this.state.count}
</div>
);
}
}
let element = (
<Welcome />
);
state改变视图的原理就是内部会重新调用render()
方法,俗称re-render
操作。
这里还有注意一点,setState()并不会影响其他state值,内部会完成合并的处理。
state细节详解及React18的自动批处理
自动批处理
自动批处理,即有助于减少在状态更改时发生的重新渲染次数。在React18之前也有批处理的,但是在Promise、setTimeout、原生事件中是不起作用的。
实际上自动批处理指的是,同一时机多次调用setState()
方法的一种处理机制。
handleClick = () => {
this.setState({
msg: 'hi'
});
this.setState({
count: 1
});
}
这里的代码当点击触发后,虽然调用了两次setState()
方法,但是只会触发一次render()
方法的重新执行。那么这就是所谓的自动批处理机制,这样是有助于性能的,减少重新执行的次数。
而且不管在什么时机下,都不会有问题的,这个在React18版本之前并不是所有的情况都好用的,比如:定时器。
handleClick = () => {
setTimeout(()=>{
this.setState({
msg: 'hi'
});
this.setState({
count: 1
});
}, 2000)
}
上面代码在React18之前的版本中,将会触发两次render()
方法。默认是自动批处理的,当然也可以改成不是自动批处理的方式,通过ReactDOM.flushSync
这个方法。
handleClick = () => {
ReactDOM.flushSync(()=>{
this.setState({
msg: 'hi'
});
})
ReactDOM.flushSync(()=>{
this.setState({
count: 1
});
})
}
异步处理
既然React18对多次调用采用的是自动批处理机制,那么就说明这个setState()
方法是异步的,所以要注意方法调用完后,我们的state数据并不会立即发生变化,因为state可能会被先执行了。
handleClick = () => {
/* this.setState({
count: this.state.count + 1
});
console.log( this.state.count ); */
this.setState({
count: this.state.count + 1
}, ()=>{ //异步执行结束后的回调函数
console.log( this.state.count );
});
}
可利用setState()
方法的第二个参数来保证数据更新后再去执行。这里还要注意同样的数据修改只会修改一次,可利用setState()
的回调函数写法来保证每一次都能触发。
handleClick = () => {
/* this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
}); */
this.setState((state)=> ({count: state.count + 1}));
this.setState((state)=> ({count: state.count + 1}));
this.setState((state)=> ({count: state.count + 1}));
}
这样页面按钮点击一次,count会从0直接变成了3。
PureComponent与shouldComponentUpdate
PureComponent与shouldComponentUpdate这两个方法都是为了减少没必要的渲染,React给开发者提供了改善渲染的优化方法。
shouldComponentUpdate
当我们在调用setState()
方法的时候,如果数据没有改变,实际上也会重新触发render()
方法。
class Welcome extends React.PureComponent {
state = {
msg: 'hello',
count: 0
}
handleClick = () => {
this.setState({
msg: 'hello'
});
}
render(){
console.log('render');
return (
<div>
<button onClick={this.handleClick}>点击</button>
{this.state.msg}, {this.state.count}
</div>
);
}
}
let element = (
<Welcome />
);
上面的render()
方法还是会不断的触发,但是实际上这些render触发是没有意义的,所以可以通过shouldComponentUpdate
钩子函数进行性能优化处理。
class Welcome extends React.Component {
state = {
msg: 'hello',
count: 0
}
handleClick = () => {
this.setState({
msg: 'hi'
});
}
shouldComponentUpdate = (nextProps, nextState) => {
if(this.state.msg === nextState.msg){
return false;
}
else{
return true;
}
}
render(){
console.log('render');
return (
<div>
<button onClick={this.handleClick}>点击</button>
{this.state.msg}, {this.state.count}
</div>
);
}
}
let element = (
<Welcome />
);
shouldComponentUpdate()方法的返回值,如果返回false就不进行界面的更新,如果返回true就会进行界面的更新。这样就可以根据传递的值有没有改变来决定是否进行重新的渲染。
PureComponent
PureComponent表示纯组件,当监控的值比较多的时候,自己去完成判断实在是太麻烦了,所以可以通过PureComponent这个内置的纯组件来自动完成选择性的渲染,即数据改变了重新渲染,数据没改变就不重新渲染。
class Welcome extends React.PureComponent {
state = {
msg: 'hello',
count: 0
}
handleClick = () => {
this.setState({
msg: 'hi'
});
}
render(){
console.log('render');
return (
<div>
<button onClick={this.handleClick}>点击</button>
{this.state.msg}, {this.state.count}
</div>
);
}
}
let element = (
<Welcome />
);
改成了纯组件后,记得不要直接对数据进行修改,必须通过setState()
来完成数据的改变,不然纯组件的特性就会失效。
class Welcome extends React.PureComponent {
state = {
msg: 'hello',
count: 0,
list: ['a', 'b', 'c']
}
handleClick = () => {
/* this.setState({
list: [...this.state.list, 'd']
}); */
//错误✖
/* this.state.list.push('d');
this.setState({
list: this.state.list
}) */
}
render(){
console.log('render');
return (
<div>
<button onClick={this.handleClick}>点击</button>
<ul>
{
this.state.list.map((v, i)=> <li key={i}>{v}</li>)
}
</ul>
</div>
);
}
}
let element = (
<Welcome />
);
immutable.js不可变数据集合
在上一个小节中,我们对数组进行了浅拷贝处理,这样可以防止直接修改数组的引用地址。但是对于深层次的对象就不行了,需要进行深拷贝处理。
但是常见的深拷贝处理机制,对于性能或功能性上都有一定的制约性,所以不复杂的数据,我们直接就可以选择用lodash
库中提供的深拷贝方法处理就可以了。但是对于复杂的对象就需要拷贝性能问题,这就可以用到本小节中介绍的immutable.js不可变数据集合。
immutable.js库
Immutable 是 Facebook 开发的不可变数据集合。不可变数据一旦创建就不能被修改,使得应用开发更简单,允许使用函数式编程技术,比如惰性评估。Immutable JS 提供一个惰性 Sequence,
允许高效的队列方法链,类似 map
和 filter
,不用创建中间代表。
具体是如何做到高效的,可以参考图示。
![](./img/14-02-immutablejs.gif)
下面就来看一下immutable.js的基本使用吧,代码如下:
import Immutable from 'immutable'
class Head extends React.PureComponent {
render(){
console.log('render');
return (
<div>head component, {this.props.item.get('text')} </div>
);
}
}
class Welcome extends React.PureComponent {
state = {
msg: 'hello',
count: 0,
list: Immutable.fromJS([
{ id: 1, text: 'aaa' }
])
}
handleClick = () => {
let list = this.state.list.setIn([0, 'text'], 'bbb');
this.setState({
list
});
}
render(){
return (
<div>
<button onClick={this.handleClick}>点击</button>
<Head item={this.state.list.get(0)} />
</div>
);
}
}
主要就是通过Immutable.fromJS()先把对象转成immutable对象,再通过setIn()方法来设置数据,get()方法来获取数据。
Refs操作DOM及操作类组件
React操作原生DOM跟Vue框架是类似的,都是通过ref属性来完成的,主要使用React.createRef()
这个方法和callbackRef()
这个回调函数写法。
React.createRef()
这个方法可以创建一个ref对象,然后把这个ref对象添加到对应的JSX元素的ref属性中,就可以控制原生DOM了。
class Welcome extends React.Component {
myRef = React.createRef()
handleClick = () => {
//console.log(this.myRef.current); // 原生DOM
this.myRef.current.focus();
}
render(){
return (
<div>
<button onClick={this.handleClick}>点击</button>
<input type="text" ref={this.myRef} />
</div>
);
}
}
回调函数写法
还可以编写一个回调函数来完成,原生DOM的操作。
class Welcome extends React.Component {
callbackRef = (element) => {
element.focus();
}
handleClick = () => {
this.myRef.focus();
}
render(){
return (
<div>
<button onClick={this.handleClick}>点击</button>
<input type="text" ref={this.callbackRef} />
</div>
);
}
}
Ref操作类组件
除了可以把ref属性添加到JSX元素上,还可以把ref属性添加到类组件上,那么这样可以拿到类组件的实例对象。
class Head extends React.Component {
username = 'xiaoming';
render(){
return (
<div>head component</div>
);
}
}
class Welcome extends React.Component {
myRef = React.createRef()
handleClick = () => {
console.log(this.myRef.current); //组件的实例对象
console.log(this.myRef.current.username);
}
render(){
return (
<div>
<button onClick={this.handleClick}>点击</button>
<Head ref={this.myRef} />
</div>
);
}
}
这样可以间接的实现父子组件之间的数据通信。
ref属性还可以进行转发操作,可以把ref传递到组件内,获取到子组件的DOM元素。
class Head extends React.Component {
render(){
return (
<div ref={this.props.myRef}>head component</div>
);
}
}
class Welcome extends React.Component {
myRef = React.createRef()
handleClick = () => {
console.log(this.myRef.current);
}
render(){
return (
<div>
<button onClick={this.handleClick}>点击</button>
<Head myRef={this.myRef} />
</div>
);
}
}
详解受控组件及各种表单中的使用
受控组件
在React中是如何实现表单操作的呢?主要就是利用受控组件来完成的,那么受控组件需要采用value
+ onChange
的一个组合。
class Welcome extends React.Component {
state = {
msg: 'hello'
}
handleChange = (ev) => {
this.setState({
msg: ev.target.value
});
}
render(){
return (
<div>
<input type="text" value={this.state.msg} onChange={this.handleChange} /> { this.state.msg }
</div>
);
}
}
这样当输入框输入内容后,数据会跟着发生改变,从而视图也会跟着放生变化。
那么这种受控组件也可以针对除了输入框以外的其他表单控件,如:下拉菜单,单选按钮,多选按钮,下面分别来看一下。
// 受控的下拉菜单
class Welcome extends React.Component {
state = {
city: '上海'
}
handleChange = (ev) => {
this.setState({
city: ev.target.value
});
}
render(){
return (
<div>
<select value={this.state.city} onChange={this.handleChange}>
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="深圳">深圳</option>
</select>
{ this.state.city }
</div>
);
}
}
// 受控的单选按钮
class Welcome extends React.Component {
state = {
gender: '女'
}
handleChange = (ev) => {
this.setState({
gender: ev.target.value
});
}
render(){
return (
<div>
<input type="radio" name="gender" value="男" onChange={this.handleChange} checked={this.state.gender === '男'} /> 男
<input type="radio" name="gender" value="女" onChange={this.handleChange} checked={this.state.gender === '女'} /> 女
<br />
{ this.state.gender }
</div>
);
}
}
// 受控的多选按钮
class Welcome extends React.Component {
state = {
furits: ['苹果', '西瓜']
}
handleChange = (ev) => {
if(ev.target.checked){
this.setState({
furits: [...this.state.furits, ev.target.value]
});
}
else{
let furits = [...this.state.furits];
furits.splice(furits.indexOf(ev.target.value), 1)
this.setState({
furits
});
}
}
render(){
return (
<div>
<input type="checkbox" value="苹果" onChange={this.handleChange} checked={ this.state.furits.includes('苹果') } /> 苹果
<input type="checkbox" value="香蕉" onChange={this.handleChange} checked={ this.state.furits.includes('香蕉') } /> 香蕉
<input type="checkbox" value="西瓜" onChange={this.handleChange} checked={ this.state.furits.includes('西瓜') } /> 西瓜
<input type="checkbox" value="葡萄" onChange={this.handleChange} checked={ this.state.furits.includes('葡萄') } /> 葡萄
<br />
{ this.state.furits }
</div>
);
}
}
详解非受控组件的实现方案
非受控组件
在上一个小节中,我们学习了受控组件,但是有时候受控组件不能完全满足我们的需求,所以React中还提供了非受控组件的方式来操作表单元素。
使用非受控组件,这时表单数据将交由 DOM 节点来处理,代码如下:
class Welcome extends React.Component {
state = {
msg: 'hello'
}
changeInput = (ev) => {
this.setState({
msg: ev.target.value
});
}
render(){
return (
<div>
<input type="text" defaultValue={this.state.msg} onInput={this.changeInput} /> { this.state.msg }
</div>
);
}
}
其中defaultValue属性可以设置表单的默认值,下面再举一个上传控件的非受控组件使用方式。
class Welcome extends React.Component {
myRef = React.createRef()
handleSubmit = (ev) => {
console.log(
this.myRef.current.files[0].name
);
ev.preventDefault();
}
render(){
return (
<form onSubmit={this.handleSubmit}>
<input type="file" ref={this.myRef} />
<input type="submit" value="submit" />
</form>
);
}
}
详解常见生命周期钩子函数
在学习Vue的时候,我们就已经介绍了生命周期钩子函数的概念,React中也存在一些钩子函数。我们可以为类组件声明一些特殊的方法,当组件挂载、更新或卸载时就会去执行这些函数。
要想学习React类组件的生命周期钩子函数,可以参考生命周期图谱。地址:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
![](./img/14-03-常见生命周期图谱.png)
生命周期主要分为三个阶段:
- 挂载时
- 更新时
- 卸载时
挂载时对应的钩子函数有:constructor
,render
,componentDidMount
。
更新时对应的钩子函数有:render
,componentDidUpdate
卸载时对应的钩子函数有:componentWillUnmount
可以看到挂载时和更新时都有render
这个方法。这就是为什么state改变的时候,会触发render
重渲染操作。
class Welcome extends React.Component {
state = {
msg: 'hello world'
}
constructor(props){
super(props);
console.log('constructor');
}
componentDidMount = () => {
// react中发起ajax请求的初始操作,在这个钩子中完成
console.log('componentDidMount');
}
componentDidUpdate = () => {
// 等DOM更新后触发的钩子
console.log('componentDidUpdate');
}
componentWillUnmount = () => {
console.log('componentWillUnmount');
}
handleClick = () => {
/* this.setState({
msg: 'hi react'
}); */
//this.forceUpdate();
root.unmount(); // 触发卸载组件
}
render(){
console.log('render');
return (
<div>
<button onClick={this.handleClick}>点击</button>
{ this.state.msg }
</div>
);
}
}
详解不常见生命周期钩子函数
- getDerivedStateFromProps:props派生state的值
- shouldComponentUpdate:优化render渲染次数
- getSnapshotBeforeUpdate:DOM更新前的快照
getDerivedStateFromProps
这个钩子主要是由props来决定state的值,这个需求比较少,下面来看例子。
class Welcome extends React.Component {
state = {
isAdd: false,
lastNow: 0
}
static getDerivedStateFromProps = (props, state) => {
return {
isAdd: props.currentNow > state.lastNow,
lastNow: props.currentNow
}
}
render(){
return (
<div>
{ this.state.isAdd ? '累加' : '累减' }, { this.state.lastNow }
</div>
);
}
}
let now = 0;
let dir = 1;
setInterval(()=>{
if(now === 0){
dir = 1;
}
else if(now === 5){
dir = -1;
}
now += dir;
let element = (
<Welcome currentNow={now} />
);
root.render(element);
}, 1000)
通过props的变化来决定state的值,可以完成一些界面的更新操作。
shouldComponentUpdate
根据返回的结果的不同,选择性进行渲染,是进行性能优化的一种手段,这个钩子在前面学习PureComponent小节中就已经学习到了,这里不再赘述该如何使用。
getSnapshotBeforeUpdate
这个钩子可以触发DOM更新前的快照,可以把更新前的一些数据通过return提供出来,并通过componentDidUpdate
钩子的第三个参数进行接收。
可以利用这一点来进行DOM前后对比的差异比较,代码如下:
class Welcome extends React.Component {
state = {
list: ['a', 'b', 'c']
}
myRef = React.createRef()
handleClick = () => {
this.setState({
list: [...this.state.list, 'd', 'e', 'f']
})
}
getSnapshotBeforeUpdate = (props, state) => {
return this.myRef.current.offsetHeight;
}
componentDidUpdate = (props, state, snapshot) => {
console.log( this.myRef.current.offsetHeight - snapshot );
}
render(){
return (
<div>
<button onClick={this.handleClick}>点击</button>
<ul ref={this.myRef}>
{ this.state.list.map((v, i)=> <li key={i}>{v}</li>) }
</ul>
</div>
);
}
}
组件内容的组合模式
React组件也是可以进行内容分发的,但是并不想Vue一样通过插槽来进行接收,而是通过props.children这个属性进行接收的。
class Welcome extends React.Component {
render(){
return (
<div>
hello world, { this.props.children }
</div>
);
}
}
let element = (
<Welcome>
<h2>这是一个标题</h2>
</Welcome>
);
那么如何进行多内容的分区域处理呢?也就是Vue中多插槽的概念。这个就不能利用props.children来实现了,只能采用React模板的能力,通过传递JSX元素的方式进行实现。
class Welcome extends React.Component {
render(){
return (
<div>
{ this.props.title }
hello world
{ this.props.content }
</div>
);
}
}
let element = (
<Welcome title={ <h2>这是一个标题</h2> } content={ <p>这是一个段落</p> } />
);
复用组件功能之Render Props模式
Render Props模式
术语 “render props” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。利用这种方式可以实现组件之间的功能复用操作。
class MouseXY extends React.Component {
state = {
x: 0,
y: 0
}
componentDidMount = () => {
document.addEventListener('mousemove', this.move)
}
componentWillUnmount = () => {
document.removeEventListener('mousemove', this.move)
}
move = (ev) => {
this.setState({
x: ev.pageX,
y: ev.pageY
});
}
render(){
return (
<React.Fragment>
{ this.props.render(this.state.x, this.state.y) }
</React.Fragment>
);
}
}
class Welcome extends React.Component {
render(){
return (
<MouseXY render={(x, y)=>
<div>
hello world, {x}, {y}
</div>
} />
);
}
}
let element = (
<Welcome />
);
主要就是render属性后面的值是一个回调函数,通过这个函数的形参可以得到组件中的数据,从而实现功能的复用。
复用组件功能之HOC高阶组件模式
HOC高阶组件
除了Render Props模式可以复用组件外,还可以利用HOC高阶组件来实现,他是React 中用于复用组件逻辑的一种高级技巧,具体而言,就是参数为组件,返回值为新组件的函数。
function withMouseXY(WithComponent){
return class extends React.Component {
state = {
x: 0,
y: 0
}
componentDidMount = () => {
document.addEventListener('mousemove', this.move)
}
componentWillUnmount = () => {
document.removeEventListener('mousemove', this.move)
}
move = (ev) => {
this.setState({
x: ev.pageX,
y: ev.pageY
})
}
render(){
return <WithComponent {...this.state} />
}
}
}
class Welcome extends React.Component {
render(){
return (
<div>
hello world, { this.props.x }, { this.props.y }
</div>
);
}
}
const MouseWelcome = withMouseXY(Welcome)
let element = (
<MouseWelcome />
);
组件跨层级通信方案Context
Context通信
前面我们学习了父子组件之间的通信,有时候我们需要多层组件之间的嵌套,那么如果从最外层一层一层的把数据传递到最内层的话势必会非常的麻烦。
所以context的作用就是解决这个问题,可以把数据直接从最外层传递给最内层的组件。
let MyContext = React.createContext();
class Welcome extends React.Component {
state = {
msg: 'welcome组件的数据'
}
render(){
return (
<div>
Hello Welcome
<MyContext.Provider value={this.state.msg}>
<Head />
</MyContext.Provider>
</div>
);
}
}
class Head extends React.Component {
render(){
return (
<div>
Hello Head
<Title />
</div>
);
}
}
class Title extends React.Component {
static contextType = MyContext
componentDidMount = () => {
console.log( this.context );
}
render(){
return (
<div>
Hello Title <MyContext.Consumer>{ value => value }</MyContext.Consumer>
</div>
);
}
}
let element = (
<Welcome />
);
这里传递的语法,是通过<MyContext.Provider>
组件携带value
属性进行向下传递的,那么接收的语法是通过<MyContext.Consumer>
组件。
也可以定义一个静态方法static contextType = MyContext
,这样就可以在逻辑中通过this.context
来拿到同样的值。
简易购物车的综合案例
利用本章所学习的React类组件知识,一起来完成一个简易的购物车效果,案例如下所示。
![](./img/14-04-简易购物车效果.png)
这个效果首先准备了对应的json数据,然后再去拆分组件,最后实现逻辑的部分,代码如下:
{
"errcode": 0,
"list": [
{
"id": 1,
"isActive": false,
"name": "西瓜",
"price": 5,
"number": 2
},
...
]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{margin: 0; padding: 0;}
li{ list-style: none;}
.cart{ width: 700px; margin: 30px auto;}
ul{ overflow: hidden;}
li{ width: 100px; border: 5px gray dotted; border-radius: 20px; padding: 20px; float: left; margin:10px;}
.remove, .add{ cursor: pointer;}
.cartbtn{ font-size:14px; text-align: center; background: red; color: white; padding: 3px; border-radius: 5px; margin-top: 10px; cursor: pointer;}
li.active{ border-color:red;}
li.active .cartbtn{ background-color: skyblue;}
.all{ text-align: center; margin: 20px 0;}
</style>
<script src="../react.development.js"></script>
<script src="../react-dom.development.js"></script>
<script src="../babel.min.js"></script>
<script src="../lodash.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
let app = document.querySelector('#app');
let root = ReactDOM.createRoot(app);
class Cart extends React.PureComponent {
state = {
list: [],
all: 0
}
componentDidMount = () => {
fetch('./data.json').then((res)=>{
return res.json();
}).then((res)=>{
if(res.errcode === 0){
this.setState({
list: res.list
});
}
})
}
componentDidUpdate = () => {
this.computedAll()
}
handleAdd = (id) => {
return () => {
let list = _.cloneDeep(this.state.list)
let now = list.find((v)=> v.id === id)
now.number++;
this.setState({
list
});
}
}
handleRemove = (id) => {
return () => {
let list = _.cloneDeep(this.state.list)
let now = list.find((v)=> v.id === id)
if( now.number > 1 ){
now.number--;
}
this.setState({
list
});
}
}
handleCartBtn = (id) => {
return () => {
let list = _.cloneDeep(this.state.list)
let now = list.find((v)=> v.id === id)
now.isActive = !now.isActive
this.setState({
list
});
}
}
computedAll = () => {
let all = 0;
this.state.list.filter((v)=> v.isActive).forEach((v)=>{
all += v.price * v.number;
})
this.setState({
all
});
}
render(){
return (
<div className="cart">
<ul>
{
this.state.list.map((v)=> <Item key={v.id} {...v} handleAdd={this.handleAdd} handleRemove={this.handleRemove} handleCartBtn={this.handleCartBtn} />)
}
</ul>
<div className="all">
总金额:<span>{ this.state.all }</span>元
</div>
</div>
);
}
}
class Item extends React.PureComponent {
render(){
const { id, isActive, name, price, number, handleAdd, handleRemove, handleCartBtn } = this.props;
return (
<li className={ isActive ? 'active' : '' }>
<h3>{name}</h3>
<p>单价:{price}</p>
<p>
数量:
<span className="remove" onClick={handleRemove(id)}>-</span>
<span>{number}</span>
<span className="add" onClick={handleAdd(id)}>+</span>
</p>
<div className="cartbtn" onClick={handleCartBtn(id)}>{ isActive ? '取消购买' : '添加到购物车' }</div>
</li>
);
}
}
let element = (
<Cart />
);
root.render(element)
</script>
</body>
</html>
章节总结
小伙伴大家好,本章学习了React18核心概念与类组件使用 - 入门React18第一步。
总结内容
- 加深对虚拟DOM的理解,使用JSX与模板语法生成结构
- 类组件与函数组件,以及组件之间是如何进行通信的
- 如何在类组件中实现数据的响应式,以及React18自动批处理方式
- 优化手段,纯组件,不可变数据集合等
- 常见功能,Refs,受控,生命周期,组合,HOC,Context等