1.1、什么是JSX?
JSX = JavaScript XML,这是React官方发明的一种JS语法(糖)
概念:JSX是 JavaScript XML(HTML)的缩写,表示在 JS 代码中书写 HTML 结构
设想如下变量声明:
const element = <h1>Hello, world!</h1>;
这个有趣的标签语法既不是字符串也不是 HTML。
它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模板语言,但它具有 JavaScript 的全部功能。
JSX 可以生成 React “元素”。
React 认为渲染逻辑本质上与其他 UI 逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。
React 并没有采用将标记与逻辑分离到不同文件这种人为的分离方式,而是通过将二者共同存放在称之为“组件”的松散耦合单元之中,来实现关注点分离。我们将在后面章节中深入学习组件。如果你还没有适应在 JS 中使用标记语言,这个会议讨论应该可以说服你。
React 不强制要求使用 JSX,但是大多数人发现,在 JavaScript 代码中将 JSX 和 UI 放在一起时,会在视觉上有辅助作用。它还可以使 React 显示更多有用的错误和警告消息。
浏览器默认是不支持JSX的,所以jsx语法必须使用@babel/preset-react进行编译,编译的结果React.createElement()这种Api的代码。
示例:<div>Hello</div> ==>@babel/preset-react==> React.createElement('div',{},'Hello')
在React开发中,JSX语法是可选的(也就是你可以不使用JSX)。如果不使用JSX语法,React组件代码将变得特别麻烦(难以维护)。所以几乎所有React开发都用的是JSX语法。
JSX是Javascript的一种语法拓展
JSX是JavaScript XML简写,表示在JavaScript中编写XML格式代码(也就是HTML格式)
优势:
注意:JSX 并不是标准的 JS 语法,是 JS 的语法扩展,浏览器默认是不识别的,脚手架中内置的 @babel/plugin-transform-react-jsx 包,用来解析该语法
目标任务:
能够在JSX中使用表达式
语法
{ JS 表达式 }
const name = '张三'
<h1>你好,我叫{name}</h1> // <h1>你好,我叫张三</h1>
可以使用的表达式
特别注意
if 语句/ switch-case 语句/ 变量声明语句,这些叫做语句,不是表达式,不能出现在 {}
中。
可以使用?:与&&替代if的功能
在下面的示例中,我们将调用 JavaScript 函数 formatName(user)
的结果,并将结果嵌入到 <h1>
元素中。
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1>
Hello, {formatName(user)}! </h1>
);
为了便于阅读,我们会将 JSX 拆分为多行。同时,我们建议将内容包裹在括号中,虽然这样做不是强制要求的,但是这可以避免遇到自动插入分号陷阱。
map()
方法定义在JavaScript的Array
中,它返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。 注意:
map()
不会对空数组进行检测map()
不会改变原始数组
array.map(function(currentValue, index, arr), thisValue)
参数说明:
function(currentValue, index, arr)
:必须。为一个函数,数组中的每个元素都会执行这个函数。其中函数参数:currentValue
:必须。当前元素的的值。index
:可选。当前元素的索引。arr
:可选。当前元素属于的数组对象。thisValue
:可选。对象作为该执行回调时使用,传递给函数,用作"this
"的值。//返回由原数组中每个元素的平方组成的新数组:
let array = [1, 2, 3, 4, 5];
let newArray = array.map((item) => {
return item * item;
})
console.log(newArray) // [1, 4, 9, 16, 25]
JSX 表达式必须具有一个父元素。没有父元素时请使用<></>
目标任务:
能够在JSX中实现列表渲染
页面的构建离不开重复的列表结构,比如歌曲列表,商品列表等,我们知道vue中用的是v-for,react这边如何实现呢?
实现:使用数组的map
方法
案例:
// 列表
const songs = [
{ id: 1, name: '痴心绝对' },
{ id: 2, name: '像我这样的人' },
{ id: 3, name: '南山南' }
]
function App() {
return (
<div className="App">
<ul>
{
songs.map(item => <li>{item.name}</li>)
}
</ul>
</div>
)
}
export default App
注意点:需要为遍历项添加 key
属性
目标任务:
能够在JSX中实现条件渲染
作用:根据是否满足条件生成HTML结构,比如Loading效果
实现:可以使用 三元运算符
或 逻辑与(&&)运算符
案例:
// 来个布尔值
const flag = true
function App() {
return (
<div className="App">
{/* 条件渲染字符串 */}
{flag ? 'react真有趣' : 'vue真有趣'}
{/* 条件渲染标签/组件 */}
{flag ? <span>this is span</span> : null}
</div>
)
}
export default App
目标任务:
能够在JSX中实现css样式处理
行内样式 - style
import ReactDOM from "react-dom/client";
//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));
let songs = ["好汉歌", "南山南", "滴答"];
//3、将Vnode渲染到根结点上
root.render(
<div>
<h2 style={{ color: "red", backgroundColor: "yellow" }}>歌曲列表</h2>
<ul>
{songs.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
行内样式 - style - 更优写法
const styleObj = {
color:red
}
function App() {
return (
<div className="App">
<div style={ styleObj }>this is a div</div>
</div>
)
}
export default App
类名 - className(推荐)
app.css
.title {
font-size: 30px;
color: blue;
}
app.js
import './app.css'
function App() {
return (
<div className="App">
<div className='title'>this is a div</div>
</div>
)
}
export default App
类名 - className - 动态类名控制
import './app.css'
const showTitle = true
function App() {
return (
<div className="App">
<div className={ showTitle ? 'title' : ''}>this is a div</div>
</div>
)
}
export default App
<></>
(幽灵节点)替代class -> className
for -> htmlFor
()
包裹,防止bug出现在编译之后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象。
也就是说,你可以在 if
语句和 for
循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX:
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>; }
return <h1>Hello, Stranger.</h1>;}
你可以通过使用引号,来将属性值指定为字符串字面量:
const element = <a href="https://www.reactjs.org"> link </a>;
也可以使用大括号,来在属性值中插入一个 JavaScript 表达式:
const element = <img src={user.avatarUrl}></img>;
在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。你应该仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。
警告: 因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用
camelCase
(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。 例如,JSX 里的class
变成了className
,而tabindex
则变为tabIndex
。
属性也可以是一个箭头函数:
import ReactDOM from "react-dom/client";
import React from "react";
import "./App.css";
//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));
let counter = 0;
const element = (
<div>
<h2>计算器</h2>
<button
onClick={() => {
counter++;
console.log(counter);
}}
>
点击加1
</button>
</div>
);
//3、将Vnode渲染到根结点上
root.render(element);
也可以是一个普通函数:
import ReactDOM from "react-dom/client";
import React from "react";
import "./App.css";
//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));
let counter = 0;
const element = (
<div>
<h2>计算器</h2>
<button onClick={increment}>点击加1</button>
</div>
);
function increment() {
counter++;
console.log(counter);
}
//3、将Vnode渲染到根结点上
root.render(element);
假如一个标签里面没有内容,你可以使用 />
来闭合标签,就像 XML 语法一样:
const element = <img src={user.avatarUrl} />;
JSX 标签里能够包含很多子元素:
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
你可以安全地在 JSX 当中插入用户输入内容:
const title = response.potentiallyMaliciousInput;
// 直接使用是安全的:
const element = <h1>{title}</h1>;
React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。
Babel 会把 JSX 转译成一个名为 React.createElement()
函数调用。
以下两种示例代码完全等效:
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React.createElement()
会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:
// 注意:这是简化过的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
这些对象被称为 “React 元素”。它们描述了你希望在屏幕上看到的内容。React 通过读取这些对象,然后使用它们来构建 DOM 以及保持随时更新。
import ReactDOM from "react-dom/client";
import React from "react";
import "./App.css";
//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));
const element = React.createElement(
"h1",
{ className: "blueBg" },
"Hello, world!"
);
//3、将Vnode渲染到根结点上
root.render(element);
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展,但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。如果我们将一个个功能块拆分后,就可以像搭建积木一下来搭建我们的项目。
SPA指的是Single Page Application,就是只有一张Web页面的应用。单页应用程序 (SPA) 是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。 浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制。因此,对单页应用来说模块化的开发和设计显得相当重要。
单页Web应用,顾名思义,就是只有一张Web页面的应用。浏览器一开始会加载必需的HTML、CSS和JavaScript,之后所有的操作都在这张页面上完成,这一切都由JavaScript来控制。因此,单页Web应用会包含大量的JavaScript代码,复杂度可想而知,模块化开发和设计的重要性不言而喻。
速度:更好的用户体验,让用户在web app感受native app的速度和流畅
MVVM:经典MVVM开发模式,前后端各负其责
ajax:重前端,业务逻辑全部在本地操作,数据都需要通过AJAX同步、提交
路由:在URL中采用#号来作为当前视图的地址,改变#号后的参数,页面并不会重载
优点:
1.分离前后端关注点,前端负责View,后端负责Model,各司其职; 2.服务器只接口提供数据,不用展示逻辑和页面合成,提高性能; 3.同一套后端程序代码,不用修改兼容Web界面、手机; 4.用户体验好、快,内容的改变不需要重新加载整个页面 5.可以缓存较多数据,减少服务器压力 6.单页应用像网络一样,几乎随处可以访问—不像大多数的桌面应用,用户可以通过任务网络连接和适当的浏览器访问单页应用。如今,这一名单包括智能手机、平板电脑、电视、笔记本电脑和台式计算机。
缺点:
1.SEO问题 2.刚开始的时候加载可能慢很多 3.用户操作需要写逻辑,前进、后退等 4.页面复杂度提高很多,复杂逻辑难度成倍
组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。
组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
组件它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
组件是 React的核心慨念,定 React应用程序的基石。组件将应用的UI拆分成独立的、可复用的模块,React 用任厅止定田一个一个组件搭建而成的。
定义一个组件有两种方式,便用ES 6 class(类组件)和使用函数(函数组件)。我们先介绍使用class定义组件方式。
使用class定义组件需要满足两个条件:
(1)class继承自 React.Component。
(2) class内部必须定义 render方法,render方法返回代表该组件UI的React元素。
使用create-react-app新建一个简易BBS项目,在这个项目中定义一个组件PostList,用于展示BBS 的帖子列表。
PostList的定义如下:
import React, { Component } from "react";
class PostList extends Component {
render() {
return (
<div>
<h2>帖子列表:</h2>
<ul>
<li>男子吃霸王餐还教育老板和气生财</li>
<li>莫斯科险遭无人机炸弹攻击</li>
<li>武汉官方把房地产归为困难行业</li>
</ul>
</div>
);
}
}
export default PostList;
index.js
import ReactDOM from "react-dom/client";
import React from "react";
import PostList from "./PostList";
//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));
let vNode = <PostList />;
//3、将Vnode渲染到根结点上
root.render(vNode);
运行效果:
定义组件最简单的方式就是编写 JavaScript 函数:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。
index.js内容如下:
import ReactDOM from "react-dom/client";
import React from "react";
//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));
//2、定义函数组件
function Welcome(props) {
return <h2>Hello {props.name}!</h2>;
}
//3、使用组件
let vNode = <Welcome name="zhangguo" />;
//4、将Vnode渲染到根结点上
root.render(vNode);
运行结果:
约定说明
2.3.1中的PostList 中的每一个帖子都使用一个标签直接包裹,但一个帖子不仅包含能子的标题,还会包含帖子的创建人、帖子创建时间等信息,这时候标签下的结构就会变得复杂。而且每一个帖子都需要重写一次这 个复杂的结构,PostList 的结构将会变成类似这样的形式:
import React, { Component } from "react";
class PostList extends Component {
render() {
return (
<div>
<h2>帖子列表:</h2>
<ul>
<li>
<div>男子吃霸王餐还教育老板和气生财</div>
<div>
创建人:<span>小明</span>
</div>
<div>
创建时间:<span>2023-02-03 18:19:22</span>
</div>
</li>
<li>
<div>莫斯科险遭无人机炸弹攻击</div>
<div>
创建人:<span>小军</span>
</div>
<div>
创建时间:<span>2023-01-22 21:22:35</span>
</div>
</li>
<li>
<div>武汉官方把房地产归为困难行业</div>
<div>
创建人:<span>小华</span>
</div>
<div>
创建时间:<span>2022-12-22 15:14:56</span>
</div>
</li>
</ul>
</div>
);
}
}
export default PostList;
但是,帖子列表的数括依然存在于 PostList中,如何将数据传递给每个 PostItem 组件呢?这时候就需要用到组件的props属性。组件的 props用于把父组件中的数据或方法传递给子组件,供子组件使用。
props是一个简单结构的对象,它包含的属性正是由组件作为JSX标签使用时的属性组成。
例如下面是一个使用User组
<User name='React' age='4' address=' America'>
此时User组件的 props结构如下:
props ={
name: 'React',
age: '4',
address: 'America'
}
利用props定义PostItem组件,PostItem.js如下所示:
import React, { Component } from "react";
class PostItem extends Component {
render() {
const { title, author, date } = this.props;
return (
<li>
<div>{title}</div>
<div>
创建人:<span>{author}</span>
</div>
<div>
创建时间:<span>{date}</span>
</div>
</li>
);
}
}
export default PostItem;
src/PostList.js
import React, { Component } from "react";
import PostItem from "./PostItem";
const data = [
{
title: "男子吃霸王餐还教育老板和气生财",
author: "小明",
date: "2023-02-03 18:19:22",
},
{
title: "莫斯科险遭无人机炸弹攻击",
author: "小军",
date: "2023-01-22 21:22:35",
},
{
title: "武汉官方把房地产归为困难行业",
author: "小华",
date: "2022-12-22 15:14:56",
},
];
class PostList extends Component {
render() {
return (
<div>
<h2>帖子列表:</h2>
<ul>
{data.map((item) => (
<PostItem
title={item.title}
author={item.author}
date={item.date}
/>
))}
</ul>
</div>
);
}
}
export default PostList;
index.js
import ReactDOM from "react-dom/client";
import React from "react";
import PostList from "./PostList";
//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));
let vNode = <PostList />;
//3、将Vnode渲染到根结点上
root.render(vNode);
运行结果:
组件的 state是组件内部的状态,state的变化最终将反映到组件UI的上。我们在组件的构造方法constructor中通过this.state定义组件的初始状态,并通过调用 this.setState 方法改变组件状态(也是改变组件状态的唯一方式),进而组件UI也会随之重新渲染。
下面来改造下BBS项目。我们为每一个帖子增加一个“点赞”按钮每点击一次,该帖子的点赞数增加1。点赞数是会发生变化的,它的变化也会影响到组件UI,因此我们将点赞数vote 作为Postltem的一个状态定义到它的state内。
修改后的PostItem:
import React, { Component } from "react";
class PostItem extends Component {
constructor(props) {
super(props); //调用Component的构造方法,用于完成React组件的初始化工作
this.state = {
vote: 0,
};
}
//处理点击逻辑
handleClick() {
let vote = this.state.vote;
vote++;
this.setState({ vote: vote });
}
render() {
const { title, author, date } = this.props;
return (
<li>
<div>{title}</div>
<div>
创建人:<span>{author}</span>
</div>
<div>
创建时间:<span>{date}</span>
</div>
<div>
<button
onClick={() => {
this.handleClick();
}}
>
点赞 {this.state.vote}
</button>
</div>
</li>
);
}
}
export default PostItem;
运行结果:
React组件正是由props和state两种类型的数据驱动渲染出组件 UI。props是组件对外的接口,组件通过 props接收外部传入的数据(包括方法); state是组件对内的接口,组件内部状态的变化通过state反映。另外,props是只读的,你不能在组内部修改 props; state是可变的,组件状态的变化通过修改state来实现。
是不是每个组件内部都需要定义state呢?当然不是。state用来反映组件内部状态变化,如果一个组件的内部状态是不变的,当然就用不到state,这样的组件称之为无状态组件,例如PostList。 反之,一个组件的内部状态会发生变化,就需要使用state 来保存变化,这种组件称为有状态组件,例如PostItem。 定义无状态组件除了使用 ES 6 class的方式外,还可以使用函数定义,一个函数组件接收props作为参数,返回代表这个组件的UI React 元素结构。 例如,下面是一个简单的函数组件:
function Welcome (props) {
return <h1>Hello, {props.name }</hl>;
}
可以看出,函数组件的写法比类组件的写法要简洁很多,在使用无状态组件时,应该尽量将其定义成函数组件。
在开发React应用时,一定要先认真思考哪些组件应该设计成有状态组件,哪些组件应该设计成无状态组件。并且,应该尽可能多地使用无状态组件,无状态组件不用关心状态的变化,只聚焦于UI的展示,因而更容易被复用。React应用组件设计的一般思路是,通过定义少数的有状态组件管理整个应用的状态变化,并且将状态通过props传递给其余的无状态组件,由无状态组件完成页面绝大部分UI的渲染工作。总之,有状态组件主要关注处理状态变化的业务逻辑,无状态组件主要关注组件UI的渲染。
下面让我们回过头来看一下BBS项目的组件设计。当前的组件设计并不合适,主要体现在:
(1)帖子列表通过一个常量data保存在组件之外,但帖子列表的数据增加或原有帖子的删除都会导致帖子列表数据的变化。
(2)每一个 PostItem都维持个 vote状态,但除了vote以外,帖子其他的信息(如标题、创建人等)都保存在PostList中,这显然也是不合理的。
我们对这两个组件进行重新设计,将PostList 设计为有状态组件,负责帖子列表数据的获取以及点赞行为的处理,将PostItem设计为无状态组件,只负责每一个帖子的 展示。
修改后的PostList.js:
import React, { Component } from "react";
import PostItem from "./PostItem";
class PostList extends Component {
constructor(props) {
super(props);
this.state = {
posts: [],
};
this.timer = null;
//改变handleVote中的this指向
this.handleVote = this.handleVote.bind(this);
}
data = [
{
id: 1,
title: "男子吃霸王餐还教育老板和气生财",
author: "小明",
date: "2023-02-03 18:19:22",
vote: 0,
},
{
id: 2,
title: "莫斯科险遭无人机炸弹攻击",
author: "小军",
date: "2023-01-22 21:22:35",
vote: 0,
},
{
id: 3,
title: "武汉官方把房地产归为困难行业",
author: "小华",
date: "2022-12-22 15:14:56",
vote: 0,
},
];
//组件挂载到DOM后
componentDidMount() {
this.timer = setTimeout(() => {
this.setState({
posts: this.data,
});
}, 1000);
}
//组件从DOM中卸载
componentWillUnmount() {
clearTimeout(this.timer);
}
handleVote(id) {
//遍历所有的贴子,如果编号相同,则增加点赞后返回
const posts = this.state.posts.map((item) =>
item.id === id ? { ...item, vote: ++item.vote } : item
);
//重新设置状态
this.setState({
posts,
});
}
render() {
return (
<div>
<h2>帖子列表:</h2>
<ul>
{this.state.posts.map((item) => (
<PostItem post={item} onVote={this.handleVote} />
))}
</ul>
</div>
);
}
}
export default PostList;
修改后的PostItem.js:
import React from "react";
function PostItem(props) {
const { post, onVote } = props;
const handleVote = () => {
onVote(post.id);
};
return (
<li>
<div>{post.title}</div>
<div>
创建人:<span>{post.author}</span>
</div>
<div>
创建时间:<span>{post.date}</span>
</div>
<div>
<button onClick={handleVote}>点赞 {post.vote}</button>
</div>
</li>
);
}
export default PostItem;
运行效果:
2.6.1.组件特殊属性——propTypes
对Component设置propTypes属性,可以为Component的props属性进行类型检查。
import React, { Component } from "react";
import PropTypes from "prop-types";
class Hello extends Component {
render() {
return <h2>Hello {this.props.name}!</h2>;
}
}
//设置组件的props对象的类型信息
Hello.propTypes = {
//要求name为string类型
name: PropTypes.string,
};
export default Hello;
调用:
import ReactDOM from "react-dom/client";
import React from "react";
import Hello from "./Hello";
//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));
let vNode = <Hello name={99} />;
//3、将Vnode渲染到根结点上
root.render(vNode);
PropTypes提供了许多验证工具,用来帮助你确定props数据的有效性。在上面这个例子中,我们使用了PropTypes.stirng。意思是:name的值类型应该是string。 当Component的props接收到一个无效的值时,浏览器控制台就会输出一个警告。因此,<Hello name={99}/> 控制台会出现如下警告:
处于性能原因,类型检查仅在开发模式下进行。
import React from 'react';
import PropTypes from 'prop-types';
class MyComponent extends React.Component {
render() {
// 利用属性做更多得事
}
}
MyComponent.propTypes = {
//你可以定义一个属性是特定的JS类型(Array,Boolean,Function,Number,Object,String,Symbol)。默认情况下,这些都是可选的。
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,
//指定类型为:任何可以被渲染的元素,包括数字,字符串,react 元素,数组,fragment。
optionalNode: PropTypes.node,
// 指定类型为:一个react 元素
optionalElement: PropTypes.element,
//你可以类型为某个类的实例,这里使用JS的instanceOf操作符实现
optionalMessage: PropTypes.instanceOf(Message),
//指定枚举类型:你可以把属性限制在某些特定值之内
optionalEnum: PropTypes.oneOf(['News', 'Photos']),
// 指定多个类型:你也可以把属性类型限制在某些指定的类型范围内
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
]),
// 指定某个类型的数组
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
// 指定类型为对象,且对象属性值是特定的类型
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
//指定类型为对象,且可以规定哪些属性必须有,哪些属性可以没有
optionalObjectWithShape: PropTypes.shape({
optionalProperty: PropTypes.string,
requiredProperty: PropTypes.number.isRequired
}),
// 指定类型为对象,且可以指定对象的哪些属性必须有,哪些属性可以没有。如果出现没有定义的属性,会出现警告。
//下面的代码optionalObjectWithStrictShape的属性值为对象,但是对象的属性最多有两个,optionalProperty 和 requiredProperty。
//出现第三个属性,控制台出现警告。
optionalObjectWithStrictShape: PropTypes.exact({
optionalProperty: PropTypes.string,
requiredProperty: PropTypes.number.isRequired
}),
//加上isReqired限制,可以指定某个属性必须提供,如果没有出现警告。
requiredFunc: PropTypes.func.isRequired,
requiredAny: PropTypes.any.isRequired,
// 你也可以指定一个自定义的验证器。如果验证不通过,它应该返回Error对象,而不是`console.warn `或抛出错误。`oneOfType`中不起作用。
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
},
//你也可以提供一个自定义的验证器 arrayOf和objectOf。如果验证失败,它应该返回一个Error对象。
//验证器用来验证数组或对象的每个值。验证器的前两个参数是数组或对象本身,还有对应的key。
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
if (!/matchme/.test(propValue[key])) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
})
示例:
Hello.js
import React, { Component } from "react";
import PropTypes from "prop-types";
class Hello extends Component {
render() {
return <h2>Hello {this.props.name}!</h2>;
}
}
//设置组件的props对象的类型信息
Hello.propTypes = {
//要求name为string类型
name: PropTypes.string,
sex: PropTypes.oneOf(["男", "女"]),
//props 属性对象 ,propName 当前属性名,componentName组件名
phone: function (props, propName, componentName) {
if (!/\d{13}/.test(props[propName])) {
return new Error("手机号码必须是13位的数字");
}
},
};
export default Hello;
index.js
import ReactDOM from "react-dom/client";
import React from "react";
import PostList from "./PostList";
import Hello from "./Hello";
//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));
let vNode = <Hello name={99} sex="未知" phone="abc" />;
//3、将Vnode渲染到根结点上
root.render(vNode);
结果:
static propTypes={
属性:PropTypes.类型.(是否必填,不能为空)
}
//city必须是string类型且必须填写
city: PropTypes.string.isRequired,
限制单个子元素
使用 PropTypes.element 你可以指定只有一个子元素可以被传递给组件。
//将children限制为单个子元素。
Greeting.propTypes = {
name: PropTypes.string,
children: PropTypes.element.isRequired
};
如果如下方式引用Greeting组件:
//传了两个子元素给组件Greeting那么,控制台会出现警告
//传了两个子元素给组件Greeting那么,控制台会出现警告
<Greeting name={'world'}>
<span>子元素1</span>
<span>子元素2</span>
</Greeting>
警告如图:
限制其它子元素
children: PropTypes.number,
获取子元素:
使用children
class Hello extends Component {
render() {
return (
<h2>
Hello {this.props.name}! {this.props.children}
</h2>
);
}
}
使用属性
// index.tsx
import React from 'react'
import NavBar from './NavBar'
const ReactSlot = () => {
return (
<div>
<NavBar
leftSlot={<div>left---这里内容可以随意填充</div>}
centerSlot={<div>center---这里内容可以随意填充</div>}
rightSlot={<div>right---这里内容可以随意填充</div>}
></NavBar>
</div>
)
}
export default ReactSlot
// NavBar.tsx
import React, { ReactNode } from 'react'
import './navbar.css'
type Props = {
leftSlot: ReactNode
centerSlot: ReactNode
rightSlot: ReactNode
}
const NavBar = (props:Props) => {
return (
<div className='navbar-container'>
<div className='navbar-left'>
{props.leftSlot}
</div>
<div className='navbar-center'>
{props.centerSlot}
</div>
<div className='navbar-right'>
{props.rightSlot}
</div>
</div>
)
}
export default NavBar
你可以给组件分配一个特殊的defaultProps属性。
//给Greeting属性中的name值指定默认值。当组件引用的时候,没有传入name属性时,会使用默认值。
Greeting.defaultProps = {
name: 'Stranger'
};
// ES6可以这样写
class Greeting extends React.Component {
static defaultProps = {
name: 'stranger'
}
render() {
return (
<div>Hello, {this.props.name}</div>
)
}
}
Hello.js
import React, { Component } from "react";
import PropTypes from "prop-types";
class Hello extends Component {
render() {
return (
<h2>
Hello {this.props.name}! {this.props.children} {this.props.city}
</h2>
);
}
}
//设置组件的props对象的类型信息
Hello.propTypes = {
//要求name为string类型
name: PropTypes.string,
sex: PropTypes.oneOf(["男", "女"]),
//props 属性对象 ,propName 当前属性名,componentName组件名
phone: function (props, propName, componentName) {
if (!/\d{13}/.test(props[propName])) {
return new Error("手机号码必须是13位的数字");
}
},
children: PropTypes.number,
//city必须是string类型且必须填写
city: PropTypes.string.isRequired,
};
Hello.defaultProps = {
city: "珠海",
};
export default Hello;
如果有默认值就变成了非必填了。
与传统使用CSS给HTML添加样式的方式相比,React在给元素添加样式的时候方式有所不同。React的核心哲学之一就是让可视化的组件自包含,并且可复用。这就是为什么HTML元素和Javascript放在一起组成了(组件)。本节内容我们将介绍React定义样式的方式。
首先创建Person.css,定义我们所需要的样式,具体样式代码定义为如下形式:
.Person{width:60%;margin:16pxauto;border:1pxsolid#eee;box-shadow:2px3px#ccc;padding:16px;text-align:center;}
input{width:200px;border:1pxsolid#eee;outline:none;border-radius:10px;padding:16px;}
如果要使用样式,需要在Person.js组件中引入:
import'./Person.css'
这时可以查看页面变化了。
React推崇的是内联的方式定义样式。这样做的目的就在于让你的组件更加的容易复用。下面给按钮添加一个内联样式:
来到App.js文件,将button按钮定义为如下形式:
style={{backgroundColor:'white',border:'1px solid blue',padding:'8px',cursor:'pointer'}}>
更改状态值
需要注意的是,JSX中使用样式对象定义内联样式,复合样式使用驼峰命名法,对象属性直接使用逗号隔开。
// 让数组中的每一项变成双倍
const numbers = [2,2,4,5];
const doubles = numbers.map((item,index) => {
return <li style={{ color: 'red', fontWeight: 200 }} key={index}> {item * 2}}</li>
})
使用图片
import React from "react";
import like from "./assets/images/like.png";
function PostItem(props) {
const { post, onVote } = props;
const handleVote = () => {
onVote(post.id);
};
return (
<li>
<div>{post.title}</div>
<div>
创建人:<span>{post.author}</span>
</div>
<div>
创建时间:<span>{post.date}</span>
</div>
<div>
<span>
<img src={like} onClick={handleVote} />
</span>
<span>{post.vote}</span>
</div>
</li>
);
}
export default PostItem;
注意图片不要放到src目录以外。
其实React组件并不是真正的DOM, 而是会生成JS对象的虚拟DOM, 虚拟DOM会经历创建,更新,删除的过程
这一个完整的过程就构成了组件的生命周期,React提供了钩子函数让我们可以在组件生命周期的不同阶段添加操作
在 React v17版本删除componentWillMount()
、componentWillReceiveProps()
、componentWillUpdate()
这三个函数,保留使用 UNSAFE_componentWillMount()
、UNSAFE_componentWillReceiveProps()
、UNSAFE_componentWillUpdate()
这张图是从 react生命周期链接里找的,里面有可以根据react不同版本查看对应的生命周期函数。
常用的:
不常用的:
react的生命周期大概分为
说明:
React.Component
,则必须调用super(props)
this
实例注意: constructor 构造函数只在初始化化的时候执行一次
示例代码如下:
import React, { Component } from "react";
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this);
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<h2>计算器</h2>
<button onClick={this.handleClick}>点击加1</button>
<span> {this.state.count} </span>
</div>
);
}
}
export default Counter;
运行结果:
组件state也可以不定义在constructor构造函数中,事件函数也可以通过箭头函数处理this问题
因此如果不想使用constructor 也可以将两者移出
示例代码如下
import React, { Component } from "react";
class Counter extends Component {
//初始化组件状态
state = {
count: 0,
};
//箭头函数处理this问题
handleClick = () => {
console.log(this);
this.setState(() => ({ count: this.state.count + 1 }));
};
render() {
return (
<div>
<h2>计算器</h2>
<button onClick={this.handleClick}>点击加1</button>
<span> {this.state.count} </span>
</div>
);
}
}
export default Counter;
输出结果:
箭头函数中的this指向了当前组件实例,而普通函数则指向undefined。箭头函数
内this
是词法作用域,由上下文确定。
props
能更新到组件内部 state
中,必须是静态的。它应返回一个对象来更新 state,如果返回 null
则不更新任何内容。渲染函数
,唯一的一定不能省略的函数,必须有返回值,返回null或false表示不渲染任何DOM元素。它是一个仅仅用于渲染的纯函数,返回值完全取决于this.state和this.props,不能在函数中任何修改props、state、拉取数据等具有副作用的操作。render函数返回的是JSX的对象,该函数并不因为这渲染到DOM树,何时进行真正的渲染是有React库决定的。挂载成功函数
。该函数不会再render函数调用完成之后立即调用,因为render函数仅仅是返回了JSX的对象,并没有立即挂载到DOM树上,而componentDidMount是在组件被渲染到DOM树之后被调用的。另外,componentDidMount函数在进行服务器端渲染时不会被调用。import React, { Component } from "react";
class Hi extends Component {
constructor(props) {
super(props);
console.log("constructor()函数被调用,构造函数");
this.state = {
n: 100,
};
}
render() {
console.log("render()函数被调用,返回UI描述");
return <h2>Hello {this.props.name}!</h2>;
}
static getDerivedStateFromProps() {
console.log(
"getDerivedStateFromProps()函数被调用,让 props 能更新到组件内部 state中"
);
return null;
}
componentDidMount() {
console.log("componentDidMount()函数被调用,组件被挂载到DOM后");
}
}
export default Hi;
运行结果:
修改案例,查看this
import React, { Component } from "react";
class Hi extends Component {
constructor(props) {
super(props);
console.log("constructor()函数被调用,构造函数", this);
this.state = {
n: 100,
};
}
render() {
console.log("render()函数被调用,返回UI描述", this);
return <h2>Hello {this.props.name}!</h2>;
}
static getDerivedStateFromProps() {
console.log(
"getDerivedStateFromProps()函数被调用,让 props 能更新到组件内部 state中",
this
);
return null;
}
componentDidMount() {
console.log("componentDidMount()函数被调用,组件被挂载到DOM后", this);
}
}
export default Hi;
当组件挂载到DOM树上之后,props/state
被修改会导致组件进行更新操作。更新过程会以此调用如下的生命周期函数:
是否重新渲染组件
返回bool值,true表示要更新,false表示不更新,使用得当将大大提高React组件的性能,避免不需要的渲染。渲染函数
。更新完成函数
。说明:
shouldComponentUpdate
函数使用来判读是否更新渲染组件import React, { Component } from "react";
class Counter extends Component {
constructor(props) {
super(props);
console.log("constructor()构造函数", this);
}
//初始化组件状态
state = {
count: 0,
};
//箭头函数处理this问题
handleClick = () => {
console.log("调用this.setState更新状态", this);
this.setState(() => ({ count: this.state.count + 1 }));
};
render() {
console.log("render()渲染UI", this);
return (
<div>
<h2>计算器</h2>
<button onClick={this.handleClick}>点击加1</button>
<span> {this.state.count} </span>
</div>
);
}
static getDerivedStateFromProps() {
console.log(
"getDerivedStateFromProps()函数被调用,让 props 能更新到组件内部 state中",
this
);
return null;
}
//是否允许组件更新
shouldComponentUpdate(props, state) {
console.log("shouldComponentUpdate()函数被调用,", props, state, this);
return true;
}
componentDidMount() {
console.log("componentDidMount()挂载成功", this);
}
}
export default Counter;
结果
说明:
import React, { Component } from "react";
class Counter extends Component {
constructor(props) {
super(props);
console.log("constructor()构造函数", this);
}
//初始化组件状态
state = {
count: 0,
};
//箭头函数处理this问题
handleClick = () => {
console.log("调用this.setState更新状态", this);
this.setState(() => ({ count: this.state.count + 1 }));
};
render() {
console.log("render()渲染UI", this);
return (
<div>
<h2>计算器</h2>
<button onClick={this.handleClick}>点击加1</button>
<span> {this.state.count} </span>
</div>
);
}
static getDerivedStateFromProps() {
console.log(
"getDerivedStateFromProps()函数被调用,让 props 能更新到组件内部 state中",
this
);
return null;
}
//是否允许组件更新
shouldComponentUpdate(props, state) {
console.log(
"shouldComponentUpdate()函数被调用,是否允许组件更新",
props,
state,
this
);
return true;
}
//组件更新后
componentDidUpdate(prevProps, prevState) {
console.log(
"componentDidUpdate()被调用,组件更新后",
prevProps,
prevState,
this
);
}
componentDidMount() {
console.log("componentDidMount()挂载成功", this);
}
}
export default Counter;
结果
注意state是更新前的状态。
可以通过React.createRef()创建Refs并通过ref属性联系到React组件。Refs通常当组件被创建时被分配给实例变量,这样它们就能在组件中被引用。
import React, { Component } from "react";
class RefDemoCom extends Component {
constructor(props) {
super(props);
this.txtName = React.createRef();
}
clickHandler = () => {
console.log(this.txtName);
this.txtName.current.focus();
this.txtName.current.value = new Date().toLocaleString();
this.txtName.current.style.color = "blue";
};
render() {
return (
<div>
<h2>React.createRef()</h2>
<input type="text" ref={this.txtName} />
<button onClick={this.clickHandler}>点击获得按钮</button>
</div>
);
}
}
export default RefDemoCom;
说明:
getSnapshotBeforeUpdate
必须跟componentDidUpdate
一起使用,否则就报错componentWillReceiveProps
和componentWillUpdate
一起使用getSnapshotBeforeUpdate
返回的结果将作为参数传递给componentDidUpdate
示例:
import React, { Component } from "react";
class BeforeUpdateDemo extends Component {
constructor(props) {
super(props);
this.state = { name: "tom" };
}
componentDidMount() {
setTimeout(() => {
this.setState({ name: "jack" });
}, 1000);
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log("更新前的状态:", prevState);
document.getElementById("prev").innerHTML = "更新前:" + prevState.name;
return document.getElementById("container").clientHeight;
}
componentDidUpdate(prevProps, prevState, preHeight) {
console.log("更新后的状态:", this.state);
document.getElementById("next").innerHTML = "更新后:" + this.state.name;
const height = document.getElementById("container").clientHeight;
console.log("原高度:" + preHeight);
console.log("现高度:" + height);
}
render() {
return (
<div id="container">
<h2>示例</h2>
<div id="prev"></div>
<div id="next"></div>
</div>
);
}
}
export default BeforeUpdateDemo;
运行结果:
清除计时器
、清除事件监听
。示例:
componentWillUnmount()
会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount()
中创建的订阅等。
componentWillUnmount()
中不应调用 setState()
,因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。
import React, { Component } from "react";
class HelloComponent extends Component {
componentWillUnmount() {
console.log("componentWillUnmount()被调用了!");
}
render() {
return <div>HelloComponent被加载了</div>;
}
}
class WillUnmountDemo extends Component {
state = {
i: 0,
};
handleClick = () => {
this.setState({
i: ++this.state.i,
});
};
render() {
return (
<div>
<h2>计算器</h2>
<button onClick={this.handleClick}>i的当前值:{this.state.i}</button>
{this.state.i % 2 === 0 ? (
<HelloComponent />
) : (
<h2>HelloComponent未加载</h2>
)}
</div>
);
}
}
export default WillUnmountDemo;
结果:
React v16.3虽然是一个小版本升级,但是却对React组件生命周期函数有巨大变化。
新增生命周期如下:
同时逐渐废弃了以下生命周期:
17 版本删除了 componentWillMount()
、componentWillReceiveProps()
、componentWillUpdate()
这三个函数,保留使用 UNSAFE_componentWillMount()
、UNSAFE_componentWillReceiveProps()
、UNSAFE_componentWillUpdate()
3.0、页面中有一个按钮,点击按钮显示隐藏右边的文字Hello,初始化时显示,点击第一次隐藏,再点击显示。。。
3.1、定义一个组件,当文本框中输入内容时在文本框后显示输入的值,双向绑定。
3.2、请完成课程中的所有示例。
3.3、请定义一个vue分页组件,可以实现客户端分页功能,接收参数
3.4、定义一个对话框组件,要求显示在body标签内
3.5、定义一个选项卡组件,3个不同的组件,点击卡片名称时动态切换。
3.6、完成如下示例
举例:举个在实际项目中使用此生命周期的例子,在聊天时的气泡页会遇到弹新的消息气泡场景,此时组件重新渲染了,如果不重新给外层包裹的dom计算滚动高度,会导致dom不停下滑。
在线demo: getSnapshotBeforeUpdate例子
而使用getSnapshotBeforeUpdate可以优化此场景。在getSnapshotBeforeUpdate生命周期中记录外层dom元素的高度perScrollHeight
,在componentDidUpdate中重新计算外层dom的scrollTop。此时,页面滚动一段距离不会出现消息跳动的现象 优化后的例子
this.listRef.current.scrollTop = curScrollTop + (this.listRef.current.scrollHeight - perScrollHeight)
代码: github代码
3.7、定义一个子组件,每隔1秒数字加1,在父组件中定义一个按钮进行显示隐藏子组件,隐藏子组件时要求停止计数,点击显示时从0开始重新计数。