React知识总结✨
🚀标志为在 vscode 书写的快捷提示
❗ 标志为易错点和注意点
✨✅表示重点
React基础
React概述
React 是一个用于构建用户界面的 JavaScript 库。React 主要用来写HTML页面,或构建Web应用
React 的特点
-
1 声明式
- 程序员只需写标签,React 负责渲染 UI,并在数据变化时更新 UI
-
2 基于组件
-
3 学习一次,随处使用
React 的基本使用
- 安装
npm i react react-dom
- 使用
- 1 引入 react 和 react-dom 两个 js 文件
- 2 创建 React(虚拟DOM) 元素
React.createElement('h1',null,'Hello React'(文本节点),React.createElement('span',null,'Hello React')(元素节点)
- 返回值:React元素
- 第一个参数:要创建的React
元素名称
- 第二个参数:该React元素的
属性
eg:{ className:'box' }
- 第三个及其以后的参数:该React元素的
子节点
- 3 渲染 React 元素到页面中
ReactDOM.render(el, document.getElementById('root'))
- 第一个参数:要渲染的
React元素
- 第二个参数:
DOM对象
,用于指定渲染到页面中的位置(挂载点
)
- 第一个参数:要渲染的
React 脚手架的使用
脚手架是开发 现代Web 应用的必备。
使用 React 脚手架初始化项目
-
初始化项目,命令:npx create-react-app my-app
-
启动项目,在
项目根目录
执行命令:npm start
npx
命令
目的:提升包内提供的命令行工具的使用体验
现在:无需
安装脚手架包,就可以直接
使用这个包提供的命令
在脚手架中使用 React
1 导入 react 和 react-dom 两个包。
import xx from 'react'
import xx from 'react-dom'
- ✨ React18 写法,多了
'react-dom/client'
- ✨ React18 写法,多了
2 调用 React.createElement()
方法创建 react 元素。❌
3 调用 ReactDOM.render()
方法渲染 react 元素到页面中。
- ✨React18写法:
ReactDOM.createRoot(document.getElementById('root')).render()
- 渲染函数式组件,注意render 里面写成函数 如:
render(App())
- 渲染函数式组件,注意render 里面写成函数 如:
JSX
JSX 是 JavaScript XML 的简写,表示在 JavaScript 代码中写 XML(HTML) 格式的代码。
优势:声明式语法更加直观、与HTML结构相同,降低了学习成本、提升开发效率
✨语法糖
=> 通过 babel 插件 => 转化成 JS 能识别的语法 React.createElement()
JSX 是 React 的核心内容。
使用步骤:
1 ⭐️推荐使用 JSX
语法创建 react(虚拟DOM) 元素
const title = <h1>Hello JSX</h1>
2 使用 ReactDOM.render() 方法渲染 react 元素到页面中
ReactDOM.render(title, root)
为什么脚手架
中可以使用 JSX 语法?
-
JSX 不是标准的 ECMAScript 语法,它是 ECMAScript 的语法扩展。
-
reate-react-app 脚手架中已经默认有
babel
,无需手动配置。
JSX中对象无法直接显示,需转换成字符串
<h2> { JSON.stringfy(obj) } </h2>
String{false}
JSX 中使用 JavaScript 表达式
嵌入 JS 表达式
-
数据存储在JS中
-
语法:
{ JavaScript表达式 }
-
注意:语法中是单大括号,不是双大括号!
注意点
- 单大括号中可以使用任意的
JavaScript 表达式
* + - / 三元表达式… 不
能写js 对象
,一般只出现在style
属性里- JSX 自身也是 JS 表达式
{JSX}
不
能在{}中出现语句(比如:if/for
等)
JavaScript表达式(有值)
a
a+b
demo(1)
arr.map()
function test () {}
语句(代码)
if(){}
for(){}
switch(){case:xxxx}
✨JSX 插值表达式总结
- 基本数据类型
- string 、number 正常显示
- ❗null 、undefined、boolean 不显示
- 引用数据类型
- 对象
- 对象不能直接放在插值表达式中
- 数组
- 数组内的每项元素会当做一个个 dom 节点渲染出来
- 函数
- 函数本身也是对象,不能直接放在插值表达中,通常是要调用函数
- 函数的返回值遵循上述规则
- 对象
- 其他
- 三元、逻辑、🔔JSX本身:通常和其它的数据类型配合使用
JSX 的条件渲染
if/else
三元表达式
逻辑与运算符
JSX 的列表渲染
如果要渲染一组数组,应该使用数组的 map() 方法
注意:
- 渲染列表时应该添加 key 属性,key 属性的值要保证唯一
- 原则:map()
遍历
谁,就给谁添加 key 属性 - 尽量避免使用索引号作为 key
{songs.map(item => <li key={item.id}>{item.name}</li>)}
❗ 属性值写法:属性名={ 值 }
,与 Vue 不一样
JSX 的样式处理
行内样式 —— style
12px
可省略成 12
<h1 style={{ color: 'red', backgroundColor: 'skyblue',fontSize:30 }}></h1>
⭐️类名 —— className
<h1 className="title">JSX的样式处理</h1>
总结:
React 完全利用 JS 语言自身的能力来编写UI,而不是造轮子增强 HTML 功 能
❗️ 注意点
1、 属性名使用 驼峰命名法
2、 特殊属性名:class->className
for->htmlFor
tabindex->tabIndex
3、 没有子节点的React 元素可以使用 />
,必须有结束标签
- eg:
<span/>
4、 推荐使用 ()
包裹 JSX 避免 JS 中的自动插入分号陷阱。
const dv = <div> hello </div>
5、 标签中混入JS表达式时要用{}
6、 只有一个根标签
,必须要有一个根节点:<></>
<React.Fragment></React.Fragment>
7、 内联样式: 要用style={{ key:value }}
的形式去写
8、 标签首字母
(1).若小
写字母开头,则将该标签转为html中同名元素
,若html中无该标签对应的同名元素,则报错。
(2).若大
写字母开头,react就去渲染对应的组件
,若组件没有定义,则报错。
9、❗布尔类型、null、undefined 在 JSX 中不会显示在页面中(Vue 模板会转化成字符串再显示) String(true)
组件基础
使用React 就是使用组件,组件表示页面中的部分功能,组合多个组件实现完整的页面功能
特点:可复用、独立、可组合
React 组件的两种创建方式
1 使用函数创建组件
使用 JS 的函数(或箭头函数)创建的组件
- 函数名称必须以
大写字母
开头 - 函数组件必须有返回值,表示该
组件的结构
- 如果返回值为 null,表示不渲染任何内容
- this 是undefined,因为babel编译后开启了严格模式
- 没有 state 和 refs 属性
- 可以使用 props
- 函数传参的方式使用
渲染组件:
- 用函数名作为组件标签名
- 组件标签可以是单标签也可以是双标签
function Hello() {
return (
<div>这是我的第一个函数组件!</div>
)
}
ReactDOM.render(<Hello />, root)
执行了ReactDOM.render(…之后,发生了什么?
1.React解析组件标签,找到了MyComponent组件。
2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
2 使用类创建组件
使用 ES6 的 class 创建的组件
-
类名称也必须以
大
写字母开头 -
类组件应该
继承 React.Component 父类
,从而可以使用父类中提供的方法或属性 -
类组件必须提供
render()
方法 -
render() 方法必须有
返回值
,表示该组件的结构
class Hello extends React.Component {
render() {
return <div>Hello Class Component!</div>
}
}
ReactDOM.render(<Hello />, root)
//render是放在哪里的?—— MyComponent的原型对象上,供实例使用。
//render中的this是谁?—— MyComponent的实例对象 <=> MyComponent组件实例对象。
执行了ReactDOM.render(…之后,发生了什么?
1.React解析组件标签,找到了MyComponent组件。
2.发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用到原型上的render方法。
3.将render返回的 虚拟DOM 转为真实DOM,随后呈现在页面中。
3 抽离为独立 JS 文件
组件作为一个独立的个体,一般都会放到一个单独的 JS 文件中
步骤:
创建
-
创建MyFooter.js
-
在 MyFooter.js 中导入React
import React from 'react'
-
创建组件(函数 或 类)
// 类 src/componnets/MyFooter.js export default class MyFooter extends React.Component { render() { return <div>Hello Class Component!</div> } } // 函数 src/componnets/MyHeader.js export default () => { return ( <header> <h1>页面头部文件</h1> </header> ) }
-
在 MyFooter.js 中导出该组件
export default MyFooter
使用:
-
在 index.js 中导入 MyFooter组件
import MyFooterfrom './MyFooter'
-
渲染组件
ReactDOM.render(<Hello />, root)
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import MyFooter from './components/MyFooter'
import MyHeader from './components/MyHeader'
class App extends React.Component {
render() {
return (
<>
<MyHeader />
<h1>Hello React</h1>
<MyFooter />
</>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
React 事件处理
事件绑定
语法:
on+事件名称={事件处理程序}
,比如:onClick={() => {}}/{handelClick}
❗注意不要写括号调用
React 事件采用驼峰命名法
事件对象
可以通过事件处理程序的参数获取到事件对象 event
React 中的事件对象叫做:合成事件(对象)
(1).通过onXxx属性指定事件处理函数(注意大小写)
a.React使用的是自定义(合成)事件, 而不是使用的原生DOM事件 —————— `为了更好的兼容性`
b.React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) ————————`为了的高效`
(2).通过event.target得到发生事件的DOM元素对象 ——————————`避免过度使用 ref`
有状态组件和无状态组件
函数组件又叫做无状态(数据)组件,类组件又叫做有状态组件
函数组件
- 没有自己的状态,只负责数据展示(静)
类组件
- 有自己的状态,负责更新 UI,让页面“动” 起来
组件(类组件)中的 state
和 setState
state的基本使用
状态(state)即数据,是组件内部的私有数据,只能在组件内部使用
state 的值是对象
,表示一个组件中可以有多个数据
- 获取状态:this.state
- ❗模板中使用
{ this.state }
只有一个大括号
语法:
class Hello extends React.Component {
// 简化语法
state= {
count: 0
}
render() {
return (
<button onClick="this.setState({count:this.state.count + 1
})"></button>
// 错误 this.state.count += 1
)
}
}
🔔 React 重要思想:单向数据流,this.state 只负责访问,更新需要用 this.setState()
不要直接修改 state 中的值,这是错误的!!!
setState()修改状态
- 状态是可变的
- 语法:
this.setState({ 要修改的数据 })
- ❌注意:
不
要直接修改state
中的值,这是错误的!!! - setState() 作用: 1. 修改 state 2. 更新UI (调用了 render 方法)
- 思想:
数据驱动视图
可以修改 state,后面使用 setState({})
方法可以使render方法重新执行,进而更新数据
❌常见错误:通过赋值的方式修改 state,render 方法不会运行,视图不会更新(数据和视图不同步)
从 JSX 中抽离事件处理程序
原因:事件处理程序中 this 的值为 undefined
- 由于changeWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用。类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined
希望:this 指向组件实例
(render方法中的 this 即为组件实例)
自定义方法————要用赋值语句的形式+箭头函数
事件绑定 this 指向
React 事件处理 语法:
on事件类型={ 事件处理函数 }
❗ 花括号内绑定的是事件处理函数,而不是函数的调用
eg:onClick={this.handleClick}
1 箭头函数
利用箭头函数自身不绑定this的特点
render() 方法中的 this 为组件实例,可以获取到 setState()
事件传参 语法:on事件类型={ () => this.handleXxx(参数) }
<button onClick={() => this.onIncrement(1)}></button>
2 Function.prototype.bind()
利用ES5中的bind方法,将事件处理程序中的this与组件实例绑定到一起
class Hello extends React.Component {
constructor() {
super()
this.onIncrement = this.onIncrement.bind(this)
this.state = { count:0 }
}
// ...省略 onIncrement
render() {
return (
<button onClick={this.onIncrement}></button>
)
}
}
3 ⭐️class 的实例方法
利用箭头函数形式的class实例方法
注意:该语法是实验性语法,但是,由于 babel 的存在可以直接使用
onIncrement = () => {
this.setState({ … })
}
onIncrement 放在 实例对象上
由于 onIncrement 是作为onClick的回调,所以不是通过实例调用的,是直接调用
//类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined
补充注意点
- React 组件自带的结构体,this 指向底层做过处理,如 render 结构指向为组件实例
- ❗ 如果是自定义的函数,需要写成
箭头函数
写法,否则 this 指向为 undefined
表单处理 & ref
受控组件
- React 将
state
与表单元素值value
(相当于 v-model 双向绑定)绑定到一起,由 state 的值来控制表单元素的值 - React 对表单元素的 onChange 事件进行封装,统一使用 onChange 获取表单值即可
步骤:
1 在 state 中添加一个状态,作为表单元素的value值(控制表单元素值的来源)
state = { txt: '' }
<input type="text" value={this.state.txt}/>
2 给表单元素绑定 change
事件,将 表单元素的值 设置为 state 的值(控制表单元素值的变化)
<input type="text" value={this.state.txt} onChange={e => this.setState({ txt: e.target.value })} />
❗示例总结:
1 文本框、富文本框、下拉框 操作value
属性
2 复选框 操作checked
属性
handleChecked = e =>{
this.setState({ isChecked: e.target.value })
}
state = { isChecked:'' }
<input type:'checkbox' checked={ this.state.isChecked } onChange={ this.handleChecked }/>
✨✨多表单元素优化
1 给表单元素添加name属性,名称与 state 相同
2 根据表单元素类型获取对应值
3 在 change 事件处理程序中通过对象名称的插值符号 []
来修改对应的state
<input type="text" name="txt" value={this.state.txt} onChange={this.handleForm}/>
handelFrom = e =>{
// 🎈利用解构语法简化代码
const { name,type,checked,value } = e.target
this.setDate({[name]:type === 'checkbox' ? checked : value})
}
非受控组件(DOM方式)❌
借助于 ref,使用原生 DOM 方式来获取表单元素值
场景:
- 获取
DOM
元素,和document.querySelector()
获取无差别 - 获取组件,ref 价值所在✨
使用步骤
1 调用 React.createRef() 方法创建一个 ref 对象
constructor() {
super()
this.txtRef = React.createRef()
}
2 将创建好的 ref 对象添加到文本框中
<input type="text" ref={this.txtRef} />
3 通过 ref 对象获取到文本框的值
Console.log(this.txtRef.current.value)
组件进阶
组件通讯介绍
打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯。
组件的 props
作用:接收传递给组件的数据
1 传递数据:给组件标签添加属性
<Hello name="jack" age={19} />
- 字符串
" "
- 变量
{}
- 字符串
2 接收数据:函数
组件通过参数props
接收数据,类
组件通过 this.props
接收数据
class式:
class Hello extends React.Component {
constructor(props) {
// 推荐将props传递给父类构造函数
super(props)
}
render() {
//
return (
<div>接收到的数据:{ this.props.age }</div>
) // props 是一个对象
}
}
函数式:
const Hello = props => {
return (
<div>接收到数据:{ props.name }</div>
)
}
const person = {name:'老刘',age:18,sex:'女'}
ReactDOM.render(<Person {...p}/>,document.getElementById('test3'))
特点:
- 可以给组件传递任意类型的数据
- props 是
只读
的对象,只能读取属性的值,无法修改对象 - 注意:使用
类
组件时,如果写了构造函数
,应该将 props 传递给super()
,否则,无法在构造函数
中获取到 props!- 推荐将props传递给父类构造函数
- 构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
组件通讯的三种方式
1 父组件 -> 子组件
-
父组件提供要传递的state数据
state = { lastName: '王' }
-
给子组件标签添加属性,值为 state 中的数据
<Child name={ this.state.lastName } />
-
子组件中通过
props
接收父组件中传递的数据- 函数组件
function Child(props) { return <div>子组件接收到数据:{ props.name }</div> }
- ✨函数组件的参数就是 props,可直接
解构
出父组件传过来的数据
- 类组件
{ this.props.name }
{ ()=>this.props.HandleTilte(this.state.title) }
- 函数组件
2 子组件 -> 父组件
利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数。
-
父组件提供一个回调函数(用于接收数据)
getChildMsg = data=> {this.setState({parentMsg:data})}
-
将该函数作为属性的值,传递给子组件
<Child getMsg={ this.getChildMsg } />
-
子组件通过 props 调用回调函数
handleClick=()=>{ this.props.getMsg(this.state.childMsg) }
-
将子组件的数据作为参数传递给回调函数
3 兄弟组件
将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
公共父组件职责:1. 提供共享状态 2. 提供操作共享状态的方法
要通讯的子组件只需通过 props 接收状态或操作状态的方法
❗注意点:
- 可以给组件传递任意类型的数据
- 包括虚拟DOM结构、函数(用来子向父传递数据)(比 Vue 更灵活)
- props 是只读的,不允许修改 props 的数据( Vue 的 props 也是只读的)
Context
App => Child 组件
- 使用 Context,,实现跨组件传递数据(比如:主题、语言等)
使用步骤:
1 调用 React. createContext()
创建 Provider
(提供数据) 和 Consumer
(消费数据) 两个组件。
const { Provider, Consumer } = React.createContext()
2 使用 Provider 组件作为父节点
<Provider> <div className="App"><Child1 /></div> </Provider>
3 设置 value 属性,表示要传递的数据。
<Provider value="pink">
4 子组件调用 Consumer 组件接收数据。
<Consumer> { data => <span> data参数表示接收到的数据 -- {data}</span>}</Consumer>
5 孩子的孩子接收数据:
<Consumer> { data => <span> data参数表示接收到的数据 -- {data}</span>}</Consumer>
注:如果传过来的数据是对象,用解构,或者JSON.stringfy(date)
<Consumer>
{({ a, b }) => {
return (
<div>
{a}---{b}
</div>
)
}}
</Consumer>
<Consumer>
{value => {
return <div>{JSON.stringify(value)}</div>
}}
</Consumer>
❗✨注意事项:
-
Consumer 内部嵌套的函数会自动执行,要注意函数的返回值需符合 模板数据展示语法要求
-
分拆的单文件组件如何处理?
- 解决方案:确保 Provider 和 Consumer 是由同一个 Context 对象解构出来的
// src/store.js import React from 'react' // 创建一个上下文对象,Provider 负责提供数据 ,Consumer 负责消费数据 export const { Provider, Consumer } = React.createContext() // src/Test.js import { Consumer } from './store' // ...书写子组件的业务代码 // src/index.js import { Provider } from './store' import Son from './Test' // 书写父组件的业务代码
props 深入
children 属性—组件的插槽
表示
组件标签
的子节点。当组件标签有子节点时,props 就会有该属性
值可以是任意值(文本、React元素、组件,甚至是函数)
React 插槽和 Vue 插槽写法区别:
Vue插槽 : <slot></slot>
React插槽: { this.props.children }
const Hello= props => {
return (
<div>
组件的子节点:{props.children}
</div>
)
}
<Hello>我是子节点</Hello>
这里的“我是子节点”是 props.children
在 子组件
里使用:
{/* 渲染子节点 文本、组件、JSX节点 */}
{this.props.children}
{/* {this.props.children()} 子节点为函数时 */}
props 校验
允许在创建组件的时候,就指定 props 的类型、格式等,捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性
使用步骤
-
导入 prop-types 包 (无需下载,React自带的) 🚀impt
-
使用
组件名.propTypes = {colors: PropTypes.array}
来给组件的props添加校验规则 (写在组件定义之后) -
也可用以下方法定义在组件内:(类组件✨)
static propTypes = { title:PropTypes.string }
-
校验规则通过 PropTypes 对象来指定
约束规则
-
常见类型:
array、bool、func、number、object、string
-
React元素类型:
element
-
必填项:
isRequired
-
✨特定结构的对象:
shape({ })
// 常见类型
potional: PropTypes.number,
// 必选
requiredFunc: PropTypes.func.isRequired,
// 特定结构的对象
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
props 的默认值
场景:分页组件 -> 每页显示条数
作用:给 props 设置默认值,在未传入 props 时生效
App.defaultProps = { pageSize: 10 }
类组件✨
也可在组件内写:static defaultProps = { pageSize:10 }
传:<App pageSize={20}/>
函数组件✨
function List({ pagesize=10 }){return (...)}
知识补充:
类的静态成员:
class Person {
// 类的实例属性
name = 'zs'
// 等价于 Person.age = 18 ,是添加给了 Person 类的成员,而非 new 出来的实例
static age = 18
// 原型实例方法
sayHi() {
console.log('哈哈')
}
static goodBye() {
console.log('byebye')
}
}
组件的生命周期
组件的生命周期概述
意义:
- 组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等
组件的生命周期:
- 组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程,生命周期的每个阶段总是伴随着一些
方法调用
,这些方法就是生命周期的钩子函数
。
钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。
只有 类组件 才有生命周期。
生命周期的三个阶段
1 创建时
执行时机 :组件创建时(页面加载时)
钩子函数的执行顺序: constructor
-> render
-> componentDidMount
钩子 函数 | 触发时机 | 作用 |
---|---|---|
constructor | 创建组件时,最先执行,初始化的时候只执行一次 | 1. 初始化state 2. 创建 Ref 3. 使用 bind 解决 this 指向问题等 |
render | 每次组件渲染都会触发 | 渲染UI(注意: 不能在里面调用setState() ) |
componentDidMount | 组件挂载(完成DOM渲染)后执行,初始化的时候执行一次 | 1. 发送网络请求 2.DOM操作 |
✨开发经验:
-
construcor()
平常很少用,一般用简写的方式初始化 state ,即:state = { msg:'Hello' }
-
componentDidMount
🚀cdm 可快速生成结构-
组件挂载时可获取本地存储,如:
componentDidMount(){ this.setState({list:JSON.parse(localStorage.getItem('todos'))|| []}) }
-
2 更新时
执行时机 : 1 setState()
2 forceUpdate()
3 组件接收到新的 props
钩子函数的执行顺序 : render
-> componentDidUpdate
🚀cdu
钩子函数 | 触发时机 | 作用 |
---|---|---|
render | 每次组件渲染都会触发 | 渲染UI(与 挂载阶段 是同一个render) |
componentDidUpdate | 组件更新后(DOM渲染完毕) | DOM操作,可以获取到更新后的DOM内容,不要直接调用setState |
❗ render
生命周期注意点:
- setState 会导致递归更新!所以不能在 render 直接调用
- 不要在 render 内写定时器,会累加
✨React 可通过 componentDidUpdate
钩子实现类似 Vue 中的 watch
功能
componentDidUpdate(prevProps,prevState){
// count 新值和旧值不一样,说明 count 变化了(Vue的watch)
if(this.state.count !== prevState.count){
console.log('count变化了,实现Vue的watch')
}
}
也可在componentDidUpdate
中设置本地存储
3 卸载时
执行时机 : 组件从页面中消失 🚀cwu
钩子函数 | 触发时机 | 作用 |
---|---|---|
componentWillUnmount | 组件卸载(从页面中消失) | 执行清理工作(比如:清理定时器等) |
render-props高阶组件
React组件复用概述
复用什么?
- state 2. 操作state的方法 (组件状态逻辑 )
两种方式:
1. render props模式 2. 高阶组件(HOC)
注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)
render props 模式
使用步骤
1 创建Mouse组件,在组件中提供复用的状态逻辑代码(1. 状态
2. 操作状态的方法
)
2 将要复用的状态作为 props.render(state) 方法的参数,暴露到组件外部
render() { return this.props.render(this.state) }
3 使用 props.render() 的返回值
作为要渲染的内容
<Mouse render={(mouse) => <p>鼠标当前位置 {mouse.x},{mouse.y}</p>}/>
上面的 render 属性可以使用任意名
函数要是没有返回值,就是直接调用,然后返回了一个 undefined
children代替render属性
1 组件内部:
render(){ return this.props.children(this.state)}
2 使用组件:
<Mouse> <p>{({x,y} => <p>鼠标的位置:{x} {y})}</p> </Mouse>
代码优化
1 给 render porps 添加 props 校验
2 在组件卸载时解除 mousemove 事件绑定
Mouse.propTypes = { children: PropTypes.func.isRequired }
componentWillUnmount(){
window.removeEventListener('mousemove',this.handleMouseMove)
}
高阶函数
如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
1.若A函数,接收的参数
是一个函数,那么A就可以称之为高阶函数。
2.若A函数,调用的返回值
依然是一个函数,那么A就可以称之为高阶函数。
常见的高阶函数有:Promise、setTimeout、arr.map()等等
可以实现函数的复用
//保存表单数据到状态中
saveFormData = (dataType)=>{
return (event)=>{
this.setState({[dataType]:event.target.value})
}
}
用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/>
密码:<input onChange={this.saveFormData('password')} type="password" name="password"/>
不用函数柯里化的方法
saveFormData = (dataType,event)=>{
this.setState({[dataType]:event.target.value})
}
<input onChange={ e => this.saveFormData('username',e)}
<input onChange={ e => this.saveFormData('password',e)}
函数的柯里化
通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
function sum(a){
return(b)=>{
return (c)=>{
return a+b+c
}
}
}
高阶组件
高阶组件(HOC,Higher-Order Component)是一个函数,接收要包装的组件,返回增强后的组件
高阶组件内部创建一个类
组件,在这个类组件中提供复用的状态逻辑
代码,通过props将复用的状态传递
给被包装组件 WrappedComponent
使用步骤:
1 创建一个函数,约定以 with 开头
2 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
3 在函数内部创建一个类组件,提供复用状态逻辑代码,并返回
4 在该组件中 渲染参数组件,同时将状态通过 props 传递给参数组件
function withMouse(WrappedComponent) {
class Mouse extends React.Component {
render(){
return <WrappedComponent {...this.state} {...this.props}/>
}
}
return Mouse
}
5 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
// 创建组件
const MousePosition = withMouse(Position)
// 渲染组件
<MousePosition />
6 设置displayName
-
使用高阶组件存在的问题:
- 得到的两个组件名称相同
-
原因:
- 默认情况下,React使用组件名称作为 displayName
-
displayName的作用:
- 用于设置调试信息(React Developer Tools信息)
-
设置方式:
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}` function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component' }
7 传递props
问题:
- props丢失
原因:
- 高阶组件没有往下传递props
解决方式:
<WrappedComponent {...this.state} {...this.props} />
React原理
✨setState() 的说明 — 🔔面试题-setState深入
更新数据
setState() 是
异步
更新数据的
❗后面的 setState() 不要依赖于前面的 setState()
对象写法:覆盖运行,多次调用 setState ,只会触发最后一次
count:1
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 2 })
{/* 🔔 视图结果为:3 */}
<h2>count:{this.state.count}</h2>
函数写法:所有的 count 更新会叠加运行
count:1
this.setState((state) => {
return { count: state.count + 1 }
})
this.setState((state) => {
return { count: state.count + 2 }
})
{/* 🔔 视图结果为:4 */}
<h2>count:{this.state.count}</h2>
推荐语法:
setState((state, props) => {})
参数state
:表示最新的state
参数props
:表示最新的props
第二个参数
DOM 更新后才触发的回调函数,相当于 Vue 的 this.$nextTick(()=>{ })
setState(updater[, callback])
this.setState(
(state, props) => {},
() => {
document.title = '更新state后的标题:' + this.state.count
}
)
知识点补充—事件的覆盖和叠加
// 会覆盖前面的 onclick
btn.onclick = function () {
console.log('1')
}
btn.onclick = function () {
console.log('2')
}
// 不覆盖前面添加的 click 事件,全部叠加运行
btn.addEventListener('click', () => {
console.log(1)
})
btn.addEventListener('click', () => {
console.log(2)
})
JSX 语法的转化过程
JSX 仅仅是 createElement()
方法的语法糖(简化语法)
JSX 语法被 @babel/preset-react
插件编译为 createElement()
方法
React 元素:是一个对象,用来描述你希望在屏幕上看到的内容
组件更新机制
setState() 的两个作用:
- 修改 state
- 更新组件(UI)
过程:
父组件重新渲染时,也会重新渲染子组件
,但只会渲染当前组件子树(当前组件及其所有子组件)
官网原话:大部分情况下,你应该遵循默认行为。
✨组件性能优化
减轻 state
- 只存储跟组件相关的数据(比如:
count
/ 列表数据 /loading
等 - 不要做渲染的数据不要放在 state 中,比如定时器 id 等
-
对于需要在多个方法中用到的数据,应该放在
this
中(这样不会加重 state)// 与 state 同级: timerId = -1 componentDidMount() { this.timerId = setInterval(() => { console.log('定时器运行中') }, 1000) } componentWillUnmount() { clearInterval(this.timerId) }
-
避免不必要的重新渲染
由于 组件更新机制:
父组件更新会引起子组件也被更新,那么子组件没有任何变化时也会重新渲染
解决:使用钩子函数 shouldComponentUpdate(nextProps, nextState)
🚀scu
-
作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染
-
触发时机:更新阶段的钩子函数,组件重新渲染前执行 (shouldComponentUpdate --> render)
class Hello extends Component{
shouldComponentUpdate(nextProps, nextState){
// 根据条件,决定是否重新渲染组件
return boolean(eg: nextState.number !== this.state.number)
}
render(){...}
}
纯组件
PureComponent 与 React.Component 功能相似
✨区别:PureComponent 内部自动实现了 shouldComponentUpdate
钩子,不需要手动比较
PureComponent 底层源码揭秘:纯组件内部通过分别 对比 前后两次 props
和 state
的值,如果没有改变,返回 false 以告知 React 可以跳过更新。
class Hello extends React.PureComponent{
render(){
return (
<div>纯组件</div>
)
}
}
纯组件内部的对比是 shallow compare(浅层对比)
-
对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)
最新的state.number === 上一次的state.number // false,重新渲染组件
-
❗对于引用类型来说:只比较对象的
引用(地址)
是否相同,而不是❌比较值const obj = { number: 0 } ❌const newObj = obj(不要直接赋值) newObj.number = 2 // ❌错误做法 state.obj.number = 2 最新的state.obj === 上一次的state.obj // true,不重新渲染组件
-
❗state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!
✅对象
const newObj = { ...state.obj,number:2 }
setState({ obj:newObj })
✅数组
- ❌不要用数组的push / unshift 等直接修改当前数组的的方法
- ✅应该用 concat 或 slice 等这些返回新数组的方法
this.setState({ list:[...this.state.list, {新数据}] })
虚拟 DOM 和 Diff 算法(实现部分更新)
虚拟 DOM本质是保存 DOM 关键信息的 JS 对象(state + JSX)
好处
提高DOM更新的性能,不频繁的操作真实Dom,在内存中找到变化的部分,再更新真实DOM
执行过程
- 初次渲染时,React 会根据初始state(Model),创建一个虚拟 DOM 对象(树)。
- 根据虚拟 DOM 生成真正的 DOM,渲染到页面中。
- 当数据变化后(setState()),重新根据新的数据,创建新的虚拟DOM对象(树)。
- 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容。
- 最终,React 只将变化的内容更新(patch)到 DOM 中,重新渲染到页面。
React路由基础—V5
路由基本使用
使用React路由简单来说,就是配置 路径和组件(配对)
使用步骤:
-
安装:
yarn add react-router-dom
-
导入路由的三个核心组件:
Router / Route / Link
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
-
✨使用
Router
组件包裹整个应用(重要)- 一个 React应用只需用一次
<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)
类似 Vue 的 new VueRouter()
✨**BrowserRouter
**:使用 H5 的 history
API 实现(localhost:3000/first)
✨✅ ❗开发经验:导入 HashRouter / BrowserRouter 组件后建议通过 as 改名为 Router
import { BrowserRouter as Router } from 'react-router-dom'
Link/NavLink
组件:
用于指定导航链接(a 标签),类似 Vue 的
<router-link>
组件
Route(OBJECT)
组件:
指定路由展示组件相关信息,相当于 Vue 的
<router-view>
+ 路由配置
-
path
属性:路由规则 -
component
属性:展示的组件 -
推荐写法:通过 component 属性绑定组件,会通过 props 传递路由相关信息给子组件,工作常用
<Route path='/find' component={Find} />
-
以下写法只负责展示组件,没有传递路由相关 props 给子组件
<Route path='/my'> <My/> </Route >
-
Route组件写在哪,渲染出来的组件就展示在哪
Switch 组件 — 404错误处理提示处理
通常,我们会把
Route
包裹在一个Switch
组件中
在Switch
组件中,不管有多少个路由规则匹配到了,都只会渲染第一个匹配的组件
✅通过Switch
组件内,最后一个
Route 组件可用于实现404错误页面的提示
<Switch>
<Route path='/find' component={Find} />
<Route path='/my' component={My} />
{/* 处理 404 情况需要写 Switch 的最后 */}
<Route component={NotFound}></Route>
</Switch>
❗注意事项:
Link/NavLink
组件需要嵌套到 HashRouter / BrowserRouter
组件内
NavLink
组件如果路径匹配的话,会自动添加 class="active"
的类名
- 样式要引入 index.js 中
❗记得 import 再用
React路由 V5
的基本使用
import { BrowserRouter as Router, Route, Link, NavLink } from 'react-router-dom'
<Router>
<h1>仿网易云音乐路由 - 公共的头部</h1>
<Link to="/find">发现音乐</Link>
<br />
<NavLink to="/my">我的音乐</NavLink>
<Route path="/find" component={Find}></Route>
<Route path="/my" component={My}></Route>
</Router>
路由执行过程
- 点击 Link 组件(a标签),修改了浏览器地址栏中的 url 。
- React 路由监听到地址栏 url 的变化。
- React 路由内部遍历所有 Route 组件,使用路由规则( path )与 pathname 进行匹配。
- 当路由规则(path)能够匹配地址栏中的 pathname 时,就展示该 Route 组件的内容。
路由嵌套
src/views/Find.js
<NavLink to='/find/toplist'>排行榜</NavLink>
{/* React的 Route 组件:相当于 Vue 的 <router-view> + 路由配置 */}
<Route path='/find/toplist' component={Toplist} />
编程式导航
通过 JS 代码来实现页面跳转
history
是 React 路由提供的,用于获取浏览器历史记录的相关信息push(path)
:跳转到某个页面,参数 path 表示要跳转的路径go(n)
: 前进或后退到某个页面,参数 n 表示前进或后退页面数量(比如:-1 表示后退到上一页)
this.props.history.push('/my')
// vue 的写法 this.$router.push('/my')
默认路由
表示进入页面时就会匹配的路由,默认路由path为:/
<Route path='/' component={Home} />
匹配模式
默认情况下,React 路由是模糊匹配模式
模糊匹配模式
模糊匹配规则:只要 pathname 以 path 开头就会匹配成功
path
代表 Route 组件的 path 属性pathname
代表 Link 组件的 to 属性(也就是location.pathname
)
精确匹配
避免默认路由在任何情况下都会展示
给 Route 组件添加 exact
属性,让其变为精确匹配模式
// 此时,该组件只能匹配 pathname=“/” 这一种情况
<Route exact path="/" component=... />
✨推荐:给默认路由添加 exact 属性。
动态路由和路由参数
动态路由参数,通过 /:xxx
占位
<Route path='/playlist/:id' component={PlayListDetail} />
获取动态路由参数
React: this.props.match.params.xxx
Vue: this.$route.params.xx
React路由基础—V6
❗只写与 V5不同的点
核心组件
Routes
提供一个路由出口,满足条件的路由组件会渲染到组件内部,定义 path 和组件之间的关系
语法说明:
<Routes>
<Route/>
<Route/>
</Routes>
Route
用于指定导航链接,完成路由匹配
<Route path="/about" element={ <About/> }
path 指定匹配的路径地址 element 指定要渲染的组件
编程式导航
语法:
navigage('/about,{ replace:true }')
-
导入useNavigate钩子函数
-
执行钩子函数得到跳转函数
-
执行跳转函数完成跳转
✨注:如果在跳转时不想加历史记录,可以添加额外参数 replace 为 true
跳转携带参数
1 searchParams传参
传参:navigage('/about/?id=100')
取参:
let [params] = useSearchParams()
let id = params.get('id')
2 params传参(path配合:/:id)
传参:navigage('/about/1001')
取参:
let [params] = useParams()
let id = params.get('id')
嵌套路由
1. App.js: 定义嵌套路由声明
<Routes>
// 定义嵌套关系
<Route path='/' element={ <Layout /> }>
<Route path='/board' element={ <Board /> } />
<Route/>
</Routes>
2. Layout.js: 使用 <Outlet/>
指定二级路由出口
import { Outlet } from 'react-router-dom'
function Layout (){
return (
<div>
// 二级路由出口
<Outlet />
</div>
)
}
默认二级路由
场景:首先渲染完毕就要显示的二级路由
步骤:
- 给默认的路由标记 index
- 去掉跳转路径 path
<Routes>
<Route path='/' element={ <Layout /> }>
<Route index element={ <Board /> } />
<Route path='/article' element={ <Board /> } />
<Route/>
</Routes>
404路由配置
场景:当所有的路径都没有匹配成功的时候显示
语法说明:在各级路由的最后添加 *号路由
作为兜底
<Routes>
<Route path='/' element={ <Layout /> }>
<Route index element={ <Board /> } />
<Route path='/article' element={ <Article /> } />
<Route path='*' element={ <NotFound /> } />
<Route/>
</Routes>
✨Hooks基础
什么是hooks
Hooks的本质:一套能够使
函数组件
更强大,更灵活的“钩子”,react v16.8开始
❗注意点:
- 有了hooks之后,为了兼容老版本,class类组件并没有被移除,俩者都可以使用
- 有了hooks之后,不能在把函数成为无状态组件了,因为hooks为函数组件提供了状态
- hooks只能在
函数组件
中使用
Hooks解决了什么问题
1 组件的状态逻辑复用
mixin混入的数据来源不清晰,HOC高阶组件的嵌套问题
2 class组件自身的问题
学习成本高,比如各种生命周期,this指向问题等
useState
替换之前的 state 写法
语法:
const [ 访问state的变量名, 设置state的函数名 ] = useState(初始值)
❗注意事项:
- useState 需要主动导入
- 函数组件没有 this ( this的值为 undefined )
state={ count:0 }
==> const [count,seCount] = useState(0)
this.setState({ count:this.state.count +1 })
==> setCount(count + 1)
函数组件声明多个状态
useState() 可以调用多次,声明多个状态
为函数组件添加多个状态,再调用 useState 即可
const [isShow,setIsShow] = useState(true)
<button onClick={()=> setIsShow(!isShow)}></button>
❗useState
注意事项
1 只能出现在函数组件中
2 不能嵌套在if/for/其它函数中(react按照hooks的调用顺序识别每一个hook)
-
let num = 1 function List(){ num++ if(num / 2 === 0){ const [name, setName] = useState('cp') } const [list,setList] = useState([]) } // 俩个hook的顺序不是固定的,这是不可以的!!!
3 可以通过开发者工具查看hooks状态
useEffect - 副作用
主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用比如:
官方定义: 数据获取,设置订阅,localstorage操作以及手动更改 React 组件中的 DOM 都属于副作用。
官方提示:
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount
这三个函数的组合。
1. 组件挂载时会执行一次 2. 每次组件更新时会执行 3. useEffect 内部返回的回调函数,会在组件被卸载时自动执行
基本使用
1 导入 useEffect
函数
2 调用 useEffect
函数,并传入回调函数
3 在回调函数中编写副作用处理(dom操作)
4 修改数据状态
5 检测副作用是否生效
useEffect - 传入和不传入第二个可选参数区别
没传第二个参数:componentDidMount + componentDidUpdate
- 组件挂载时会执行一次
- 每次组件更新(所有的state, props)时会执行
- useEffect 内部返回的回调函数,会在组件被卸载时自动执行
useEffect(()=>{
console.log(11111, '没有传入第二个参数', count)
})
传递了第二个参数:componentDidUpdate + 内部判断参数变化
用法1:添加空数组
组件只在首次渲染时执行一次
// componentDidMount()
useEffect(() => {
console.log('组件挂载时,运行一次~~')
}, [])
用法2:添加特定依赖项
类似 Vue 的立刻执行的 watch,在首次渲染时执行,在依赖项发生变化时重新执行
// componentDidUpdate()
useEffect(() => {
console.log('count更新时', count)
}, [count])
✨Hooks进阶
useState - 回调函数的参数
使用场景
参数只会在初次渲染中起作用,初始 state 需要通过计算
才能获得,则可以传入一个函数,返回计算后的初始值,供初始渲染的时候用
语法:
const [name,setName] = useState(()=>{
// 编写计算逻辑 return '计算之后的初始值'
})
❗语法规则:
- 回调函数return出去的值将作为初始值
- 回调函数中的逻辑只会在组件初始化的时候执行一次
应用:
如何实现一个自增按钮,可以由使用的时候以传参的方式决定递增的初始值?
import { useState } from 'react'
export default function App() {
return <Count initCount={10} />
}
function Count({ initCount }) {
const [count, setCount] = useState(() => initCount)
return (
<div>
<h2>count:{count}</h2>
<button onClick={() => setCount(count + 1)}> +</button>
</div>
)
}
useEffect清理操作
语法:
useEffect() 内部返回的回调函数,会在组件卸载
时自动执行(相当于类组件的 componentWillUnmount)
作用:
执行清除操作(如:清理定时器,清理全局注册的事件,清理订阅
等)
useEffect(() => {
// 挂载时运行的代码
console.log('组件挂载!!!')
window.addEventListener('resize', fn)
// ✅ 卸载时运行的代码
return () => {
console.log('组件卸载了~~~')
window.removeEventListener('resize', fn)
}
}, [])
useEffect - 发送请求获取数据
❗开发注意事项:
❌1 不要给 useEffect 第一级函数添加 async
- 因为异步会导致清理函数无法立即返回
❌常见错误写法: useEffect(async () => {}, [])
✅正确写法:
const [channels, setChannels] = useState([])
useEffect(() => {
async function loadDate(){
// 使用 axios 请求数据
const res = await request({
url: '/v1_0/channels',
method: 'get',
})
setChannels(res.data.channels)
}
loadDate()
}, [])
2 ✅useEffect 回调函数中用到的数据就是依赖数据,就应该出现在依赖项数组中,如果不添加依赖项就会有bug出现
useRef
可以在函数组件中获取真实的dom元素对象或者是组件对象
使用步骤:
- 导入
useRef
函数 - 执行
useRef
函数并传入null,返回值为一个对象 内部有一个current属性存放拿到的dom对象(组件实例) - 通过 ref 绑定 要获取的元素或者组件
获取dom
import { useEffect, useRef } from 'react'
function App() {
const h1Ref = useRef(null)
useEffect(() => {
console.log(h1Ref)
},[])
return (
<div>
<h1 ref={ h1Ref }>this is h1</h1>
</div>
)
}
export default App
获取组件实例
函数组件由于没有实例,不能使用ref获取,如果想获取组件实例,必须是类组件
Foo.js
class Foo extends React.Component {
sayHi = () => {
console.log('say hi')
}
render(){
return <div>Foo</div>
}
}
export default Foo
App.js
import { useEffect, useRef } from 'react'
import Foo from './Foo'
function App() {
const h1Foo = useRef(null)
useEffect(() => {
console.log(h1Foo)
}, [])
return (
<div> <Foo ref={ h1Foo } /></div>
)
}
export default App
useContext
实现步骤
- 使用
createContext
创建Context对象 - 在顶层组件通过
Provider
提供数据 - 在底层组件通过
useContext
函数获取数据
代码实现
import { createContext, useContext } from 'react'
// 创建Context对象
const Context = createContext()
function Foo() {
return <div>Foo <Bar/></div>
}
function Bar() {
// 底层组件通过useContext函数获取数据
const name = useContext(Context)
return <div>Bar {name}</div>
}
function App() {
return (
// 顶层组件通过Provider 提供数据
<Context.Provider value={'this is name'}>
<div><Foo/></div>
</Context.Provider>
)
}
export default App
杂记
TodoList案例重难点分析 — 类组件
❗不要直接改 state 的数据
如:id:list.length++
这样其实就是直接改了 list ,给它直接加了一个 empty 值
控制显示隐藏:
style={{ display: list.length === 0 ? 'none' : '' }}
✨调用父组件的方法步骤:
- 父组件准备方法
- 传递给子组件
- 子组件解构出来,判断是否是带参函数
- 是,
() => addList(参数)
- 不是,
addList()
- 是,
删除任务实现:
-
传 id
-
用数组过滤方法
-
更新 list 值
delTodo = id => { this.setState({ list: this.state.list.filter(item => item.id !== id) }) }
✨❗更新 单选框状态
changeChecked = id => {
const newList = this.state.list.map(item => {
if (item.id === id) {
// 需要更新 done
return {
...item,
done: !item.done
}
} else {
return item
}
})
this.setState({
list: newList
})
}
✨更新全选状态
// 1. allChecked 由反选框计算出来
const allChecked = list.length !== 0 && list.every(item => item.done)
// 2. 改 allChecked 即更改反选框的值
changeAll = allChecked => {
const newList = this.state.list.map(item => {
return { ...item, done: allChecked }
})
this.setState({
list: newList
})
}
✨添加任务用拓展运算符
addList = options => {
this.setState({
list: [...this.state.list, options]
})
}
计算完成任务总量的两种方法
1 filter
const count = list.filter(item => item.done).length
✅2 reduce
适合结合价格、数量等复杂的算法
const count = list.reduce((sum, item) => {
if (item.done) return ++sum
else return sum
}, 0)
✨本地存储
组件挂载时获取本地存储:
componentDidMount() {
this.setState({
list: JSON.parse(localStorage.getItem('todos')) || [],
type: JSON.parse(localStorage.getItem('todos-type')) || 'all'
})
}
组件(state,props)更新时,设置本地存储
componentDidUpdate(prevProps, prevState) {
localStorage.setItem('todos', JSON.stringify(this.state.list))
localStorage.setItem('todos-type', JSON.stringify(this.state.type))
}
函数组件的存储:
// 组件挂载时 - 获取本地存储数据
useEffect(() => {
console.log('组件挂载时 - 获取本地存储数据')
setList(JSON.parse(localStorage.getItem('todos')))
setActive(localStorage.getItem('todos-type'))
}, [])
// 数据更新时 - 设置本地存储数据
useEffect(() => {
console.log('数据更新时 - 设置本地存储数据')
localStorage.setItem('todos', JSON.stringify(list))
localStorage.setItem('todos-type', active)
}, [list, active])
❗类组件中自己写的函数记得要写成箭头函数!!
✅React 进阶
状态逻辑复用
高阶函数
什么是高阶组件?
高阶组件本质是一个函数,类比高阶组件,入参为组件,返回值也为组件,可以在函数体内给入参组件携带更多参数
如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
1.若A函数,接收的参数
是一个函数,那么A就可以称之为高阶函数
。
2.若A函数,调用的返回值
依然是一个函数
,那么A就可以称之为高阶函数。
常见的高阶函数有:Promise
、setTimeout
、arr.map()
等等
高阶组件的代码实现
实现两个组件:1 在页面中显示鼠标在页面中的位置 2 在页面中实现span标签跟随鼠标移动
HocWrapper.js
import React from 'react'
const withMouse =(BaseComponent) => {
class Wrapper extends React.Component {
// 状态和逻辑公共部分
state = {
x: 0,
y: 0,
}
moveHandler = (e) => {
this.setState({
x: e.pageX,
y: e.pageY,
})
}
}
componentDidMount() {
document.addEventListener('mousemove', this.moveHandler)
}
componentWillUnmount() {
document.removeEventListener('mousemove', this.moveHandler)
}
render() {
const { x, y } = this.state
// 数据状态x,y 想怎么用怎么用
return <BaseComponent x={x} y={y} />
}
}
return Wrapper
}
export { withMouse }
使用高阶组件
import React from 'react'
import { withMouse } from './HocWrapper'
function AComponent({ x, y }) {
return (
<div>
x:{x},y:{y}
</div>
)
}
// 调用高阶组件包裹源组件
const WrapperA = withMouse(AComponent)
class App extends React.Component {
render() {
return (
<div>
<WrapperA />
</div>
)
}
}
export default App
render props
render props本质是一个
组件
,将组件的children属性设计为一个函数,在调用函数的同时传入额外参数
render props定义 renderProps.jsx
// render Props
import React from 'react'
class WithMouse extends React.Component {
state = {
x: 0,
y: 0,
}
moveHandler = (e) => {
this.setState({
x: e.pageX,
y: e.pageY,
})
}
componentDidMount() {
document.addEventListener('mousemove', this.moveHandler)
}
componentWillUnmount() {
document.removeEventListener('mousemove', this.moveHandler)
}
render() {
return <div>{this.props.children(this.state)}</div>
}
}
export default WithMouse
使用 render props
import React from 'react'
import WithMouse from './renderProps'
function AComponent({ x, y }) {
return (
<div>
x:{x},y:{y}
</div>
)
}
class App extends React.Component {
render() {
return (
<div>
<WithMouse>{(state) => <AComponent {...state} />}</WithMouse>
</div>
)
}
}
export default App
hooks
天生为组合而生
hook实现
import { useState, useEffect } from 'react'
function useMouse () {
const [p, setP] = useState({
x: 0,
y: 0
})
function moveHandler (e) {
setP({
x: e.pageX,
y: e.pageY
})
}
useEffect(() => {
document.addEventListener('mousemove', moveHandler)
return () => {
document.removeEventListener('mousemove', moveHandler)
}
}, [])
return [p.x, p.y]
}
export { useMouse }
hook使用
import { useMouse } from './withMouse'
function AComponent() {
const [x, y] = useMouse()
return (
<div>
x:{x},y:{y}
</div>
)
}
性能优化
优化的方向:减少没必要的组件更新
组件更新机制:
父组件更新会引起子组件也被更新,那么子组件没有任何变化时也会重新渲染
React.memo
React.memo是一个高阶组件,其实就是一个函数,通过比较**
props的变化
**决定是否重新渲染,如果通过内部的对比发现props并没有变化,则不会重新渲染,从而达到提高渲染性能的目的
基础使用
1)props不变不重新渲染
import React, { useState } from 'react'
// 通过React.memo包裹子组件
const Son = React.memo(() => {
console.log('我是son组件,我更新了')
return <div>this is son </div>
})
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
<Son />
</div>
)
}
export default App
再次运行发现,子组件只会在首次渲染时执行一次,后续App组件的更新不再影响它
2)props变化重新渲染
import React, { useState } from 'react'
const Son = React.memo(() => {
console.log('我是son组件,我更新了')
return <div>this is son </div>
})
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
<Son count={count} />
</div>
)
}
export default App
再次运行发现,子组件会随着count的变化而变化
复杂类型的Props
由于react内部对于复杂类型数据进行的是浅对比,只对比引用,如果引用不同,则代表俩次的props是不一致的,如果不一致,就重新渲染
import React, { useState } from 'react'
const Son = React.memo(() => {
console.log('我是son组件,我更新了')
return <div>this is son </div>
})
function App() {
const [count, setCount] = useState(0)
// 这里我们每次 修改count进行App组件更新 list都会生成一个不同的引用 所以造成子组件又重新渲染了
const list = [1, 2, 3]
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
<Son list={list} />
</div>
)
}
export default App
自定义props对比
如果不想使用React.memo内置的对比方式,而是想自己做判断,也是可以的,它为我们开放了对比函数,只需要在memo函数中传入第二个参数即可
import React, { useState } from 'react'
const Son = React.memo(
() => {
console.log('我是son组件,我更新了')
return <div>this is son </div>
},
(prev, next) => {
// 自定义对比关系 决定是否要重新渲染
// 如果返回false 代表俩次props发生了改变 组件重新渲染
// 如果返回true 代表来此props没有发生变化 组件不会重新渲染
return prev.list.length === next.list.length
}
)
function App() {
const [count, setCount] = useState(0)
const list = [1, 2, 3]
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
<Son list={list} />
</div>
)
}
export default App
useCallBack
作用:缓存一个函数,只在依赖项变化的时候才会更新引用值,经常与React.memo配合使用
未优化版本
我们上一节刚讲过,通过React.memo虽然可以在一定程度上避免子组件务必要的渲染,但是对于引用类型是无效的,那函数也是一个引用类型,且在父传子的时候也是可以把函数传给子组件的,这个时候就可以利用useCallBack和React.memo配合使用
import React, { useState } from 'react'
const Son = React.memo(() => {
console.log('Son组件更新了')
return <div>this is son</div>
})
function App() {
const [count, setCount] = useState(0)
const getName = () => {
return 'this is app'
}
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
{/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
<Son getName={getName} />
</div>
)
}
export default App
优化版本
import React, { useState, useCallback } from 'react'
const Son = React.memo(() => {
console.log('Son组件更新了')
return <div>this is son</div>
})
function App() {
const [count, setCount] = useState(0)
// 缓存
const getName = useCallback(() => {
return 'this is app'
}, [])
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
{/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
<Son getName={getName} />
</div>
)
}
export default App
useMemo
作用:缓存一个函数,该函数只在依赖项变化时才会重新执行此函数,在一些计算量很大的场景中非常有用
错误演示
import React, { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const getNums = () => {
// 模拟昂贵计算
const nums = new Array(10000).fill(0).map((item) => item)
console.log('计算了')
return nums
}
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
{getNums()}
</div>
)
}
export default App
getNums内部有一个很大的计算,每次只有count发生变化,组件重新执行,计算也跟着重新执行,然后这个计算很明显只计算一次就可以了,这时候,就需要使用useMemo来进行优化
优化版本
import React, { useMemo, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const getNums = useMemo(() => {
// 模拟昂贵计算
const nums = new Array(10000).fill(0).map((item) => item)
console.log('计算了')
return nums
}, [])
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
{getNums}
</div>
)
}
export default App
✨虚拟DOM与diff算法
虚拟DOM是什么?
1 本质是Object类型的对象(一般对象)
2 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。
3 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。
Diff算法
gth
}
)
function App() {
const [count, setCount] = useState(0)
const list = [1, 2, 3]
return (
<button onClick={() => setCount(count + 1)}>+{count}
)
}
export default App
## useCallBack
> 作用:缓存一个函数,只在依赖项变化的时候才会更新引用值,经常与React.memo配合使用
**未优化版本**
我们上一节刚讲过,通过React.memo虽然可以在一定程度上避免子组件务必要的渲染,但是对于引用类型是无效的,那函数也是一个引用类型,且在父传子的时候也是可以把函数传给子组件的,这个时候就可以利用useCallBack和React.memo配合使用
```jsx
import React, { useState } from 'react'
const Son = React.memo(() => {
console.log('Son组件更新了')
return <div>this is son</div>
})
function App() {
const [count, setCount] = useState(0)
const getName = () => {
return 'this is app'
}
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
{/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
<Son getName={getName} />
</div>
)
}
export default App
优化版本
import React, { useState, useCallback } from 'react'
const Son = React.memo(() => {
console.log('Son组件更新了')
return <div>this is son</div>
})
function App() {
const [count, setCount] = useState(0)
// 缓存
const getName = useCallback(() => {
return 'this is app'
}, [])
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
{/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
<Son getName={getName} />
</div>
)
}
export default App
useMemo
作用:缓存一个函数,该函数只在依赖项变化时才会重新执行此函数,在一些计算量很大的场景中非常有用
错误演示
import React, { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const getNums = () => {
// 模拟昂贵计算
const nums = new Array(10000).fill(0).map((item) => item)
console.log('计算了')
return nums
}
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
{getNums()}
</div>
)
}
export default App
getNums内部有一个很大的计算,每次只有count发生变化,组件重新执行,计算也跟着重新执行,然后这个计算很明显只计算一次就可以了,这时候,就需要使用useMemo来进行优化
优化版本
import React, { useMemo, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const getNums = useMemo(() => {
// 模拟昂贵计算
const nums = new Array(10000).fill(0).map((item) => item)
console.log('计算了')
return nums
}, [])
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
{getNums}
</div>
)
}
export default App
✨虚拟DOM与diff算法
虚拟DOM是什么?
1 本质是Object类型的对象(一般对象)
2 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。
3 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。
Diff算法
对比 初始虚拟 DOM 树 和 更新后的虚拟 DOM 树,找到不同之处,最终,只将不同的地方更新到页面中