一、JSX
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 “元素”。
1.2、为什么使用 JSX?
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格式)
优势:
- 声明式语法更加直观
- 与HTML结构相同
- 降低了学习成本、提升开发效率
注意:JSX 并不是标准的 JS 语法,是 JS 的语法扩展,浏览器默认是不识别的,脚手架中内置的 @babel/plugin-transform-react-jsx 包,用来解析该语法
1.3、JSX中使用js表达式
目标任务:
能够在JSX中使用表达式
语法
{ JS 表达式 }
const name = '张三' <h1>你好,我叫{name}</h1> // <h1>你好,我叫
张三
</h1>
可以使用的表达式
- 字符串、数值、布尔值、null、undefined、object( [] / {} )
- 1 + 2、'abc'.split('')、['a', 'b'].join('-')
- 内置函数,自定义函数
特别注意
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 拆分为多行。同时,我们建议将内容包裹在括号中,虽然这样做不是强制要求的,但是这可以避免遇到自动插入分号陷阱。
1.4. JSX列表渲染
1.4.1、map函数
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]
1.4.2、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
属性
- key 在 HTML 结构中是看不到的,是 React 内部用来进行性能优化时使用
- key 在当前列表中要唯一的字符串或者数值(String/Number)
- 如果列表中有像 id 这种的唯一值,就用 id 来作为 key 值
- 如果列表中没有像 id 这种的唯一值,就可以使用 index(下标)来作为 key 值
1.5、JSX条件渲染
目标任务:
能够在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
1.6. JSX样式处理
目标任务:
能够在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
1.7、注意事项
- JSX必须有一个根节点,如果没有根节点,可以使用
<></>
(幽灵节点)替代 - 所有标签必须形成闭合,成对闭合或者自闭合都可以
- JSX中的语法更加贴近JS语法,属性名采用驼峰命名法
class -> className
for -> htmlFor
- JSX支持多行(换行),如果需要换行,需使用
()
包裹,防止bug出现
1.8、JSX 也是一个表达式
在编译之后,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>;}
1.9、JSX 中指定属性
你可以通过使用引号,来将属性值指定为字符串字面量:
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 属性名称的命名约定。
属性也可以是一个箭头函数:
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);
1.10、使用 JSX 指定子元素
假如一个标签里面没有内容,你可以使用 />
来闭合标签,就像 XML 语法一样:
const element = <img src={user.avatarUrl} />;
JSX 标签里能够包含很多子元素:
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
1.11、JSX 防止注入攻击
你可以安全地在 JSX 当中插入用户输入内容:
const title = response.potentiallyMaliciousInput;
// 直接使用是安全的:
const element = <h1>{title}</h1>;
React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。
1.12、JSX 表示对象
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);
二、组件 Component
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展,但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。如果我们将一个个功能块拆分后,就可以像搭建积木一下来搭建我们的项目。![](/i/l/?n=22&i=blog/63651/202211/63651-20221101160540109-1097535676.png)
2.1、SPA
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.页面复杂度提高很多,复杂逻辑难度成倍
2.2、什么是组件?
组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。
组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
组件它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
2.3、组件定义
组件是 React的核心慨念,定 React应用程序的基石。组件将应用的UI拆分成独立的、可复用的模块,React 用任厅止定田一个一个组件搭建而成的。
定义一个组件有两种方式,便用ES 6 class(类组件)和使用函数(函数组件)。我们先介绍使用class定义组件方式。
2.3.1、使用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);
运行效果:
2.3.2、使用函数定义组件
定义组件最简单的方式就是编写 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);
运行结果:
约定说明
-
组件的名称必须首字母大写,react内部会根据这个来判断是组件还是普通的HTML标签
-
函数组件必须有返回值,表示该组件的 UI 结构;如果不需要渲染任何内容,则返回 null
-
组件就像 HTML 标签一样可以被渲染到页面中。组件表示的是一段结构内容,对于函数组件来说,渲染的内容是函数的返回值就是对应的内容
-
使用函数名称作为组件标签名称,可以成对出现也可以自闭合
2.4、组件的props
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);
运行结果:
2.5、组件的state
组件的 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来实现。
2.6、有状态组件和无状态组件
是不是每个组件内部都需要定义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、属性校验和默认属性
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}/> 控制台会出现如下警告:
处于性能原因,类型检查仅在开发模式下进行。
2.6.2.PropTypes的更多验证器
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);
结果:
2.6.3. 子元素
static propTypes={ 属性:PropTypes.类型.(是否必填,不能为空) }
//city必须是string类型且必须填写 city: PropTypes.string.isRequired,
2.6.4. 子元素
限制单个子元素
使用 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
2.6.5.指定默认属性值
你可以给组件分配一个特殊的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;
如果有默认值就变成了非必填了。
2.7、React-组件样式的两种方式
与传统使用CSS给HTML添加样式的方式相比,React在给元素添加样式的时候方式有所不同。React的核心哲学之一就是让可视化的组件自包含,并且可复用。这就是为什么HTML元素和Javascript放在一起组成了(组件)。本节内容我们将介绍React定义样式的方式。
2.7.1、第一种方式:选择器样式
首先创建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'
这时可以查看页面变化了。
2.7.2、第二种方式:内联样式
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目录以外。
2.8、组件的生命周期
其实React组件并不是真正的DOM, 而是会生成JS对象的虚拟DOM, 虚拟DOM会经历创建,更新,删除的过程
这一个完整的过程就构成了组件的生命周期,React提供了钩子函数让我们可以在组件生命周期的不同阶段添加操作
在 React v17版本删除componentWillMount()
、componentWillReceiveProps()
、componentWillUpdate()
这三个函数,保留使用 UNSAFE_componentWillMount()
、UNSAFE_componentWillReceiveProps()
、UNSAFE_componentWillUpdate()
这张图是从 react生命周期链接里找的,里面有可以根据react不同版本查看对应的生命周期函数。
2.8.1、生命周期总览
常用的:
不常用的:
react的生命周期大概分为
- 组件装载(Mount)组件第一次渲染到Dom树
- 组件更新(update)组件state,props变化引发的重新渲染
- 组件卸载(Unmount)组件从Dom树删除
2.8.2、构造方法 constructor
说明:
- 如果不需要初始化state,或者不进行方法的绑定,则不需要实现constructor构造函数
- 在组件挂载前调用构造函数,如果继承
React.Component
,则必须调用super(props)
- constructor通常用于处理了state初始化和为事件处理函数绑定
this
实例 - 尽量避免将props外部数据赋值给组件内部state状态
注意: 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
是词法作用域,由上下文确定。
2.8.3.组件装载(Mount)
- constructor: 在此初始化state,绑定成员函数this环境,props本地化,constructor 构造函数只在初始化化的时候执行一次
- getDerivedStateFromProps:在创建时或更新时的render之前执行,让
props
能更新到组件内部state
中,必须是静态的。它应返回一个对象来更新 state,如果返回null
则不更新任何内容。 - render:
渲染函数
,唯一的一定不能省略的函数,必须有返回值,返回null或false表示不渲染任何DOM元素。它是一个仅仅用于渲染的纯函数,返回值完全取决于this.state和this.props,不能在函数中任何修改props、state、拉取数据等具有副作用的操作。render函数返回的是JSX的对象,该函数并不因为这渲染到DOM树,何时进行真正的渲染是有React库决定的。 - componentDidMount:
挂载成功函数
。该函数不会再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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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;View Code
2.8.4、组件更新(update)
当组件挂载到DOM树上之后,props/state
被修改会导致组件进行更新操作。更新过程会以此调用如下的生命周期函数:
- shouldComponentUpdate(nextProps, nextState):
是否重新渲染组件
返回bool值,true表示要更新,false表示不更新,使用得当将大大提高React组件的性能,避免不需要的渲染。 - getSnapshotBeforeUpdate:在render之后,React更新dom之前执行。它使您的组件可以在可能更改之前从DOM捕获一些信息(例如滚动位置),例如在聊天气泡页中用来计算滚动高度。它返回的任何值都将作为参数传递给componentDidUpdate()
- render:
渲染函数
。 - componentDidUpdate:
更新完成函数
。
2.8.5、shouldComponentUpdate函数
说明:
shouldComponentUpdate
函数使用来判读是否更新渲染组件- 函数返回值是一个布尔值,为true,表示继续走其他的生命周期函数,更新渲染组件
- 如果返回一个false表示,不在继续向下执行其他的生命周期函数,到此终止,不更新组件渲染
- 函数接受两个参数, 第一个参数为props将要更新的数据, 第二个参数为state将要更新的数据
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;
结果
2.8.6、componentDidUpdate函数
说明:
- 组件render执行后,页面渲染完毕了以后执行的函数
- 接受两个参数,分别为更新前的props和state
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是更新前的状态。
2.8.7、React.createRef() 非生命周期函数
可以通过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;
2.8.8、getSnapshotBeforeUpdate函数
说明:
getSnapshotBeforeUpdate
必须跟componentDidUpdate
一起使用,否则就报错- 但是不能与
componentWillReceiveProps
和componentWillUpdate
一起使用 - state和props更新都会触发这个函数的执行, 在render函数之后执行
- 接受两个参数,更新前的props和当前的state
- 函数必须return 返回结果
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;
运行结果:
2.8.9.组件卸载(Unmount)
- componentWillUnmount:当React组件要从DOM树上删除前,会调用一次这个函数。这个函数经常用于去除componentDidMount函数带来的副作用,例如
清除计时器
、清除事件监听
。
示例:
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;
结果:
2.8.10、React V16.3 新增的生命周期方法
React v16.3虽然是一个小版本升级,但是却对React组件生命周期函数有巨大变化。
新增生命周期如下:
- getDerivedStateFromProps
- getSnapshotBeforeUpdate
同时逐渐废弃了以下生命周期:
- componentWillReceiveProps
- componentWillMount
- componentWillUpdate
17 版本删除了 componentWillMount()
、componentWillReceiveProps()
、componentWillUpdate()
这三个函数,保留使用 UNSAFE_componentWillMount()
、UNSAFE_componentWillReceiveProps()
、UNSAFE_componentWillUpdate()
2.8.11、不能使用setState的生命周期
2.8.12、小结
三、作业
3.1、定义一个组件,当文本框中输入内容时在文本框后显示输入的值,双向绑定。
3.2、请完成课程中的所有示例。
3.3、请定义一个vue分页组件,可以实现客户端分页功能,接收参数
3.4、定义一个对话框组件,要求显示在body标签内
3.5、定义一个选项卡组件,3个不同的组件,点击卡片名称时动态切换。![](/i/l/?n=22&i=blog/63651/202211/63651-20221104113432182-1636032943.png)
3.6、完成如下示例
举例:举个在实际项目中使用此生命周期的例子,在聊天时的气泡页会遇到弹新的消息气泡场景,此时组件重新渲染了,如果不重新给外层包裹的dom计算滚动高度,会导致dom不停下滑。
在线demo: getSnapshotBeforeUpdate例子
![动图封面](https://pic4.zhimg.com/v2-5494ddcb87d6ebd552185e1e49e5cb73_b.jpg)
而使用getSnapshotBeforeUpdate可以优化此场景。在getSnapshotBeforeUpdate生命周期中记录外层dom元素的高度perScrollHeight
,在componentDidUpdate中重新计算外层dom的scrollTop。此时,页面滚动一段距离不会出现消息跳动的现象 优化后的例子
this.listRef.current.scrollTop = curScrollTop + (this.listRef.current.scrollHeight - perScrollHeight)
代码: github代码
3.7、定义一个子组件,每隔1秒数字加1,在父组件中定义一个按钮进行显示隐藏子组件,隐藏子组件时要求停止计数,点击显示时从0开始重新计数。
四、视频