React 基础
什么是 React
React 是一个用于构建用户界面的 JavaScript
用户界面:HTML页面(前端)
React 主要用来写HTML页面,或构建Web应用
如果从 MVC 的角度来看,React 仅仅是视图层(V),也就是只负责视图的渲染,而并非提供了 完整的 M 和 C 的功能。
React 起源于 Facebook 的内部项目,后又用来架设 Instagram 的网站,并于 2013 年 5 月开源
React 的特点
声明式
基于组件
学习一次,随处使用
1、声明式
你只需要描述 UI(HTML)看起来是什么样,就跟写HTML一样
React 负责渲染 UI,并在数据变化时更新 UI
const jsx = <div className="app">
<h1>Hello React! 动态变化数据:{count}</h1> </div>
2、基于组件
组件是 React 最重要的内容
- 组件表示页面中的部分内容
- 组合、复用多个组件,可以实现完整的页面功能
3、学习一次,随处使用
使用 React 可以开发 Web 应用
- 使用 React 可以开发移动端原生应用(react-native)
- 使用 React 可以开发 VR(虚拟现实)应用(react 360)
React 的基本使用
React 的安装
安装命令:
#安装两个包 React 和 React-DOM
npm i react react-dom
- react 包是核心,提供创建元素、组件等功能
- react-dom 包提供 DOM 相关功能等
React 的使用
1、引入 react 和 react-dom 两个 js 文件
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
2、创建 React 元素
<div id="root"></div>
<script>
// (标签名 便签属性 标签中包含的内容
//第三个参数以后都是子节点,也可以创建其它标签元素
const title = React.createElement('h1', null, 'Hello React')
</script>
3、渲染 React 元素到页面中
<script>
//渲染对象 (对象名 渲染到根标签
ReactDOM.render(title, document.getElementById('root'))
</script>
注意:使用页面预览需要在服务器端打开
方法说明
React.createElement() 说明(知道)
// 返回值:React元素
// 第一个参数:要创建的React元素名称
// 第二个参数:该React元素的属性
// 第三个及其以后的参数:该React元素的子节点
const el = React.createElement('h1', { title: '标题' }, 'Hello React')
注意:特殊属性名称要根据报错提示使用,例如:class写成className
ReactDOM.render() 说明
// 第一个参数:要渲染的React元素
// 第二个参数:DOM对象,用于指定渲染到页面中的位置
ReactDOM.render(el, document.getElementById('root'))
React 脚手架的使用
React 脚手架意义
1.脚手架是开发 现代Web 应用的必备。
2.充分利用 Webpack、Babel、ESLint 等工具辅助项目开发。
3.零配置,无需手动配置繁琐的工具即可使用。
4.关注业务,而不是工具配置。
使用 React 脚手架初始化项目
初始化项目,命令:
npx create-react-app my-app
启动项目,在项目根目录执行命令:
npm start
npx 命令介绍
npm v5.2.0 引入的一条命令
- 目的:提升包内提供的命令行工具的使用体验
- 原来:先安装脚手架包,再使用这个包中提供的命令
- 现在:无需安装脚手架包,就可以直接使用这个包提供的命令
补充说明
1.推荐使用:npx create-react-app my-app
2.npm init react-app my-app
3.yarn create react-app my-app
yarn 是 Facebook 发布的包管理器,可以看做是 npm 的替代品,功能与 npm 相同
- yarn 具有快速、可靠和安全的特点
- 初始化新项目:yarn init
- 安装包: yarn add 包名称
- 安装项目依赖项: yarn
- 其他命令,请参考yarn文档
在脚手架中使用 React
- 导入 react 和 react-dom 两个包。
import React from 'react'
import ReactDOM from 'react-dom'
调用 React.createElement() 方法创建 react 元素。
调用 ReactDOM.render() 方法渲染 react 元素到页面中。
JSX 的基本使用
createElement() 的问题
1.繁琐不简洁。
2.不直观,无法一眼看出所描述的结构。
3.不优雅,用户体验不爽。
JSX 简介
JSX 是 JavaScript XML 的简写,表示在 JavaScript 代码中写 XML(HTML) 格式的代码。
优势:声明式语法更加直观、与HTML结构相同,降低了学习成本、提升开发效率
JSX 是 React 的核心内容。
使用步骤
使用 JSX 语法创建 react 元素
// 使用 JSX 语法,创建 react 元素: const title = <h1>Hello JSX</h1>
使用 ReactDOM.render() 方法渲染 react 元素到页面中
// 渲染创建好的React元素 ReactDOM.render(title, root)
思考
为什么脚手架中可以使用 JSX 语法?
JSX 不是标准的 ECMAScript 语法,它是 ECMAScript 的语法扩展。
需要使用 babel 编译处理后,才能在浏览器环境中使用。
create-react-app 脚手架中已经默认有该配置,无需手动配置。
编译 JSX 语法的包为:@babel/preset-react 。
注意点
1.React元素的属性名使用驼峰命名法
2.特殊属性名:class -> className、for -> htmlFor、tabindex -> tabIndex 。
3.没有子节点的React元素可以用 /> 结束 。
4.推荐:使用小括号包裹 JSX ,从而避免 JS 中的自动插入分号陷阱。
// 使用小括号包裹
JSX const dv = (
<div>Helo JSX</div> )
嵌入 JS 表达式
数据存储在JS中
语法:**{ JavaScript表达式 }**
表达式:所有有返回值的代码都是表达式
注意:语法中是单大括号,不是双大括号!
const name = 'Jack' const dv = (
<div>你好,我叫:???</div> )
const name = 'Jack' const dv = (
<div>你好,我叫:{name}</div> )
注意点
单大括号中可以使用任意的 JavaScript 表达式
JSX 自身也是 JS 表达式
注意:JS 中的对象是一个例外,一般只会出现在 style 属性中 ,不能在{}中出现语句(比如:if/for 等)
const h1 = <h1>我是JSX</h1>
const dv = (
<div>嵌入表达式:{h1}</div> )
JSX 的条件渲染
场景:loading效果
条件渲染:根据条件渲染特定的 JSX 结构
可以使用if/else或三元运算符或逻辑与运算符来实现
条件运算符
const loadData = () => {
if (isLoading) {
return <div>数据加载中,请稍后...</div>
}
return (
<div>数据加载完成,此处显示加载后的数据</div>
)
}
三元运算符
let fn = ()=> {
return true ? <div>1</div> : <div>没有数据</div>
}
与或运算符
//用于控制是否渲染
let fn = ()=> {
return true && <div>1</div>
}
JSX 的列表渲染
如果要渲染一组数据,应该使用数组的 map()
方法
const songs = [
{id: 1, name: '痴心绝对'},
{id: 2, name: '像我这样的人'},
{id: 3, name: '南山南'}
]
const list = (
<ul>
// { } 是JavaScript表达式
{ songs.map(item => <li>{item.name}</li>) }
</ul>
)
如果要渲染一组数据,应该使用数组的 map() 方法
注意:渲染列表时应该添加 key 属性,key 属性的值要保证唯一
- 原则:map() 遍历谁,就给谁添加 key 属性
- 注意:尽量避免使用索引号作为 key
const songs = [
{id: 1, name: '痴心绝对'},
{id: 2, name: '像我这样的人'},
{id: 3, name: '南山南'}, ]
const list = (
<ul>
{songs.map(item => <li key={item.id}>{item.name}</li>)}
</ul> )
JSX 的样式处理
行内样式 —— style
<h1 style={{ color: 'red', backgroundColor: 'skyblue' }}> JSX的样式处理 </h1>
类名 —— className(推荐)
<h1 className="title"> JSX的样式处理 </h1>
总结
JSX
1.JSX 是React 的核心内容。
2.JSX 表示在JS代码中写HTML结构,是React声明式的体现。
3.使用 JSX 配合嵌入的 JS 表达式、条件渲染、列表渲染,可以描述任意 UI 结构。
4.推荐使用 className 的方式给JSX添加样式。
5.React 完全利用 JS 语言自身的能力来编写UI,而不是造轮子增强 HTML 功 能。
React组件基础
React 组件介绍
- 组件是 React 的一等公民,使用 React 就是在用组件
- 组件表示页面中的部分功能
- 组合多个组件实现完整的页面功能
- 特点:可复用、独立、可组合
React 组件的两种创建方式
- 使用函数创建组件
- 使用类创建组件
使用函数创建组件
- 函数组件:使用 JS 的函数(或箭头函数)创建的组件
- 约定1:函数名称必须以大写字母开头
- 约定2:函数组件必须有返回值,表示该组件的结构
- 如果返回值为 null,表示不渲染任何内容
function Hello() {
return (
<div>这是我的第一个函数组件!</div>
)
}
- 渲染函数组件:用函数名作为组件标签名
- 组件标签可以是单标签也可以是双标签
function Hello() {
return (
<div>这是我的第一个函数组件!</div>
)
}
ReactDOM.render(<Hello />, root)
使用JS中的函数创建的组件叫做:函数组件
- 函数组件必须有返回值
- 组件名称必须以大写字母开头, React 据此区分 组件 和 普通的
- React 元素
- 使用函数名作为组件标签名
function Hello() {
return (
<div>这是我的第一个函数组件!</div>
)
}
ReactDOM.render(<Hello />, root)
使用类创建组件
类组件:使用 ES6 的 class 创建的组件 约定1:类名称也必须以大写字母开头 约定2:类组件应该继承 React.Component 父类,从而可以使用父类中提供的方法或属性 约定3:类组件必须提供 render() 方法 约定4:render() 方法必须有返回值,表示该组件的结构
class Hello extends React.Component {
render() {
return <div>Hello Class Component!</div>
}
}
ReactDOM.render(<Hello />, root)
抽离为独立 JS 文件
思考:项目中的组件多了之后,该如何组织这些组件呢?
选择一:将所有组件放在同一个JS文件中
选择二:将每个组件放到单独的JS文件中
组件作为一个独立的个体,一般都会放到一个单独的 JS 文件中
- 创建Hello.js
- 在 Hello.js 中导入React
- 创建组件(函数 或 类)
- 在 Hello.js 中导出该组件
- 在 index.js 中导入 Hello 组件
- 渲染组件
// Hello.js
import React from 'react'
class Hello extends React.Component {
render() {
return <div>Hello Class Component!</div>
}
}
// 导出Hello组件
export default Hello
// index.js
import Hello from './Hello'
// 渲染导入的Hello组件
ReactDOM.render(<Hello />, root)
React 事件处理
- 事件绑定
- 事件对象
事件绑定
React 事件绑定语法与 DOM 事件语法相似
语法:on+事件名称={事件处理程序},比如:onClick={() => {}}
注意:React 事件采用驼峰命名法,比如:onMouseEnter、onFocus
在函数组件中绑定事件:
class App extends React.Component {
handleClick() {
console.log('单击事件触发了')
}
render() {
return (
<button onClick={this.handleClick}></button>
)
}
}
function App() {
function handleClick() {
console.log('单击事件触发了')
}
return (
<button onClick={handleClick}>点我</button>
)
}
事件对象
可以通过事件处理程序的参数获取到事件对象
React 中的事件对象叫做:合成事件(对象)
合成事件:兼容所有浏览器,无需担心跨浏览器兼容性问题
function handleClick(e) {
e.preventDefault()
console.log('事件对象', e)
}
<a onClick={handleClick}>点我,不会跳转页面</a>
带有参数如何获取事件对象
function handleClick(msg,e) {
e.preventDefault()
console.log('事件对象', e)
console.log('数据', msg)
}
<a onClick={ (e) => {
handleClick('实参',e)
}}>点我,不会跳转页面</a>
有状态组件和无状态组件
函数组件又叫做无状态组件,类组件又叫做有状态组件
状态(state)即数据
函数组件没有自己的状态,只负责数据展示(静)
类组件有自己的状态,负责更新 UI,让页面“动” 起来
比如计数器案例中,点击按钮让数值加 1 。0 和 1 就是不同时刻的状态,而由 0 变为 1 就表示状态发生了变化。状态变化后,UI 也要相应的更新。React 中想要实现该功能,就要使用有状态组件来完成。
组件中的 state 和 setState
state的基本使用
setState()修改状态
state的基本使用
状态(state)即数据,是组件内部的私有数据,只能在组件内部使用
state 的值是对象,表示一个组件中可以有多个数据
class Hello extends React.Component {
constructor() {
super()
// 初始化state
this.state = {
count: 0
}
}
render() {
return (
<div>有状态组件</div>
)
} }
class Hello extends React.Component {
// 简化语法
state= {
count: 0
}
render() {
return (
<div>有状态组件</div>
)
}
}
获取状态:this.state
class Hello extends React.Component {
// 简化语法
state= {
count: 0
}
render() {
return (
<div>有状态组件,{this.state.count}</div>
)
} }
状态即数据
状态是私有的,只能在组件内部使用
通过 this.state 来获取状态
class Hello extends React.Component {
// 简化语法
state= {
count: 0
}
render() {
return (
<div>有状态组件,{this.state.count}</div>
)
}
}
setState()修改状态
状态是可变的
语法:this.setState({ 要修改的数据 })
注意:不要直接修改 state 中的值,这是错误的!!!
setState() 作用:1. 修改 state 2. 更新UI
思想:数据驱动视图
从 JSX 中抽离事件处理程序
JSX 中掺杂过多 JS 逻辑代码,会显得非常混乱
推荐:将逻辑抽离到单独的方法中,保证 JSX 结构清晰
原因:事件处理程序中 this 的值为 undefined
希望:this 指向组件实例(render方法中的this即为组件实例)
事件绑定 this 指向
1. 箭头函数
2. Function.prototype.bind()
3. class 的实例方法
箭头函数
利用箭头函数自身不绑定this的特点
render() 方法中的 this 为组件实例,可以获取到 setState()
class Hello extends React.Component {
onIncrement() {
this.setState({ … })
}
render() {
// 箭头函数中的this指向外部环境,此处为:render()方法
return (
<button onClick={() => this.onIncrement()}></button>
)
}
}
Function.prototype.bind()
利用ES5中的bind方法,将事件处理程序中的this与组件实例绑定到一起
class Hello extends React.Component {
constructor() {
super()
this.onIncrement = this.onIncrement.bind(this)
}
// ...省略 onIncrement
render() {
return (
<button onClick={this.onIncrement}></button>
)
}
}
class 的实例方法
利用箭头函数形式的class实例方法 ,是直接将方法直接存储在实例
注意:该语法是实验性语法,但是,由于babel的存在可以直接使用
class Hello extends React.Component {
onIncrement = () => {
this.setState({ … })
}
render() {
return (
<button onClick={this.onIncrement}></button>
)
}
}
总结
1.推荐:使用class的实例方法
2.箭头函数
3.bind
//推荐使用的方法
class Hello extends React.Component {
onIncrement = () => {
this.setState({ … })
}
render() {
return (
<button onClick={this.onIncrement}></button>
)
}
}
表单处理
1. 受控组件
2. 非受控组件(DOM方式)
受控组件
HTML 中的表单元素是可输入的,也就是有自己的可变状态
而,React 中可变状态通常保存在 state 中,并且只能通过 setState() 方法来修改
HTML 中的表单元素是可输入的,也就是有自己的可变状态
而,React 中可变状态通常保存在 state 中,并且只能通过 setState() 方法来修改
React将 state 与表单元素值value绑定到一起,由 state 的值来控制表单元素的值
受控组件:其值受到 React 控制的表单元素
<input type="text" value={this.state.txt} />
步骤:
- 在 state 中添加一个状态,作为表单元素的value值(控制表单元素值的来源)
- 给表单元素绑定 change 事件,将 表单元素的值 设置为 state 的值(控制表单元素值的变化)
state = { txt: '' }
<input type="text" value={this.state.txt}
onChange={e => this.setState({ txt: e.target.value })}
/>
示例:
文本框、文本域、下拉框
复选框
示例总结:
文本框、文本域、下拉框 操作value属性
复选框 操作checked属性
多表单元素优化:
问题:每个表单元素都有一个单独的事件处理程序处理太繁琐
优化:使用一个事件处理程序同时处理多个表单元素
多表单元素优化步骤:
1.给表单元素添加name属性,名称与 state 相同
2.根据表单元素类型获取对应值
3.在 change 事件处理程序中通过 [name] 来修改对应的state
<input
type="text"
name="txt"
value={this.state.txt}
onChange={this.handleForm} />
// 根据表单元素类型获取值
const value = target.type === 'checkbox'
? target.checked
: target.value
// 根据name设置对应state this.setState({
[name]: value })
非受控组件
ref获取 DOM 或组件
说明:借助于 ref,使用原生 DOM 方式来获取表单元素值
ref 的作用:获取 DOM 或组件
使用步骤:
- 调用 React.createRef() 方法创建一个 ref 对象 ,该对象不可复用
//标准形式
class App extends React.Component{
constructor() {
super()
this.txtRef = React.createRef()
}
}
//简写形式
class App extends React.Component{
txtRef = React.createRef()
}
- 将创建好的 ref 对象添加到文本框中
<input type="text" ref={this.txtRef} />
- 通过 ref 对象获取到文本框的值
Console.log(this.txtRef.current.value)
组件通讯
组件通讯简介
组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据。在组件化过程中,我们将一个完整的功能 拆分成多个组件,以更好的完成整个应用的功能。而在这个过程中,多个组件之间不可避免的要共享某些数据 。为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯。
组件的 props
组件是封闭的,要接收外部数据应该通过 props 来实现 props的作用:接收传递给组件的数据 传递数据:给组件标签添加属性 接收数据:函数组件通过参数props接收数据,类组件通过 this.props 接收数据
<Hello name="jack" age={19} />
function Hello(props) {
console.log(props)
return (
<div>接收到数据:{props.name}</div>
)
}
// --------------------------------------------------
class Hello extends React.Component {
render() {
return (
<div>接收到的数据:{this.props.age}</div>
)
}
}
特点
- 可以给组件传递任意类型的数据
- props 是只读的对象,只能读取属性的值,无法修改对象
- 注意:使用类组件时,如果写了构造函数,应该将 props 传递给 super(),否则,无法在构造函数中获取到 props!
class Hello extends React.Component {
constructor(props) {
// 推荐将props传递给父类构造函数
super(props)
}
render() {
return <div>接收到的数据:{this.props.age}</div>
}
}
组件通讯的三种方式
组件之间的通讯分为 3 种:
- 父组件 -> 子组件
- 子组件 -> 父组件
- 兄弟组件
父组件传递数据给子组件
- 父组件提供要传递的state数据
- 给子组件标签添加属性,值为 state 中的数据
- 子组件中通过 props 接收父组件中传递的数据
class Parent extends React.Component {
state = { lastName: '王' }
render() {
return (
<div>
传递数据给子组件:<Child name={this.state.lastName} />
</div>
)
}
}
function Child(props) {
return <div>子组件接收到数据:{props.name}</div>
}
子组件传递数据给父组件
思路:利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数。
- 父组件提供一个回调函数(用于接收数据)
- 将该函数作为属性的值,传递给子组件
- 子组件通过 props 调用回调函数
- 将子组件的数据作为参数传递给回调函数
class Parent extends React.Component {
getChildMsg = (msg) => {
console.log('接收到子组件数据', msg)
}
render() {
return (
<div>
子组件:<Child getMsg={this.getChildMsg} />
</div>
)
}
}
class Child extends React.Component {
state = { childMsg: 'React' }
handleClick = () => {
this.props.getMsg(this.state.childMsg)
}
return (
<button onClick={this.handleClick}>点我,给父组件传递数据</button>
)
}
兄弟组件
将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态 思想:状态提升 公共父组件职责:1. 提供共享状态 2. 提供操作共享状态的方法 要通讯的子组件只需通过 props 接收状态或操作状态的方法
Context
思考:App 组件要传递数据给 Child 组件,该如何处理? 处理方式:使用 props 一层层组件往下传递(繁琐)
思考:App 组件要传递数据给 Child 组件,该如何处理?
更好的姿势:使用 Context
作用:跨组件传递数据(比如:主题、语言等)
使用步骤:
调用 React. createContext() 创建 Provider(提供数据) 和 Consumer(消费数据) 两个组件。
const { Provider, Consumer } = React.createContext()
使用 Provider 组件作为父节点。
<Provider> <div className="App"> <Child1 /> </div> </Provider>
设置 value 属性,表示要传递的数据。
<Provider value="pink">
调用 Consumer 组件接收数据
<Consumer> {data => <span>data参数表示接收到的数据 -- {data}</span>} </Consumer>
总结
- 如果两个组件是远方亲戚(比如,嵌套多层)可以使用Context实现组件通讯
- Context提供了两个组件:Provider 和 Consumer
- Provider组件:用来提供数据
- Consumer组件:用来消费数据
props 深入
children 属性
children 属性:表示组件标签的子节点。当组件标签有子节点时,props 就会有该属性 children 属性与普通的props一样,值可以是任意值(文本、React元素、组件,甚至是函数)
function Hello(props) {
return (
<div>
组件的子节点:{props.children}
</div>
)
}
<Hello>我是子节点</Hello>
props 校验
对于组件来说,props 是外来的,无法保证组件使用者传入什么格式的数据 如果传入的数据格式不对,可能会导致组件内部报错 关键问题:组件的使用者不知道明确的错误原因
// 小明创建的组件App
function App(props) {
const arr = props.colors
const lis = arr.map((item, index) => <li key={index}>{item.name}</li>)
return (
<ul>{lis}</ul>
)
}
// 小红使用组件App
<App colors={19} />
props 校验:允许在创建组件的时候,就指定 props 的类型、格式等 作用:捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性
App.propTypes = {
colors: PropTypes.array
}
使用步骤
- 安装包 prop-types
yarn add prop-types / npm i props-types
- 导入 prop-types 包
- 使用组件名.propTypes = {} 来给组件的props添加校验规则
- 校验规则通过 PropTypes 对象来指定
import PropTypes from 'prop-types'
function App(props) {
return (
<h1>Hi, {props.colors}</h1>
)
}
App.propTypes = {
// 约定colors属性为array类型
// 如果类型不对,则报出明确错误,便于分析错误原因
colors: PropTypes.array
}
约束规则
- 常见类型:array、bool、func、number、object、string
- React元素类型:element
- 必填项:isRequired
- 特定结构的对象:shape({ })
// 常见类型
optionalFunc: PropTypes.func,
// 必选
requiredFunc: PropTypes.func.isRequired,
// 特定结构的对象
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
props 的默认值
场景:分页组件 ->每页显示条数 作用:给 props 设置默认值,在未传入 props 时生效
function App(props) {
return (
<div>
此处展示props的默认值:{props.pageSize}
</div>
)
}
// 设置默认值
App.defaultProps = {
pageSize: 10
}
// 不传入pageSize属性
<App />
组件的生命周期
组件的生命周期概述
意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等 组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程 生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数。 钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。 只有 类组件 才有生命周期。
生命周期的三个阶段
- 每个阶段的执行时机
- 每个阶段钩子函数的执行顺序
- 每个阶段钩子函数的作用
创建时(挂载阶段)
执行时机:组件创建时(页面加载时) 执行顺序
更新时(更新阶段)
执行时机:1. setState() 2. forceUpdate() 3. 组件接收到新的props 说明:以上三者任意一种变化,组件就会重新渲染 执行顺序:
注意: forceUpdate() 是强制更新数据的方法
卸载时(卸载阶段)
执行时机:组件从页面中消失
在此期间会清楚用DOM注册的一些事件
钩子函数
创建时(挂载阶段)
- constructor() 初始化state,给事件处理程序绑定this
- render() 渲染UI,该阶段不能调用setState()
- componentDidMount()组件挂载完成,发送请求,操作DOM
更新时(更新阶段)
- render()
- componentDidUpdate() 组件更新后执行,发送请求,操作DOM,这里的setState()必须要有结束条件
卸载时(卸载阶段)
- componentWillUnmount() 组件销毁之后执行
完整版生命周期钩子函数
render-props和高阶组件
React组件复用概述
思考:如果两个组件中的部分功能相似或相同,该如何处理? 处理方式:复用相似的功能(联想函数封装) 复用什么?1. state 2. 操作state的方法 (组件状态逻辑 ) 两种方式:1. render props模式 2. 高阶组件(HOC) 注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)
render props 模式
思路分析
思路:将要复用的state和操作state的方法封装到一个组件中 问题1:如何拿到该组件中复用的state? 在使用组件时,添加一个值为函数的prop,通过 函数参数 来获取(需要组件内部实现)
问题2:如何渲染任意的UI? 使用该函数的返回值作为要渲染的UI内容(需要组件内部实现)
<Mouse />
<Mouse render={(mouse) => {}}/>
<Mouse render={(mouse) => (
<p>鼠标当前位置 {mouse.x},{mouse.y}</p>
)}/>
使用步骤
- 创建Mouse组件,在组件中提供复用的状态逻辑代码(1. 状态 2. 操作状态的方法)
- 将要复用的状态作为 props.render(state) 方法的参数,暴露到组件外部
- 使用 props.render() 的返回值作为要渲染的内容
class Mouse extends React.Component {
// … 省略state和操作state的方法
render() {
return this.props.render(this.state)
}
}
<Mouse render={(mouse) => <p>鼠标当前位置 {mouse.x},{mouse.y}</p>}/>
演示Mouse组件的复用
Mouse组件负责:封装复用的状态逻辑代码(1. 状态 2. 操作状态的方法) 状态:鼠标坐标(x, y) 操作状态的方法:鼠标移动事件 传入的render prop负责:使用复用的状态来渲染UI结构
class Mouse extends React.Component {
// … 省略state和操作state的方法
render() {
return this.props.render(this.state)
}
}
<Mouse render={(mouse) => <p>鼠标当前位置 {mouse.x},{mouse.y}</p>}/>
children代替render属性
注意:并不是该模式叫 render props 就必须使用名为render的prop,实际上可以使用任意名称的prop 把prop是一个函数并且告诉组件要渲染什么内容的技术叫做:render props模式 推荐:使用 children 代替 render 属性
<Mouse>
{({x, y}) => <p>鼠标的位置是 {x},{y}</p> }
</Mouse>
// 组件内部:
this.props.children(this.state)
// Context 中的用法:
<Consumer>
{data => <span>data参数表示接收到的数据 -- {data}</span>}
</Consumer>
代码优化
- 推荐:给 render props 模式添加 props校验
- 应该在组件卸载时解除 mousemove 事件绑定
Mouse.propTypes = {
chidlren: PropTypes.func.isRequired
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
高阶组件
概述
目的:实现状态逻辑复用 采用 包装(装饰)模式 ,比如说:手机壳 手机:获取保护功能 手机壳 :提供保护功能 高阶组件就相当于手机壳,通过包装组件,增强组件功能
思路分析
高阶组件(HOC,Higher-Order Component)是一个函数,接收要包装的组件,返回增强后的组件 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给 被包装组件 WrappedComponen
const EnhancedComponent = withHOC(WrappedComponent)
// 高阶组件内部创建的类组件:
class Mouse extends React.Component {
render() {
return <WrappedComponent {...this.state} />
}
}
使用步骤
创建一个函数,名称约定以 with 开头
function withMouse() {}
指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
function withMouse(WrappedComponent) {}
在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
function withMouse(WrappedComponent) {
class Mouse extends React.Component {}
return Mouse
}
// Mouse组件的render方法中:
return <WrappedComponent {...this.state} />
调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
// 创建组件,Position为需要包装的组件 const MousePosition = withMouse(Position) // 渲染组件,渲染包装之后的组件,在没有包装前的组件中里面是有this.props.X接收数据 <MousePosition />
设置displayName
使用高阶组件存在的问题:得到的两个组件名称相同 原因:默认情况下,React使用组件名称作为 displayName 解决方式:为 高阶组件 设置 displayName 便于调试时区分不同的组件 displayName的作用:用于设置调试信息(React Developer Tools信息) 设置方式:
// 在高阶组件包装函数返回值 return Mouse 前,编写下如下代码
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
传递props
问题:props丢失 原因:高阶组件没有往下传递props 解决方式:在包装组件里需要再传递一次,渲染 WrappedComponent 时,将 state 和 this.props 一起传递给组件 传递方式:
<WrappedComponent {...this.state} {...this.props} />
React 原理揭秘
setState() 的说明
更新数据
setState() 是异步更新数据的 注意:使用该语法时,后面的 setState() 不要依赖于前面的 setState() 可以多次调用 setState() ,只会触发一次重新渲染
this.state = { count: 1 }
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 1
推荐语法
推荐:需要依据上一次执行的操作来实现,使用 setState((state, props) => {}) 语法 参数state:表示最新的state 参数props:表示最新的props
this.setState((state, props) => {
return {
count: state.count + 1
}
})
console.log(this.state.count) // 1
第二个参数
场景:在状态更新(页面完成重新渲染)后立即执行某个操作 语法: setState(updater[, callback])
this.setState(
(state, props) => {},
() => {console.log('这个回调函数会在状态更新后立即执行')}
)
this.setState(
(state, props) => {},
() => {
document.title = '更新state后的标题:' + this.state.count
}
)
JSX 语法的转化过程
JSX 仅仅是 createElement() 方法的语法糖(简化语法) JSX 语法被 @babel/preset-react 插件编译为 createElement() 方法 React 元素:是一个对象,用来描述你希望在屏幕上看到的内容
组件更新机制
setState() 的两个作用: 1. 修改 state 2. 更新组件(UI) 过程:父组件重新渲染时,也会重新渲染子组件。但只会渲染当前组件子树(当前组件及其所有子组件)
组件性能优化
减轻 state
减轻 state:只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading 等) 注意:不用做渲染的数据不要放在 state 中,比如定时器 id等 对于这种需要在多个方法中用到的数据,应该放在 this 中
class Hello extends Component {
componentDidMount() {
// timerId存储到this中,而不是state中
this.timerId = setInterval(() => {}, 2000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
render() { … }
}
避免不必要的重新渲染
组件更新机制:父组件更新会引起子组件也被更新,这种思路很清晰 问题:子组件没有任何变化时也会重新渲染 如何避免不必要的重新渲染呢? 解决方式:使用钩子函数 shouldComponentUpdate(nextProps, nextState) 作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染 触发时机:更新阶段的钩子函数,组件重新渲染前执行 (shouldComponentUpdate -> render)
class Hello extends Component {
shouldComponentUpdate(nextProps, nextState) {
// 根据条件,决定是否重新渲染组件
return false
}
render() {…}
}
纯组件
纯组件:PureComponent 与 React.Component 功能相似 区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较 原理:纯组件内部通过分别 对比 前后两次 props 和 state 的值,来决定是否重新渲染组件
class Hello extends React.PureComponent {
render() {
return (
<div>纯组件</div>
)
}
}
纯组件的浅拷贝问题
简单数据类型
说明:纯组件内部的对比是 shallow compare(浅层对比) 对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)
let number = 0
let newNumber = number
newNumber = 2
console.log(number === newNumber) // false
state = { number: 0 }
setState({
number: Math.floor(Math.random() * 3)
})
// PureComponent内部对比:
最新的state.number === 上一次的state.number // false,重新渲染组件
复杂数据类型
说明:纯组件内部的对比是 shallow compare(浅层对比) 对于引用类型来说:只比较对象的引用(地址)是否相同 注意:state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!(示例)
// 正确!创建新数据
const newObj = {...state.obj, number: 2}
setState({ obj: newObj })
// 正确!创建新数据
// 不要用数组的push / unshift 等直接修改当前数组的的方法
// 而应该用 concat 或 slice 等这些返回新数组的方法
this.setState({
list: [...this.state.list, {新数据}]
})
虚拟 DOM 和 Diff 算法
React 更新视图的思想是:只要 state 变化就重新渲染视图 特点:思路非常清晰 问题:组件中只有一个 DOM 元素需要更新时,也得把整个组件的内容重新渲染到页面中? 理想状态:部分更新,只更新变化的地方。 问题:React 是如何做到部分更新的? 不是 虚拟 DOM 配合 Diff 算法
虚拟 DOM:本质上就是一个 JS 对象,用来描述你希望在屏幕上看到的内容(UI)。
执行过程
- 初次渲染时,React 会根据初始state(Model),创建一个虚拟 DOM 对象(树)。
- 根据虚拟 DOM 生成真正的 DOM,渲染到页面中。
- 当数据变化后(setState()),重新根据新的数据,创建新的虚拟DOM对象(树)。
- 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容。
- 最终,React 只将变化的内容更新(patch)到 DOM 中,重新渲染到页面。
代码演示
组件 render() 调用后,根据 状态 和 JSX结构 生成虚拟DOM对象 示例中,只更新 p 元素的文本节点内容
{
type: 'div',
props: {
children: [
{ type: 'h1', props: {children: '随机数'} },
{ type: 'p', props: {children: 0} }
]
}
}
// ...省略其他结构
{ type: 'p', props: {children: 2} }
React 路由基础
React路由介绍
现代的前端应用大多都是 SPA(单页应用程序),也就是只有一个 HTML 页面的应用程序。因为它的用户体 验更好、对服务器的压力更小,所以更受欢迎。为了有效的使用单个页面来管理原来多页面的功能,前端路由 应运而生。
- 前端路由的功能:让用户从一个视图(页面)导航到另一个视图(页面)
- 前端路由是一套映射规则,在React中,是 URL路径 与 组件 的对应关系
- 使用React路由简单来说,就是配置 路径和组件(配对)
路由的基本使用
使用步骤
安装:
yarn add react-router-dom
导入路由的三个核心组件:Router / Route / Link
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
使用 Router 组件包裹整个应用(重要)
<Router> <div className="App"> // … 省略页面内容 </div> </Router>
使用 Link 组件作为导航菜单(路由入口)
<Link to="/first">页面一</Link>
使用 Route 组件配置路由规则和要展示的组件(路由出口)
const First = () => <p>页面一的页面内容</p> <Router> <div className="App"> <Link to="/first">页面一</Link> <Route path="/first" component={First}></Route> </div> </Router>
常用组件说明
- Router 组件:包裹整个应用,一个 React 应用只需要使用一次
- 两种常用 Router:HashRouter 和 BrowserRouter
- HashRouter:使用 URL 的哈希值实现(localhost:3000/#/first)
- (推荐)BrowserRouter:使用 H5 的 history API 实现(localhost:3000/first)
Link 组件:用于指定导航链接(a 标签)
// to属性:浏览器地址栏中的pathname(location.pathname)
<Link to="/first">页面一</Link>
Route 组件:指定路由展示组件相关信息
// path属性:路由规则
// component属性:展示的组件
// Route组件写在哪,渲染出来的组件就展示在哪
<Route path="/first" component={First}></Route>
路由的执行过程
- 点击 Link 组件(a标签),修改了浏览器地址栏中的 url 。
- React 路由监听到地址栏 url 的变化。
- React 路由内部遍历所有 Route 组件,使用路由规则( path )与 pathname 进行匹配。
- 当路由规则(path)能够匹配地址栏中的 pathname 时,就展示该 Route 组件的内容。
编程式导航
场景:点击登录按钮,登录成功后,通过代码跳转到后台首页,如何实现?
编程式导航:通过 JS 代码来实现页面跳转
history 是 React 路由提供的,用于获取浏览器历史记录的相关信息
push(path):跳转到某个页面,参数 path 表示要跳转的路径
go(n): 前进或后退到某个页面,参数 n 表示前进或后退页面数量(比如:-1 表示后退到上一页)
class Login extends Component {
handleLogin = () => {
// ...
this.props.history.push('/home')
}
render() {...省略其他代码}
}
默认路由
问题:现在的路由都是点击导航菜单后展示的,如何在进入页面的时候就展示呢?
默认路由:表示进入页面时就会匹配的路由
默认路由path为:/
<Route path="/" component={Home} />
匹配模式
模糊匹配模式
问题:当 Link组件的 to 属性值为 “/login”时,为什么 默认路由 也被匹配成功?
默认情况下,React 路由是模糊匹配模式
模糊匹配规则:只要 pathname 以 path 开头就会匹配成功
<Link to="/login">登录页面</Link>
<Route path="/" component={Home} /> 匹配成功
// path 代表Route组件的path属性
// pathname 代表Link组件的to属性(也就是 location.pathname)
path | 能够匹配的pathname |
---|---|
/ | 所有 pathname |
/first | /first 或 /first/a 或 /first/a/b/… |
精确匹配
问题:默认路由任何情况下都会展示,如何避免这种问题?
给 Route 组件添加 exact 属性,让其变为精确匹配模式
精确匹配:只有当 path 和 pathname 完全匹配时才会展示该路由
// 此时,该组件只能匹配 pathname=“/” 这一种情况
<Route exact path="/" component=... />
推荐:给默认路由添加 exact 属性。