标志为在 vscode 书写的快捷提示
❗ 标志为易错点和注意点
✨✅表示重点
React 是一个用于构建用户界面的 JavaScript 库。React 主要用来写HTML页面,或构建Web应用
1 声明式
2 基于组件
3 学习一次,随处使用
npm i react react-dom
React.createElement('h1',null,'Hello React'(文本节点),React.createElement('span',null,'Hello React')(元素节点)
元素名称
属性
eg:{ className:'box' }
子节点
ReactDOM.render(el, document.getElementById('root'))
React元素
DOM对象
,用于指定渲染到页面中的位置(挂载点
)脚手架是开发 现代Web 应用的必备。
初始化项目,命令:npx create-react-app my-app
启动项目,在项目根目录
执行命令:npm start
npx
命令
目的:提升包内提供的命令行工具的使用体验
现在:无需
安装脚手架包,就可以直接
使用这个包提供的命令
1 导入 react 和 react-dom 两个包。
import xx from 'react'
import xx from 'react-dom'
'react-dom/client'
2 调用 React.createElement()
方法创建 react 元素。❌
3 调用 ReactDOM.render()
方法渲染 react 元素到页面中。
ReactDOM.createRoot(document.getElementById('root')).render()
render(App())
JSX 是 JavaScript XML 的简写,表示在 JavaScript 代码中写 XML(HTML) 格式的代码。
优势:声明式语法更加直观、与HTML结构相同,降低了学习成本、提升开发效率
✨语法糖
=> 通过 babel 插件 => 转化成 JS 能识别的语法 React.createElement()
JSX 是 React 的核心内容。
1 ⭐️推荐使用 JSX
语法创建 react(虚拟DOM) 元素
const title =
Hello JSX
2 使用 ReactDOM.render() 方法渲染 react 元素到页面中
ReactDOM.render(title, root)
为什么脚手架
中可以使用 JSX 语法?
JSX 不是标准的 ECMAScript 语法,它是 ECMAScript 的语法扩展。
reate-react-app 脚手架中已经默认有babel
,无需手动配置。
{ JSON.stringfy(obj) }
String{false}
数据存储在JS中
语法:{ JavaScript表达式 }
注意:语法中是单大括号,不是双大括号!
JavaScript 表达式
* + - / 三元表达式…不
能写 js 对象
,一般只出现在 style
属性里{JSX}
不
能在{}中出现语句(比如:if/for
等)a
a+b
demo(1)
arr.map()
function test () {}
if(){}
for(){}
switch(){case:xxxx}
if/else
三元表达式
逻辑与运算符
如果要渲染一组数组,应该使用数组的 map() 方法
注意:
遍历
谁,就给谁添加 key 属性{songs.map(item =>
❗ 属性值写法:属性名={ 值 }
,与 Vue 不一样
12px
可省略成 12
JSX的样式处理
React 完全利用 JS 语言自身的能力来编写UI,而不是造轮子增强 HTML 功 能
1、 属性名使用 驼峰命名法
2、 特殊属性名:class->className
for->htmlFor
tabindex->tabIndex
3、 没有子节点的React 元素可以使用 />
,必须有结束标签
4、 推荐使用 ()
包裹 JSX 避免 JS 中的自动插入分号陷阱。
const dv = hello
5、 标签中混入JS表达式时要用{}
6、 只有一个根标签
,必须要有一个根节点:<>>
7、 内联样式: 要用style={{ key:value }}
的形式去写
8、 标签首字母
(1).若小
写字母开头,则将该标签转为html中同名元素
,若html中无该标签对应的同名元素,则报错。
(2).若大
写字母开头,react就去渲染对应的组件
,若组件没有定义,则报错。
9、❗布尔类型、null、undefined 在 JSX 中不会显示在页面中(Vue 模板会转化成字符串再显示) String(true)
使用React 就是使用组件,组件表示页面中的部分功能,组合多个组件实现完整的页面功能
特点:可复用、独立、可组合
使用 JS 的函数(或箭头函数)创建的组件
大写字母
开头组件的结构
渲染组件:
function Hello() {
return (
<div>这是我的第一个函数组件!</div>
)
}
ReactDOM.render(<Hello />, root)
执行了ReactDOM.render(…之后,发生了什么?
1.React解析组件标签,找到了MyComponent组件。
2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
使用 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,随后呈现在页面中。
组件作为一个独立的个体,一般都会放到一个单独的 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( , 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'))
语法:
on+事件名称={事件处理程序}
,比如:onClick={() => {}}/{handelClick}
❗注意不要写括号调用
React 事件采用驼峰命名法
可以通过事件处理程序的参数获取到事件对象 event
React 中的事件对象叫做:合成事件(对象)
(1).通过onXxx属性指定事件处理函数(注意大小写)
a.React使用的是自定义(合成)事件, 而不是使用的原生DOM事件 —————— `为了更好的兼容性`
b.React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) ————————`为了的高效`
(2).通过event.target得到发生事件的DOM元素对象 ——————————`避免过度使用 ref`
函数组件又叫做无状态(数据)组件,类组件又叫做有状态组件
函数组件
类组件
state
和 setState
状态(state)即数据,是组件内部的私有数据,只能在组件内部使用
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 中的值,这是错误的!!!
this.setState({ 要修改的数据 })
不
要直接修改 state
中的值,这是错误的!!!数据驱动视图
可以修改 state,后面使用 setState({})
方法可以使render方法重新执行,进而更新数据
❌常见错误:通过赋值的方式修改 state,render 方法不会运行,视图不会更新(数据和视图不同步)
原因:事件处理程序中 this 的值为 undefined
希望:this 指向组件实例
(render方法中的 this 即为组件实例)
React 事件处理 语法:
on事件类型={ 事件处理函数 }
❗ 花括号内绑定的是事件处理函数,而不是函数的调用
eg:onClick={this.handleClick}
利用箭头函数自身不绑定this的特点
render() 方法中的 this 为组件实例,可以获取到 setState()
事件传参 语法:on事件类型={ () => this.handleXxx(参数) }
利用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>
)
}
}
利用箭头函数形式的class实例方法
注意:该语法是实验性语法,但是,由于 babel 的存在可以直接使用
onIncrement = () => {
this.setState({ … })
}
onIncrement 放在 实例对象上
由于 onIncrement 是作为onClick的回调,所以不是通过实例调用的,是直接调用
//类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined
箭头函数
写法,否则 this 指向为 undefinedstate
与表单元素值 value
(相当于 v-model 双向绑定)绑定到一起,由 state 的值来控制表单元素的值步骤:
1 在 state 中添加一个状态,作为表单元素的value值(控制表单元素值的来源)
state = { txt: '' }
2 给表单元素绑定 change
事件,将 表单元素的值 设置为 state 的值(控制表单元素值的变化)
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})
}
借助于 ref,使用原生 DOM 方式来获取表单元素值
场景:
DOM
元素,和 document.querySelector()
获取无差别使用步骤
1 调用 React.createRef() 方法创建一个 ref 对象
constructor() {
super()
this.txtRef = React.createRef()
}
2 将创建好的 ref 对象添加到文本框中
3 通过 ref 对象获取到文本框的值
Console.log(this.txtRef.current.value)
组件通讯介绍
打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯。
作用:接收传递给组件的数据
1 传递数据:给组件标签添加属性
" "
{}
2 接收数据:函数
组件通过参数props
接收数据,类
组件通过 this.props
接收数据
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 传递给 super()
,否则,无法在构造函数
中获取到 props!
1 父组件 -> 子组件
父组件提供要传递的state数据
state = { lastName: '王' }
给子组件标签添加属性,值为 state 中的数据
子组件中通过 props
接收父组件中传递的数据
function Child(props) { return 子组件接收到数据:{ props.name } }
解构
出父组件传过来的数据{ this.props.name }
{ ()=>this.props.HandleTilte(this.state.title) }
2 子组件 -> 父组件
利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数。
父组件提供一个回调函数(用于接收数据)
getChildMsg = data=> {this.setState({parentMsg:data})}
将该函数作为属性的值,传递给子组件
子组件通过 props 调用回调函数
handleClick=()=>{ this.props.getMsg(this.state.childMsg) }
将子组件的数据作为参数传递给回调函数
3 兄弟组件
将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
公共父组件职责:1. 提供共享状态 2. 提供操作共享状态的方法
要通讯的子组件只需通过 props 接收状态或操作状态的方法
❗注意点:
App => Child 组件
1 调用 React. createContext()
创建 Provider
(提供数据) 和 Consumer
(消费数据) 两个组件。
const { Provider, Consumer } = React.createContext()
2 使用 Provider 组件作为父节点
3 设置 value 属性,表示要传递的数据。
4 子组件调用 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 内部嵌套的函数会自动执行,要注意函数的返回值需符合 模板数据展示语法要求
分拆的单文件组件如何处理?
// 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 就会有该属性
值可以是任意值(文本、React元素、组件,甚至是函数)
React 插槽和 Vue 插槽写法区别:
Vue插槽 :
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导致的错误,给出明确的错误提示,增加组件的健壮性
导入 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 时生效
App.defaultProps = { pageSize: 10 }
类组件✨
也可在组件内写:static defaultProps = { pageSize:10 }
传:
函数组件✨
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')
}
}
意义:
组件的生命周期:
方法调用
,这些方法就是生命周期的钩子函数
。钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。
只有 类组件 才有生命周期。
执行时机 :组件创建时(页面加载时)
钩子函数的执行顺序: 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'))|| []})
}
执行时机 : 1 setState()
2 forceUpdate()
3 组件接收到新的 props
钩子函数的执行顺序 : render
-> componentDidUpdate
cdu
钩子函数 | 触发时机 | 作用 |
---|---|---|
render | 每次组件渲染都会触发 | 渲染UI(与 挂载阶段 是同一个render) |
componentDidUpdate | 组件更新后(DOM渲染完毕) | DOM操作,可以获取到更新后的DOM内容,不要直接调用setState |
❗ render
生命周期注意点:
✨React 可通过 componentDidUpdate
钩子实现类似 Vue 中的 watch
功能
componentDidUpdate(prevProps,prevState){
// count 新值和旧值不一样,说明 count 变化了(Vue的watch)
if(this.state.count !== prevState.count){
console.log('count变化了,实现Vue的watch')
}
}
也可在componentDidUpdate
中设置本地存储
执行时机 : 组件从页面中消失 cwu
钩子函数 | 触发时机 | 作用 |
---|---|---|
componentWillUnmount | 组件卸载(从页面中消失) | 执行清理工作(比如:清理定时器等) |
1. render props模式 2. 高阶组件(HOC)
注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)
1 创建Mouse组件,在组件中提供复用的状态逻辑代码(1. 状态
2. 操作状态的方法
)
2 将要复用的状态作为 props.render(state) 方法的参数,暴露到组件外部
render() { return this.props.render(this.state) }
3 使用 props.render() 的返回值
作为要渲染的内容
鼠标当前位置 {mouse.x},{mouse.y}
上面的 render 属性可以使用任意名
函数要是没有返回值,就是直接调用,然后返回了一个 undefined
1 组件内部:
render(){ return this.props.children(this.state)}
2 使用组件:
{({x,y} => 鼠标的位置:{x} {y})}
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
使用高阶组件存在的问题:
原因:
displayName的作用:
设置方式:
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component' }
7 传递props
问题:
原因:
解决方式:
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: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 仅仅是 createElement()
方法的语法糖(简化语法)
JSX 语法被 @babel/preset-react
插件编译为 createElement()
方法
React 元素:是一个对象,用来描述你希望在屏幕上看到的内容
父组件重新渲染时,也会重新渲染子组件
,但只会渲染当前组件子树(当前组件及其所有子组件)
官网原话:大部分情况下,你应该遵循默认行为。
count
/ 列表数据 /loading
等对于需要在多个方法中用到的数据,应该放在 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 内部自动实现了 shouldComponentUpdate
钩子,不需要手动比较
PureComponent 底层源码揭秘:纯组件内部通过分别 对比 前后两次 props
和 state
的值,如果没有改变,返回 false 以告知 React 可以跳过更新。
class Hello extends React.PureComponent{
render(){
return (
<div>纯组件</div>
)
}
}
对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)
最新的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 })
✅数组
this.setState({
list:[...this.state.list, {新数据}]
})
虚拟 DOM本质是保存 DOM 关键信息的 JS 对象(state + JSX)
提高DOM更新的性能,不频繁的操作真实Dom,在内存中找到变化的部分,再更新真实DOM
使用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>
包裹整个应用,一个 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 的
组件
Route(OBJECT)
组件:指定路由展示组件相关信息,相当于 Vue 的
+ 路由配置
path
属性:路由规则
component
属性:展示的组件
推荐写法:通过 component 属性绑定组件,会通过 props 传递路由相关信息给子组件,工作常用
以下写法只负责展示组件,没有传递路由相关 props 给子组件
Route组件写在哪,渲染出来的组件就展示在哪
通常,我们会把
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"
的类名
❗记得 import 再用
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>
src/views/Find.js
<NavLink to='/find/toplist'>排行榜</NavLink>
{/* React的 Route 组件:相当于 Vue 的 + 路由配置 */ }
<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
❗只写与 V5不同的点
提供一个路由出口,满足条件的路由组件会渲染到组件内部,定义 path 和组件之间的关系
<Routes>
<Route/>
<Route/>
</Routes>
用于指定导航链接,完成路由匹配
path 指定匹配的路径地址 element 指定要渲染的组件
navigage('/about,{ replace:true }')
导入useNavigate钩子函数
执行钩子函数得到跳转函数
执行跳转函数完成跳转
✨注:如果在跳转时不想加历史记录,可以添加额外参数 replace 为 true
传参:navigage('/about/?id=100')
取参:
let [params] = useSearchParams()
let id = params.get('id')
传参:navigage('/about/1001')
取参:
let [params] = useParams()
let id = params.get('id')
<Routes>
// 定义嵌套关系
<Route path='/' element={ <Layout /> }>
<Route path='/board' element={ <Board /> } />
<Route/>
</Routes>
指定二级路由出口import { Outlet } from 'react-router-dom'
function Layout (){
return (
<div>
// 二级路由出口
<Outlet />
</div>
)
}
场景:首先渲染完毕就要显示的二级路由
步骤:
<Routes>
<Route path='/' element={ <Layout /> }>
<Route index element={ <Board /> } />
<Route path='/article' element={ <Board /> } />
<Route/>
</Routes>
场景:当所有的路径都没有匹配成功的时候显示
语法说明:在各级路由的最后添加 *号路由
作为兜底
<Routes>
<Route path='/' element={ <Layout /> }>
<Route index element={ <Board /> } />
<Route path='/article' element={ <Article /> } />
<Route path='*' element={ <NotFound /> } />
<Route/>
</Routes>
Hooks的本质:一套能够使
函数组件
更强大,更灵活的“钩子”,react v16.8开始
函数组件
中使用mixin混入的数据来源不清晰,HOC高阶组件的嵌套问题
学习成本高,比如各种生命周期,this指向问题等
替换之前的 state 写法
const [ 访问state的变量名, 设置state的函数名 ] = useState(初始值)
state={ count:0 }
==> const [count,seCount] = useState(0)
this.setState({ count:this.state.count +1 })
==> setCount(count + 1)
useState() 可以调用多次,声明多个状态
const [isShow,setIsShow] = useState(true)
<button onClick={()=> setIsShow(!isShow)}></button>
useState
注意事项let num = 1
function List(){
num++
if(num / 2 === 0){
const [name, setName] = useState('cp')
}
const [list,setList] = useState([])
}
// 俩个hook的顺序不是固定的,这是不可以的!!!
主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用比如:
官方定义: 数据获取,设置订阅,localstorage操作以及手动更改 React 组件中的 DOM 都属于副作用。
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount
这三个函数的组合。
1 导入 useEffect
函数
2 调用 useEffect
函数,并传入回调函数
3 在回调函数中编写副作用处理(dom操作)
4 修改数据状态
5 检测副作用是否生效
useEffect(()=>{
console.log(11111, '没有传入第二个参数', count)
})
用法1:添加空数组
组件只在首次渲染时执行一次
// componentDidMount()
useEffect(() => {
console.log('组件挂载时,运行一次~~')
}, [])
用法2:添加特定依赖项
类似 Vue 的立刻执行的 watch,在首次渲染时执行,在依赖项发生变化时重新执行
// componentDidUpdate()
useEffect(() => {
console.log('count更新时', count)
}, [count])
参数只会在初次渲染中起作用,初始 state 需要通过计算
才能获得,则可以传入一个函数,返回计算后的初始值,供初始渲染的时候用
const [name,setName] = useState(()=>{
// 编写计算逻辑 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() 内部返回的回调函数,会在组件卸载
时自动执行(相当于类组件的 componentWillUnmount)
执行清除操作(如:清理定时器,清理全局注册的事件,清理订阅
等)
useEffect(() => {
// 挂载时运行的代码
console.log('组件挂载!!!')
window.addEventListener('resize', fn)
// ✅ 卸载时运行的代码
return () => {
console.log('组件卸载了~~~')
window.removeEventListener('resize', fn)
}
}, [])
❌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出现
可以在函数组件中获取真实的dom元素对象或者是组件对象
useRef
函数useRef
函数并传入null,返回值为一个对象 内部有一个current属性存放拿到的dom对象(组件实例)import { useEffect, useRef } from 'react'
function App() {
const h1Ref = useRef(null)
useEffect(() => {
console.log(h1Ref)
},[])
return (
this is h1
)
}
export default App
函数组件由于没有实例,不能使用ref获取,如果想获取组件实例,必须是类组件
Foo.js
class Foo extends React.Component {
sayHi = () => {
console.log('say hi')
}
render(){
return Foo
}
}
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 (
)
}
export default App
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
如: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]
})
}
filter
const count = list.filter(item => item.done).length
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'
})
}
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])
高阶组件本质是一个函数,类比高阶组件,入参为组件,返回值也为组件,可以在函数体内给入参组件携带更多参数
如果一个函数符合下面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本质是一个
组件
,将组件的children属性设计为一个函数,在调用函数的同时传入额外参数
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 {this.props.children(this.state)}
}
}
export default WithMouse
import React from 'react'
import WithMouse from './renderProps'
function AComponent({ x, y }) {
return (
x:{x},y:{y}
)
}
class App extends React.Component {
render() {
return (
{(state) => }
)
}
}
export default App
天生为组合而生
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 }
import { useMouse } from './withMouse'
function AComponent() {
const [x, y] = useMouse()
return (
x:{x},y:{y}
)
}
父组件更新会引起子组件也被更新,那么子组件没有任何变化时也会重新渲染
React.memo是一个高阶组件,其实就是一个函数,通过比较**
props的变化
**决定是否重新渲染,如果通过内部的对比发现props并没有变化,则不会重新渲染,从而达到提高渲染性能的目的
1)props不变不重新渲染
import React, { useState } from 'react'
// 通过React.memo包裹子组件
const Son = React.memo(() => {
console.log('我是son组件,我更新了')
return this is son
})
function App() {
const [count, setCount] = useState(0)
return (
)
}
export default App
再次运行发现,子组件只会在首次渲染时执行一次,后续App组件的更新不再影响它
2)props变化重新渲染
import React, { useState } from 'react'
const Son = React.memo(() => {
console.log('我是son组件,我更新了')
return this is son
})
function App() {
const [count, setCount] = useState(0)
return (
)
}
export default App
再次运行发现,子组件会随着count的变化而变化
由于react内部对于复杂类型数据进行的是浅对比,只对比引用,如果引用不同,则代表俩次的props是不一致的,如果不一致,就重新渲染
import React, { useState } from 'react'
const Son = React.memo(() => {
console.log('我是son组件,我更新了')
return this is son
})
function App() {
const [count, setCount] = useState(0)
// 这里我们每次 修改count进行App组件更新 list都会生成一个不同的引用 所以造成子组件又重新渲染了
const list = [1, 2, 3]
return (
)
}
export default App
如果不想使用React.memo内置的对比方式,而是想自己做判断,也是可以的,它为我们开放了对比函数,只需要在memo函数中传入第二个参数即可
import React, { useState } from 'react'
const Son = React.memo(
() => {
console.log('我是son组件,我更新了')
return this is son
},
(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 (
)
}
export default App
作用:缓存一个函数,只在依赖项变化的时候才会更新引用值,经常与React.memo配合使用
未优化版本
我们上一节刚讲过,通过React.memo虽然可以在一定程度上避免子组件务必要的渲染,但是对于引用类型是无效的,那函数也是一个引用类型,且在父传子的时候也是可以把函数传给子组件的,这个时候就可以利用useCallBack和React.memo配合使用
import React, { useState } from 'react'
const Son = React.memo(() => {
console.log('Son组件更新了')
return this is son
})
function App() {
const [count, setCount] = useState(0)
const getName = () => {
return 'this is app'
}
return (
{/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
)
}
export default App
优化版本
import React, { useState, useCallback } from 'react'
const Son = React.memo(() => {
console.log('Son组件更新了')
return this is son
})
function App() {
const [count, setCount] = useState(0)
// 缓存
const getName = useCallback(() => {
return 'this is app'
}, [])
return (
{/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
)
}
export default App
作用:缓存一个函数,该函数只在依赖项变化时才会重新执行此函数,在一些计算量很大的场景中非常有用
错误演示
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 (
{getNums()}
)
}
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 (
{getNums}
)
}
export default App
1 本质是Object类型的对象(一般对象)
2 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。
3 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。
gth
}
)
function App() {
const [count, setCount] = useState(0)
const list = [1, 2, 3]
return (
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 this is son
})
function App() {
const [count, setCount] = useState(0)
const getName = () => {
return 'this is app'
}
return (
{/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
)
}
export default App
优化版本
import React, { useState, useCallback } from 'react'
const Son = React.memo(() => {
console.log('Son组件更新了')
return this is son
})
function App() {
const [count, setCount] = useState(0)
// 缓存
const getName = useCallback(() => {
return 'this is app'
}, [])
return (
{/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
)
}
export default App
作用:缓存一个函数,该函数只在依赖项变化时才会重新执行此函数,在一些计算量很大的场景中非常有用
错误演示
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 (
{getNums()}
)
}
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 (
{getNums}
)
}
export default App
1 本质是Object类型的对象(一般对象)
2 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。
3 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。
对比 初始虚拟 DOM 树 和 更新后的虚拟 DOM 树,找到不同之处,最终,只将不同的地方更新到页面中