React-类组件精简笔记
1. React概述
1.1 概述
React是一个前端开源的用于构建web用户界面的Js库。
1.2 文档
官方文档(英文)、官方文档(中文)、官方文档(新版、开发中、英文)
1.3 背景
前背景是Facebook内部框架,后背景是Facebook维护,使用者贡献代码,于2013年开源。
2. 创建React项目
使用官方提供的create-react-app(React脚手架)创建react项目
使用npm:(推荐)
1 2 3 4 5 6 7 8
| npm install create-react-app@5.0.1 -g
create-react-app react-basic
npm init react-app react-basic
npm run start
|
使用yarn:
1 2 3 4 5 6
| yarn global add create-react-app
yarn create react-app react-basic
yarn start
|
下载安装网络有问题的建议换源,如何换源参考:Node开发相关
项目目录文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ├── README.md ├── package.json ├── package-lock.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js └── setupTests.js
|
安装编辑器拓展,给项目开发提供支持
Vscode编辑器拓展:VSCODE扩展-ES7+ React/Redux/React-Native snippets
3. React初体验
项目入口文件为index.js
文件,接下来以此文件进行初体验
1 2 3 4 5
|
<div id="root"> <h1 title="Hello React" id="title">Hello React</div> </div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
import React from "react";
import ReactDOM from "react-dom/client";
const title = React.createElement( "h1", { id: "title", title: "Hello React" }, "Hello React" )
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(title);
|
React.createElement(type, props, children)
React提供的创建元素的方法
type: 标签名称, 字符串类型.
props: 元素属性, 对象类型, 无属性填入 null
children: 子元素, 普通文本或 createElement 方法返回的元素对象,也可也是react元素数组
return: 元素对象
ReactDOM.createRoot(dom)
React提供的构建web应用的创建根结点的方法
dom:webapi获取的DOM元素(document.querySelecter)
return:react根结点元素对象
根结点对象的render方法
根结点对象里的render方法是用于渲染其他虚拟DOM的,相当于webapi中的document.body.appendChild,render方法的参数可以是react的dom对象,也可以是react-dom对象数组,也可也是jsx内容。
1 2 3 4 5 6 7 8 9 10 11
| import React from "react"; import ReactDOM from "react-dom/client"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render(
[ React.createElement("h1",{},"hello"), <p>我是一个段落</p> ])
|
tips:React通用方法放在React中,web应用的方法放在react-dom包中,移动端应用方法放在react-native-web包中。
4. JSX—JavaScript语法拓展
4.1 JSX概述
JSX语法是另一种在 React 中创建元素的方式,使用它创建元素更简单、直接。
JSX 是 JavaScript XML 的简写,可以理解为在 JavaScript 中书写 XML 格式的代码(XML跨平台)。
1 2 3 4 5 6 7 8 9
| const list = ( <div className="list"> <h2>动物</h2> <ul> <li>修够</li> <li>cat</li> </ul> </div> )
|
JSX不支持在浏览器中直接运行,仅支持在开发时将JSX编译成React项目代码:
问题一:既然 React.createElement 和 JSX 都可以创建元素,那么它们之间有什么关系吗?Babel REPL
虽然我们在编写代码时使用的是 JSX,但是在代码运行之前 babel 会将其转换为 React.createElement 方法的形式,所以它们之间是转换的关系。
问题二:为什么要将 JSX 转换为 React.createElement 方法?
JSX 是 Facebook 为是 JavaScript 添加的语法扩展,它并不是标准的 JavaScript 语法不能直接在浏览器中使用。
4.2 JSX注意事项
(1) JSX 格式美化
如果 JSX 中存在多个标记建议使用小括号进行包裹,否则标签格式看起来不对齐
1 2 3 4
| const jsx = <div> <ul></ul> </div>
|
1 2 3 4 5 6
| const jsx = ( <div> <ul></ul> </div> )
|
(2) 在使用 JSX 语法创建元素时,元素的最外层必须要有一个根标记。
1 2 3 4 5
| const jsx = ( <p>Hello</p> <p>world</p> )
|
1 2 3 4 5 6 7
| const jsx = ( <div> <p>Hello</p> <p>world</p> </div> )
|
为避免因为要满足规定而出现无意义标记,React 提供了幽灵标记,幽灵标记在渲染后不会产生真实的 DOM 对象。
1 2 3 4 5 6 7
| const jsx = ( <> <p>Hello</p> <p>world</p> </> )
|
1 2 3 4 5 6 7
| const jsx = ( <React.Fragment> <p>Hello</p> <p>world</p> </React.Fragment> )
|
幽灵标记可以使用在遍历的时候加key属性使用
(3) 在 JSX 中使用单标记时单标记必须是闭合状态。
1 2
| <input type="text"/> <img src="" alt="" />
|
(4) 在JSX 中标记属性使用小驼峰式命名法。
小驼峰式命名法是指如果一个标识符由多个单词组成,第一个个单词的首字母小写、其他单词的首字母大写。
1
| <input maxLength="10" readOnly autoFocus />
|
(5) 在 JSX 中为元素添加属性时使用 className 替代 class、使用 htmlFor 替代 for
因为 JSX 本质上仍然是 JavaScript,而 class、for 是 JavaScript 中的关键字不能直接使用。
1 2
| <input type="text" className="todos" /> <label htmlFor="demo"></label>
|
4.3 JSX嵌入表达式
在JSX中插入JavaScript的数据需要使用花括号{}
,并且花括号内只能是表达式的结果
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const name = '张三'; const jsx = <div>hello, {name}</div>
const activeName = "active" const jsx = <input type="text" className={activeName} />
const a = 10 const b = 20 const jsx = <p>{a * b}</p>
function getValue(){ return "hello,world" } const jsx = <h1>{getValue()}</h1>
const isError = true; const jsx = isError ? <div>发生了错误</div> : null
const other = <p style={{width: 200}}> {{name: "李四"}} </p>
|
1 2 3 4 5 6 7 8 9
| const element = ( <> {/* 头部 */} <div>header</div> {/* 底部 */} <div>footer</div> </> )
|
非表达式不能被嵌入 JSX ,错误示范:
1
| const jsx = <div>{if() {}}</div>
|
tips:在 JSX 中 true、false、null、undefiend,将不会被渲染为具体内容!
4.4 JSX条件渲染
(1) 使用if分支语句进行条件渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const isLoading = false;
function getContent() { if (isLoading) { return <span>数据加载中...</span>; } else { return <span>我是数据</span>; } }
const jsx = <div>{getContent()}</div>;
|
(2) 使用三元表达式进行条件渲染
1 2 3 4
| const isLoading = false;
const jsx = isLoading ? <span>数据加载中...</span> : <span>我是数据</span>;
|
(3) 使用逻辑运算符进行条件渲染
1 2 3 4
| const isLoading = false;
const jsx = isLoading && <span>数据加载中...</span>;
|
4.5 JSX列表渲染
(1) JSX中的数组自动展开特性
在JSX中,可以将数组直接放入插值表达式,数组将会被自动展开,数组中的元素会呗直接渲染到该位置。
1 2
| const array = ["list-item-1", "list-item-2", "list-item-3"] const jsx = <div>{array}</div>
|
1 2 3 4 5 6
| const jsxArray = [ <li>list-item-1</li>, <li>list-item-2</li>, <li>list-item-3</li>, ]; const jsx = <ul>{jsxArray}</ul>;
|
使用map方法渲染JSX
1 2 3 4 5 6 7 8 9 10
| const arrayObject = [ { name: "list-item-1" }, { name: "list-item-2" }, { name: "list-item-3" } ] const jsx = arrayObject.map((item,index) => <p key={index}>{item}</p>)
|
4.6 JSX元素样式
(1) 通过 style 属性为元素添加行内样式,样式以对象形式书写
1 2 3 4 5 6 7 8 9 10
| const styles = { width: 200, height: 200, backgroundColor: "skyblue", fontSize: 24, };
const jsx = <div style={styles}></div>;
|
(2) 通过 className 属性为元素添加样式表中的样式(全局样式)
1 2 3 4 5 6
| .box { width: 200px; height: 200px; background: skyblue; }
|
1 2 3 4
| import "./styles.css"
const jsx = <div className="box"></div>
|
1 2 3
| const isActive = true; const jsx = <div className={isActive ? "active" : ""}></div>;
|
(3) 通过 className 属性为元素添加样式表中的样式(组件级样式)
注意:组件级样式需要按__.module.css
方式命名
1 2 3 4 5 6 7 8
| .red { color: red; }
:global html { height: 100%; }
|
1 2 3 4 5 6 7
| import styles from "./App.module.css";
export default class App extends React.Component { render() { return <div className={styles.red}>文字</div> } }
|
(4) 通过 classnames 第三方库为元素动态设置类名
通过 classnames 第三方库可以实现通过 className 属性为元素动态绑定任意个类名
1 2
| classNames("foo", { bar: true });
|
1 2 3 4
| import classNames from "classnames";
const jsx = <div className={classNames("box", { active: true })}></div>;
|
4.7 插入本地图片
方法一:导入图片对象,放置在src属性上
1 2 3 4 5 6 7
| import logo from "./assets/images/logo.png";
export default class App extends React.Component { render() { return <img src={logo} alt="" />; } }
|
方法二:在src属性上使用插值并使用require导入图片对象
1
| <img src={require("./assets/images/logo.png")} alt="" />;
|
图片路径相关:
process.env.PUBLIC_URL
在全局环境下,此对象里的PUBLIC_URL为项目的src目录路径,使用此路径可以接着后边访问src下的图片路径,例如:
1
| <img src={`${process.env.PUBLIC_URL}/assets/images/logo.png`} alt="" />
|
4.8 JSX元素事件
绑定事件
在 JSX 中,事件名称采用驼峰式命名法,事件处理函数通过插值表达式嵌入到 JSX 中。
语法:on + 事件名称 = { 事件处理程序 } , 例如 onClick = { onClickHandler }
1 2 3 4 5
| function onClickHandler(){ console.log("clicked") }
const jsx = <button onClick={onClickHandler}>点击</button>
|
在类组件中元素绑定事件
1 2 3 4 5 6 7 8
| export default class App extends React.Component { onClickHandler (){ } render() { return <button onClick={this.onClickHandler}>点击</button> } }
|
4.9 JSX 选项卡
5. 类组件
5.1 概念
在前端开发中组件就是用户界面当中的一块独立的区域,在组件内部会包含这块区域中的视图代码、样式代码以及逻辑代码。
React 采用组件的方式构建用户界面,通过将多个组件进行组合形成完成的用户界面,就像搭积木。
组件设计思想
组件的核心思想之一就是 复用,定义一次到处使用。
组件可以用来封装用户界面中的重复区块,复用重复区块。
组件的另一个核心思想是 解耦。
传统web页面中,一个html就是一个页面,所有代码都被写在一个页面,容易导致代码冲突,而组件化开发中,每个组件都有自己的作用域,组件与组件之间的代码不会发生冲突,从而传统开发中一个页面内代码冲突的问题。
组件化开发的好处:复用,解耦。
5.2 创建组件
在React里一个类继承了Component类就是一个类组件
1 2 3 4 5 6 7 8 9 10 11
| import ReactDOM from "react-dom/client" import { Component } from "react";
class App extends Component { render() { return <div>头部组件</div> } }
const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
|
组件注意事项:
- 组件名称首字母大写
- 继承了 Component 的类才是 React 组件
- 类中必须包含 render 方法用于渲染用户界面,render 方法名字是固定的,不渲染时返回null
在实际 React 项目中,组件作为独立的个体一般都会被放置在单独的文件中方便维护。
1 2 3 4 5 6 7 8
| import { Component } from "react";
export default class App extends Component { render() { return <div>头部组件</div> } }
|
1 2 3 4 5
| import App from "./App"
const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
|
5.3 必要前置知识点this指向问题
this 翻译过来表示“这个”,是个代词,在不同场景this可以指代不同的事物。
在 JavaScript 代码运行的过程中,运行环境的不同 this 也随之不同,这个环境指全局作用域、函数作用域。
简单一句话概括:谁调用此方法,此方法的this就指向谁
调用方式 |
this指向 |
普通函数调用 |
window |
构造函数调用 |
实例对象 原型对象里面的方法也指向实例对象 |
对象方法调用 |
该方法归属的对象 |
事件绑定普通函数 |
绑定事件的对象 |
定时器函数 |
window |
立即执行函数 |
window |
箭头函数没有自己的this,箭头函数的this指向上一层。
5.4 类组件方法中的this
1.在类组件的render方法中、constructor 方法中的 this 指向当前类的实例对象。
1 2 3 4 5 6 7 8 9 10
| export default class App extends React.Component { constructor() { super(); console.log("constructor", this); } render() { console.log("render", this); return null; } }
|
2.类组件中元素事件的事件处理函数的this指向undefined
5.5 组件级样式
组件级样式是指样式只在当前组件中生效,当前组件样式不会溢出到其他外部组件从而影响页面中的其他部分。(命名规范必须遵循:.module.css
结尾)
1 2 3 4 5 6 7 8 9 10 11 12 13
| .page { width: 100%; }
.page .header { height: 85px; }
.page .header .logo { width: 100px; height: 100%; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import React from "react"; import styles from "./App.module.css";
export default class App extends React.Component { render() { return ( <div className={styles.page}> <div className={styles.header}> <div className={styles.logo}></div> </div> </div> ); } }
|
React 中组件级样式的实现依赖于 webpack 提供的CSS模块功能。
在构建应用的过程中,webpack会为当前组件使用的类名进行重命名。
重命名为 [ 组件名称 + 原始类名 + 随机字符串 ],从而保证类名下的样式不会溢出到组件外部。
5.6 组件状态
描述
状态:某一事物的不同形态。(例如水,达到沸腾时变水蒸气,达到0摄氏度以下时凝结成冰)
web里也有状态:(获取数据的状态)
状态 |
解释 |
空闲 |
在没有发出请求时该区域为空闲状态 |
加载中 |
请求在发出后没有得到响应前该区域为加载中状态 |
加载成功 |
当请求得到响应用户列表渲染成功后该区域为成功状态 |
加载失败 |
当请求未得到正确的响应时该区域为失败状态 |
结束 |
不论请求成功与失败请求都结束了该区域为结束状态 |
状态代码如下:
1 2 3 4 5 6
|
let status = "idle";
|
React 应用使用组件构建用户界面,所以用户界面的状态需要被声明在组件中,被声明在组件中的状态也被叫做组件状态。
创建组件状态
在类组件里设置state属性(且只能是state这个名字),此属性为组件的状态属性,当state里的内容发生了变化时,将会自动调用render方法重新渲染组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { Component } from "react";
export default class App extends Component { constructor(props) { super(props) this.state = { name: "张三" } } render() { return <div>{ this.state.name }</div> } }
export default class App extends Component { state = { name: "小明" } render() { return <div>{ this.state.name }</div> } }
|
操作组件状态
在类组件中只能使用组件实例里的 setState
方法修改组件状态(state),只有这样才能出发视图更新。
当前组件实例下并没有 setState 方法,setState 方法时父类 Component 提供的。
this.setState({ state属性名: 要更改的属性值 })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class App extends Component { state = { count: 0, }; render() { return ( <button onClick={() => { // 1. 更改状态, 接收对象作为参数, 改哪一个状态就传递哪一个状态即可. react 内部会帮助我们进行状态合并操作 // 2. 更新视图 this.setState({ count: this.state.count + 1 }); }} > {this.state.count} </button> ); } }
|
问题!事件处理函数为什么要使用箭头函数?
因为事件处理函数是普通函数的话,this指向为undefined,而不是当前组件实例对象,所以不能通过this调用setState方法,程序也会报错。而箭头函数没有自己的this,箭头函数的this将指它的上层作用域,也就是组件实例对象。
注意:在 state 对象存储多个状态的情况下,使用 setState 更新状态时只传递需要更新的状态即可,React 会先接收我们传递给 setState 方法的状态,再使用它和原有状态进行合并,从而产生新的状态。
组件状态的调试工具
React Developer Tools
也是react开发者工具
下载React开发者工具的网站,从这个网站里搜索React Developer Tools 下载相应版本即可
更改事件函数的this指向
通常我们都希望事件处理函数中的 this 关键字指向组件实例对象。
方式一:事件处理函数使用箭头函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class App extends Component { Handler() { console.log(this) } clickHandler = () => { console.log(this) } render() { return (<> {/* 写法一:直接写在插值里 */} <button onClick={() => this.Handler()}>点击</button> <button onClick={this.clickHandler}>Click</button> </>) } }
|
两个写法都可以将函数里的this指向当前组件实例
写法一小问题:组件状态发生更改后要更新试图,render方法会重新执行,Js引擎将重新创建行内匿名箭头函数,重新给元素进行事件函数绑定,性能有一丢丢损失。
写法二问题:事件处理函数从 原型方法 变成了 实例方法 ,如果当前组件被调用很多次,就会产生很多歌相同的事件处理函数,浪费内存空间。
方式二:通过bind改变事件处理函数的this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class App extends Component { constructor() { super() this.clickHandler = this.clickHandler.bind(this) } Handler (){ console.log(this) } clickHandler (){ console.log(this) } render (){ return (<> {/* 写法一:在插值里更改this */} <button onClick={this.Handler.bind(this)}></button> <button onClick={this.clickHandler}>Click</button> </>) } }
|
写法一问题:render 方法每次重新执行,都会为元素绑定新的由 bind 方法返回的事件处理函数,性能有一丢丢损失。
写法二推荐:在构造函数中将事件处理函数更改 this 指向组件实例,每次重新渲染时都不会重新创建事件处理函数,且this指向更改只执行了一次,性能最优。推荐!
组件原状态不可变
在 React 中关于状态有一核心理念,就是状态不可变,该理念需要被严格遵守。
状态不可变是指在更新组件状态时不能直接操作现有状态,而是要基于现有状态值产生新的状态值。
1 2 3 4 5 6 7 8
| state = { count: 0, list: [1, 2, 3], person: { name: "张三", age: 18, }, };
|
1 2 3 4 5 6 7 8 9 10
| this.state.count = 100; this.state.count++; this.state.list.push(4); this.state.person.name = "李四";
this.setState({ count: 100 }) this.setState({ count: this.state.count + 1 }) this.setState({ list: [...this.state.list, 4] }) this.setState({ person: { ...this.state.person, name: "李四" } })
|
1 2 3 4 5 6 7 8 9 10
| this.setState({ list: [...this.state.list.slice(0, 1), ...this.state.list.slice(2)], });
this.setState({ list: [...this.state.list.slice(0, 1), 4, ...this.state.list.slice(2)], });
|
tips:如何理解 React 状态不可变?
因为 React 在更新真实 DOM 前,要将新旧状态对象 state 进行对比找出要更新的部分,所以当前状态(也就是旧状态不能直接被更改)
5.7 非受控表单
在html的表单元素中会有表单元素自己的 value 值,也就是表单组件自己的状态值,当表单组件的状态由表单组件自己管控时,此组件就是非受控表单。
使用**createRef
**创建表单的引用对象,从表单中获取value。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { Component, createRef } from "react";
class App extends Component { inputRef = createRef() onClickHandler (){ console.log(this.inputRef.current.value) } render(){ return <> {/* 为元素绑定应饮用对象 */} <input type="text" ref={this.inputRef} ></input> <button onClick={() => this.onClickHandler()}></button> </> } }
|
5.8 受控表单
表单的值由组件状态(state)管理。
使用表单控件的 value 属性将组件状态与表单控件进行绑定。
使用表单控件的 onChange 事件实现表单控件和组件状态进行同步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class App extends React.Component { state = { text: "默认值", } changeIpt = (event) => { this.setState({text: event.target.value}) } render() { return ( <input type="text" {/* 将组件状态和文本框的 value 属性进行绑定 */} value={this.state.text} {/* 为文本框绑定 change 事件, 当用户在文本框中输入时更新组件状态 */} onChange={this.changeIpt}> </input> ) } }
|
React 中的受控表单其实和 Vue 中双向数据绑定类似。
注意⚠️:如果只为表单控件添加 value 属性而没有添加 onChange 事件,浏览器控制台将会报警告。
1
| <input type="text" value={this.state.text} />
|
警告翻译:你为表单控件供了 value 属性但是没有提供 onChange 事件,这将渲染一个只读的表单控件(用户无法在表单控件中输入内容)。
如果只是想设置成只读表单,则在表单添加 readOnly 属性即可消除警告。
表单默认值
如果只是想给表单设置默认值,则添加 defaultValue 属性即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class App extends React.Component { state = { text: "默认值", } render() { return ( <input type="text" {/* 给组件设置默认值 */} defaultValue={this.state.text}> </input> ) } }
|
受控表单执行过程:
- 用户出触发表单控件的 onChange 事件,执行 onChange 事件的事件处理函数。
- 在事件处理函数中通过事件对象获取表单控件的最新状态,并使用最新状态更新组件状态。
- 组件状态更新完成后 render 方法重新执行,在 render 方法中通过最新的组件状态渲染视图。
6. 组件通讯-类组件
6.1 组件通讯概述
对于不同组件而言,组件通讯是指数据能够在不同的组件之间进行流动。
在使用组件构建页面时,各个组件之间需要根据数据协同工作时,将会用到组件通讯,即在组件之间传递数据。
组件通讯包含父子通讯
、兄弟通讯
、远方亲戚
通讯。
6.2 组件通讯
在调用组件时通过属性的方式向组件内部传递数据
1 2 3 4 5 6 7 8 9
| import React from "react"; import Person from "./Person";
export default class App extends React.Component { render() { return <Person name="张三" age="20" />; } }
|
在组件内部通过 this.props 获取外部传递进来的数据,this.props 为对象类型。
1 2 3 4 5 6 7 8
| import React from "react";
export default class Person extends React.Component { render() { return <>{this.props.name} {this.props.age}</>; } }
|
以上代码是简写形式,完整形式如下:
1 2 3 4 5 6 7 8
| export default class Person extends React.Component { constructor(props) { super(props); } render() { return <>{this.props.name} {this.props.age}</>; } }
|
在 React 内部实例化类组件时,通过参数的方式接收外部的数据,所以在类的内部是通过构造函数进行参数接收的,由于组件类继承了 Component 类,所以需在要构造函数中调用 super 方法继承父类(Js语法要求),在继承的同时通过 super 将 props 传递到父类 Component 中,因为 React.Component 中有 this.props = props
这行代码,它回将子类传递进来的props进行绑定到 Component 父类中,而子类继承于父类,这样子类就可以通过 this 来获取 props 了。
多数据传递的优化写法
在组件传递数据时,将对象进行展开(解构)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import React from "react";
export default class App extends React.Component { state = { person: { name: "张三", age: 20, isMarry: false, }, }; render() { return <Person {...this.state.person} />; } }
class Person extends React.Component { render() { console.log(this.props); return <></>; } }
|
通讯规范
(1) 组件传递数据的类型可以是任意的。
字符串、数值、布尔值、数组、对象、函数、JSX
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export default class App extends React.Component { render() { return ( <Person name="张三" age={20} isMarry={false} skills={["编程", "射击"]} style={{ color: "red" }} sayHi={() => alert("Hi")} jsx={<em>我是em标记</em>} /> ); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export default class Person extends React.Component { render() { return ( <> <p>{this.props.name}</p> <p>{this.props.age}</p> <p>{this.props.isMarry ? "已婚" : "未婚"}</p> <p>{this.props.skills.map((skill) => <span key={skill}>{skill}</span>)}</p> <p style={this.props.style}>我是红色的文字</p> <p onClick={this.props.sayHi}>打个招呼</p> <p>{this.props.jsx}</p> </> ); } }
|
(2) 在组件和组件之间传递数据时数据流只能是从上到下,不能逆向传递,此特性被称为单向数据流。
单向数据流使应用程序中的数据流变得清晰,程序更加可控、更加容易调试。
Raact 为了让开发者遵循单项数据流原则,将 props 设置为了只读,下层组件不能修改 props 的值,避免破坏单项数据流原则。
1 2 3 4 5 6
| export default class Person extends React.Component { render() { this.props.name = "李四"; } }
|
6.3 父子组件通讯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React from "react"; import Child from "./Child";
export default class Parent extends React.Component { state = { name: "小明", age: 22 }; render() { return <div> 我是服组件 <Child name={this.state.name} age={this.state.age} /> </div> } }
|
1 2 3 4 5 6 7 8 9
| import React from "react";
export default class Child extends React.Component { render() { const { name, age } = this.props; return <div>我是子组件 {name} {age}</div> } }
|
6.4 子父组件通讯
子组件默认情况下没有权限修改父组件的状态,但是父组件本身可以更改自己的状态。
但可以通过传递修改状态的方法给子组件,由子组件进行调用,通过方法参数将要更改的数据传递给父组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import React from "react";
export default class App extend React.Component { constructor(){ super(); this.changeMsg = this.changeMsg.bind(this) } state = { msg: "Hello React" } changeMsg (newMsg){ this.setState({msg: newMsg}) } render (){ return <> <div>父组件:{this.state.msg}</div> {/* 向子组件传递更改状态的方法 */} <Message msg={this.state.msg} changeMsg={this.changeMsg} /> </> } }
|
1 2 3 4 5 6 7 8 9 10
| class Message extends React.Component { render() { return <div> Msg: {this.props.msg} {/* 调用父组件给的方法传递要修改的数据 */} <button onClick={() => this.props.changeMsg("hello wolrd")}>更改msg</button> </div> } }
|
6.5 兄弟组件通讯
由于react中只允许父向子传递状态,而子调用父方法修改状态,所以实现兄弟组件通讯的方法只能将公共的状态存放的共有的父组件中,兄弟组件通过调用父组件传递的方法传递参数进行修改兄弟组件的公共状态。
状态提升:将多组件共同使用的状态(公共状态)放置在最近的公共父级组件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import React from "react";
export default class App extends React.Component { constructor(props) { super(props); this.modifyMsg = this.modifyMsg.bind(this); }
state = { msg: "Hello React", }; modifyMsg(msg) { this.setState({ msg }); }
render() { return ( <> // 3. 向子组件传递公共状态及修改状态的方法 <Message msg={this.state.msg} /> <Machine modifyMsg={this.modifyMsg} /> </> ); } }
class Message extends React.Component { render() { return <div>{this.props.msg}</div>; } }
class Machine extends React.Component { render() { return ( <button onClick={() => this.props.modifyMsg("Hi, React.js")}> button </button> ); } }
|
6.6 跨组件通讯(上下文对象)
官方文档context
为了让兄弟组件实现状态共享我们通常会将状态进行提升,提升至它们最近的公共父级,但是当组件层级关系比较深时这种方式并不理想,因为在这个过程中有很多组件并不需要使用该状态但是却参与了状态的传递,我们将这种情况称之为 prop drilling。
为了解决以上问题,React提供了 Context
(上下文),它允许跨组件层级传递状态,只要该组件在 Context 的范围内都可以直接获取状态。
简单使用步骤分三步:
- 创建context对象 —— createContext(默认值)
- 使用context对象的Provider组件包裹 顶层组件
- 在顶层组件范围内的子组件中使用context对象的Consumer组件包裹,并从中拿到context对象的值
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12
| import React from "react"
export const myContext = createContext({ name: "小明",age: 22})
root.render( <myContext.Provider velue={{ name: "小明",age: 22 }} > <App /> </myContext.Provider> )
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React from "react" import Info from "./Info.js"
export default class App extends React.Component { render(){ return <Middel /> } }
class Middel extends React.Component { render(){ return <Info /> } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import React from "react"
import myContext from "./index.js"
export default class Info extends React.Component { render(){ return ( <myContext.Consumer> {(value) => <div> 姓名:{ value.name } 年龄:{ value.age }</div>} </myContext.Consumer> ) } }
|
上下文对象的修改问题
上下文对象context在 规定中 是不允许子组件进行修改的,但 实际上 是可以修改的,只是子组件中修改后并不会使视图更新。
解决办法:使用组件的state状态对象当作context的值进行传递,并搭配更新的方法传递给子组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import React from "react";
export const { Provider, Consumer } = createContext()
export class App extends React.Component{ constructor(){ this.changeName = this.changeName.bind(this) } state = { name: "张三", age: 22, changeName: (name) => { this.setState({ name }) } } render(){ return ( <Provider value={this.state}> <Info /> </Provider> ) } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React from "react"; import { Consumer } from "./App.js" export default class Info extends React.Component { render(){ return ( <Consumer> {(value)=>{ return <div> <p> { value.name } { value.age } </p> {/* 调用修改状态的方法,使其修改时更新试图 */} <button onClick={ () => value.changeName("小明") }> 修改name为小明 </button> </div> }} </Consumer> ) } }
|
6.7 组件子节点
组件子节点类似于 div 里的子元素,也类似于 Vue 里的插槽,但在组件子节点里可以传递字符串、数值、数组、函数、JSX等。
如何使用组件子节点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export default class App extends React.Component { render() { return <Message>Hello这里是Message组件的子节点</Message> } }
class Message extends React.Component { render(){ return <>{this.props.children}</> } }
export default class App extends React.Component { render() { return <Message children="这里是Message组件的子节点"></Message> } }
|
组件子节点的属性和方法
(1) React.Children.only:子节点是否只有一个,且必须有
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Message extends React.Component { render(){ try { React.Children.only(this.props.children); } catch (error) { return <div>Message 组件的只节点有且只能有一个</div>; } return <div>{this.props.children}</div> } }
|
注意⚠️:子节点中文本字符串不被React识别为组件子节点,必须包裹一个标签,或幽灵标记<></>
(2) React.Children.count:获取组件子节点的数量
1 2 3 4 5
| class Message extends React.Component { render() { return <>{React.Children.count(this.props.children)}</> } }
|
(3) React.Children.map:对组件子节点进行转换(当组件子组件为数组的时候)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export default class App extends React.Component { render() { return ( <Message> <img src="./img1.jpg"></img> <img src="./img2.jpg"></img> </Message> ) } }
class Message extends React.Component { render() { return ( <> {React.Children.map(this.props.children, (item) => ( <a href="https://www.baidu.com">{item}</a> ))} </> ) } }
|
(4) React.Children.toArray:将组件子节点转换为数组
当子节点没有内容的时候将被转换成空数组
当子节点有多个时转为数组类型
1 2 3 4 5 6 7 8
| class Message extends React.Component { console.log(React.Children.toArray(this.props.children)) render() { return React.Children.toArray(this.props.children) } }
|
子节点方法和属性小结:
如果组件有多个子节点,this.props.children 则是数组类型,
如果只有一个子节点,this.props.children 是对象类型,
如果组件没有子节点,this.props.children 为 undefined 类型。
利用这一特性可以根据子节点数据类型进行不同情况的视图渲染
7. 组件拓展
7.1 组件属性校验
在组件,组件属性(props) 是组件的调用者传递的,如果组件调用者传递的组件属性不符合要求,将导致组件内部代码出错,为了组件运行的稳定性,需要在组件内部对接收的组件属性进行校验。
问题示例:
1 2 3 4 5 6
| class App extends React.Component { render() { return <Colors colors={100} /> } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| class Colors extends React.Component { render() { return ( <ul> {this.props.colors.map((item) => ( <li key={item}>{item}</li> ))} </ul> ); } }
|
解决方案:
使用 PropTypes 进行类型检查
校验组件属性需要用到 prop-types 包,在 React15 之后此包就从React分离出来,变成了按需引入
1
| npm install prop-types@15.8.1
|
1 2 3 4 5 6 7 8 9 10 11
| import PropTypes from "prop-types";
class Colors extends React.Component { static propstypes = { colors: PropTypes.array, gender: PropTypes.oneOf(["male", "female"]).isRequired, } }
|
在webstorm编辑器上会提示:
运行之后的效果:有明确报错
7.2 组件属性默认值
如题,为了在组件没有传递值的时候保证程序正常运行而设置的值。
1 2 3 4 5 6
| class Colors extends React.Component { static defaultProps ={ colors: [], } }
|
8. 组件的生命周期
8.1 组件生命周期概述
生命周期是指事物从创建到消亡经历的整个过程。
人的生命周期:出生 -> 幼儿 -> 儿童 -> 青少年 -> 青年 -> 中年 -> 老年 -> 死亡
React组件的生命周期:创建 -> 挂载 -> 更新 -> 卸载 (中间还有错误处理,重新挂载)
React生命周期函数预览
有了生命周期函数后,开发者可以在不同的生命周期里植入不同业务代码。
特别注意⚠️:生命周期函数中的 this 指向当前类的实例对象。
常用类组件生命周期总览
说明 |
生命周期函数 |
执行时期 |
构造 |
constructor |
创建组件时执行,组件创建时最早执行的函数 |
渲染 |
render |
组件初次渲染和组件状态更新都会执行 |
是否更新 |
shouldComponentUpdate |
更新前决定是否更新的生命周期函数(返回值为boolean值) |
更新完成 |
componentDidUpdate |
组件每次更新(render)完成后执行 |
报错捕获 |
componentDidCatch |
在捕获错误的是欧 |
挂载完成 |
componentDidMount |
组件挂载完成后执行且在当前生命周期中只执行一次 |
即将卸载 |
componentWillUnmount |
组件即将卸载前最后执行的函数,且在当前生命周期只执行一次 |
8.2 生命周期函数-挂载阶段
在挂载阶段生命周期执行的生命周期函数
生命周期函数 |
执行时机 |
constructor |
创建组件时执行 |
render |
组件初始化渲染和组件状态更新都会执行 |
componentDidMount |
组件挂载完成后执行且在当前生命周期中只执行一次 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class App extends React.Component { constructor(props) { super(props); console.log("App constructor 1.初始化阶段") } render() { console.log("App render 2.渲染阶段") } componentDidMount() { console.log("App componentDidMount 3.挂载完成阶段") } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class App extends React.Component { constructor(props) { super(props); console.log("App constructor"); } render() { console.log("App render"); return ( <> App works <Child /> </> ); } componentDidMount() { console.log("App componentDidMount"); } }
class Child extends React.Component { constructor(props) { super(props); console.log("Child constructor"); } render() { console.log("Child render"); return <>Child works</>; } componentDidMount() { console.log("Child componentDidMount"); } }
|
组件挂载阶段生命周期函数中可以做的事情
生命周期函数 |
可以做的事情 |
render |
渲染组件用户界面(注意:在render 方法中前往不要调用 setState 方法,不然会进入无限循环) |
componentDidMount |
DOM操作,发送网络请求,更新组件状态 |
componentWillUnmount |
清理定时器,处理相关事件,删除不需要的变量、全局状态,清除订阅 |
8.3 生命周期函数-更新阶段
什么情况下会触发组件更新:
1.当前组件的 state 发生变化会触发组件更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class App extends React.Component { state = { count: 0, }; render() { console.log("render"); return ( <> <button onClick={() => this.setState({ count: this.state.count + 1 })}> {this.state.count} </button> </> ); } }
|
2.父组件更新会触发子组件更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class App extends React.Component { state = { count: 0, }; render() { console.log("App render"); return ( <> <button onClick={() => this.setState({ count: this.state.count + 1 })}> {this.state.count} </button> <Child /> </> ); } }
class Child extends React.Component { render() { console.log("Child render"); return <>Child works</>; } }
|
3.调用组件的 forceUpdate 方法进行强制更新(一般不会使用)
1 2 3 4 5 6 7 8 9 10
| class App extends React.Component { render() { console.log("render"); return ( <> <button onClick={() => this.forceUpdate()}>button</button> </> ); } }
|
组件更新阶段生命周期函数的 执行时机 与 执行顺序:
生命周期函数 |
执行时机 |
render |
组件初始化渲染和组件状态更新都会执行 |
componentDidUpdate |
组件界面更新完成后执行(1.每次组件更新之后都会执行 2.初始渲染时不执行) |
1 2 3 4 5 6 7 8 9 10 11 12 13
| class App extends React.Component { componentDidUpdate(prevProps, prevState, snapshot) { if(prevState.msg !== this.state.msg) { console.log("组件状态msg发生了变化") } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
class App extends React.Component { constructor(props) { super(props); this.state = { count: 0, }; this.buttonRef = createRef(); }
render() { return ( <> <button ref={this.buttonRef} onClick={() => this.setState({ count: this.state.count + 1 })} > {this.state.count} </button> </> ); } componentDidUpdate(prevProps, prevState, snapshot) { console.log(this.buttonRef.current); } }
|
8.4 生命周期函数-卸载阶段
组件卸载时执行的生命周期函数
生命周期函数 |
执行时机 |
componentWillUnmount |
即将卸载(销毁)组件前执行 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import React from "react"; import { root } from "./index";
class App extends React.Component { componentWillUnmount() { console.log("App componentWillUnmount"); } render() { return ( <> <Child /> <button onClick={() => root.unmount()}>卸载组件</button> </> ); } }
class Child extends React.Component { componentWillUnmount() { console.log("Child componentWillUnmount"); } render() { return <>Child works</>; } }
|
8.5 setState状态更新特性
在组件里调用this.setState()方法更新状态时,如果在一个函数体里多次调用this.setState({})更新状态,那么状态将会等函数执行之后,将所有setState更新的状态值进行覆盖合并之后再进行更新状态,这是React出于对性能的考虑而将setState设定成这样的。
setState()覆盖合并:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class App extends React.Component { constructor(props) { super(props); this.state = { count: 0, }; this.clickHandler = this.clickHandler.bind(this); }
clickHandler() { this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 2 }); this.setState({ count: this.state.count + 2 }); console.log(this.state.count); }
render() { return <button onClick={this.clickHandler}>{this.state.count}</button>; } }
|
在setState方法调用之后,其实 React 并没有立即更新状态,而是将传递给 setState 方法的参数保存了下,在函数体里,所有的console.log()打印的 state 里的值都是原来的值,并不是状态更新之后的值,只有在setState的第二个参数(回调函数里的state值才是最新的值——[往下看](#8.6 setState状态更新回调))。
React采用延迟更新的原因是因为如果每次调用setState就立即更新试图,那么 DOM 操作将变得非常频繁,导致性能变差,而React采用状态延迟更新策略后,由于状态只更新一次,DOM 也就只更新一次,性能层面得到了大幅度的提升。
8.6 setState状态更新回调
setState 方法的第二个参数是一个回调函数,该回调函数会在更新状态完成后执行(用户界面更新完成后)
1 2 3 4 5 6 7 8 9 10 11
| class App extends React.Component { state = { count: 0; } clickHandler() { this.setState({ count: this.state.count + 1 }, function () { console.log(this.state.count); }); } }
|
8.7 setState状态对象合并
当多次调用 setState 方法时 React 会先按照方法的调用顺序将方法接收的参数存储在一个列队中。
当调用栈当中的所有代码执行完后哦,React 会进行状态对象合并操作。
合并状态对象的过程中如果状态属性相同,后设置的状态属性值会覆盖先设置的状态属性值。
当状态合并完成后再次触发视图更新,这样就可以保证多次调用 setState 方法后只更新一次视图,提升应用的运行性能。
1 2 3 4 5 6 7 8 9 10 11
| { count: 0, msg: "Hello"}
{ count: 1 }
{ count: 2 }
{ greet: "hi" }
{ count: 2, msg: "Hello", greet: "hi" }
|
8.8 setState状态函数
setState 方法也可以接收函数作为参数,参数函数接收当前状态作为参数,要求参数函数基于当前参数状态返回最新状态。
1 2 3 4 5 6 7 8
| class App extends React.Component { clickHandler() { this.setState((prevState) => ({ count: prevState.count + 1 })); } render() { return <button onClick={this.clickHandler}>{this.state.count}</button>; } }
|
当多次调用 setState 方法时,每个参数函数接收到的都是基于上一次函数返回的最新状态,所以当状态属性相同时,状态不会发生覆盖现象。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class App extends React.Component { clickHandler() { this.setState((prevState) => ({ count: prevState.count + 1 })); this.setState((prevState) => ({ count: prevState.count + 2 })); }
render() { return <button onClick={this.clickHandler}>{this.state.count}</button>; } }
|
9. 高阶组件-逻辑复用
9.1 高阶组件概念
高阶组件简称 HOC,是 High-Order Component 的简写。
高阶组件的作用是增强普通组件的能力,实现普通组件逻辑的复用。
可以将高阶组件和普通组件理解为手机和手机壳的关系,通过将手机壳包装在手机上以增强手机的防摔能力。
9.1 高阶组件实现
第一步
将逻辑代码写在一个组件里,使用函数进行包裹,并返回当前类组件,并在render函数的返回值里调用传入的组件进行返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import React from "react"
export defualt function(Component){ return class WithMouse extends React.Component { state = { pageX: 0, pageY: 0 } mouseMoveHandler(event) { this.setState({ pageX: event.pageX, pageY: event.pageY }) } componentDidMount() { document.addEventListenner("mousemove", this.mouseMoveHandler) } componentWillUnmount() { document.removeEventListener("mousemove", this.mouseMoveHandler); } render() { return <Component pageX={this.state.pageX} pageY={this.state.pageY} /> } } }
|
第二步
在需要套用此逻辑的组件里引入以上组件进行包裹并导出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import WithMouse from "./withMouse"
class MoveBox extends React.Component { render() { return ( <div> MoveBox <p> 鼠标X轴位置:{this.props.pageX} 鼠标Y轴位置:{this.props.pageY} </p> </div> ) } }
export default WithMouse(MoveBox)
|
至此高阶组件已经封装好了
第三步
封装好了高阶组件,在需要用到的地方进行调用
1 2 3 4 5 6 7 8 9
|
import MoveBox from "./MoveBox.js"
export default class App extends React.Component { render() { return <MoveBox /> } }
|
9.1 高阶组件参数传递
通过以上使用,优化代码的复用
10. 渲染属性-逻辑复用
将要渲染的内容进行函数封装,在需要渲染的地方进行调用
10.1 渲染属性
每一个类组件的里边都有一个render方法用于渲染视图
11. Redux全局状态管理
11.1 状态管理概述
组件状态管理:[5.6 组件状态](#5.6 组件状态)
全局状态管理:已经有组件状态的概念后,全局状态管理是指视图状态不再由组件进行管理,而是由一个脱离组件的全局对象进行管理。
全局状态管理的好处是:组件可以直接与状态对象进行交互,避免了状态在不同的组件之间进行传递的复杂过程。
Redux就篇幅过长,就分到另一篇文章去总结了