第一章 react特点及优势
- 高效的虚拟 DOM (Virtual DOM):React 不直接操作真实 DOM,而是通过虚拟 DOM
并在内存中进行差异计算(Diffing 算法),最后才将变化更新到真实 DOM 上,这极大地提高了渲染性能 - 组件化编码模式 (Component-Based):提倡将 UI
拆分成独立、可复用的组件(包括函数式组件和类式组件),这种模块化的开发方式提高了代码的复用性、可维护性和开发效率 - JSX 语法:React 使用 JSX(JavaScript XML)语法,允许在 JavaScript 中编写类似 HTML
的结构。这种方式让代码结构更加直观,同时也具备 JavaScript 的强大逻辑处理能力 - 声明式编程:开发者只需描述 UI 应该呈现的状态,React 会负责在底层自动处理更新。这种模式简化了复杂的 UI 逻辑
- 单向数据流与状态管理:通过 state(内部状态)和 props(外部属性)来清晰地管理数据流向,确保组件的渲染结果是可预测的
- 强大的生态系统与周边技术:该教程涵盖了从基础到 React Router 6、Redux 状态管理、Ant Design UI
组件库以及各种 Hooks(如 useState, useEffect)的完整生态,体现了 React 处理复杂大型应用的能力
第二章 虚拟DOM的两种创建方式
- 使用jsx创建,可以直接写标签名,不会很繁琐
- 使用js创建,通过React.createElement()创建,在标签嵌套时会很繁琐
- 最后都通过ReactDOM.render渲染
javascript
//使用jsx
//创建虚拟DOM 没有单引号,这不是字符串
const VDOM = (<h1>
<span>hello,react</span>
</h1>)
//渲染虚拟DOM到真实DOM
ReactDOM.render(VDOM, document.getElementById("test"))
//使用js
//创建虚拟DOM
const VDOM = React.createElement("h1", { id: 'title' }, React.createElement("span", null, "hello,react"))
//渲染虚拟DOM到真实DOM
ReactDOM.render(VDOM, document.getElementById("test"))
虚拟DOM
关于虚拟DOM:
- 本质是Object类型的对象(一般对象)
- 虚拟DOM比较"轻",真实D0M比较"重",因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。
- 虚拟以DOM最终会被React转化为真实DOM,呈现在页面上。
jsx语法规则
- 定义虚拟DOM时,不要写引号。
- 标签中混入JS表达式时要用 {}。
- 样式的类名指定不要用 class,要用 className。
- 内联样式,要用 style={{key:value}} 的形式去写。
- 只有一个根标签
- 标签必须闭合
- 标签首字母
(1). 若小写字母开头,则将该标签转为 html 中同名元素,若 html 中无该标签对应的同名元素,则报错。
(2). 若大写字母开头,react 就去渲染对应的组件,若组件没有定义,则报错。
javascript
const myId = 'aTgUiGu'
const myData = 'Hello,rEact'
// 1. 创建虚拟 DOM
const VDOM = (
<div>
<h2 className="title" id={myId.toLowerCase()}>
<span style={{ color: 'white', fontSize: '29px' }}>{myData.toLowerCase()}</span>
</h2>
<h2 className="title" id={myId.toUpperCase()}>
<span style={{ color: 'white', fontSize: '29px' }}>{myData.toLowerCase()}</span>
</h2>
<input type="text" />
<Good>123</Good>
</div>
)
// 2. 渲染虚拟 DOM 到页面
ReactDOM.render(VDOM, document.getElementById('test'))
VDOM规则
在创建虚拟DOM时,里边必须是js表达式,不可以写js语句代码
那么什么是表达式,什么是语句呢?
- 表达式:
一个表达式会产生一个值,可以放在任何一个需要值的地方
下面这些都是表达式:
(1). a
(2). a + b
(3). demo(1)
(4). arr.map()
(5). function test () {} - 语句(代码):
下面这些都是语句(代码):
(1). if() {}
(2). for() {}
(3). switch() {case:xxxx}
javascript
// 模拟一些数据
const data = ['Angular', 'React', 'Vue']
// 1. 创建虚拟DOM
const VDOM = (
<div>
<h1>前端js框架列表</h1>
<ul>
{
data.map((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
</div>
)
// 2. 渲染虚拟DOM到页面
ReactDOM.render(VDOM, document.getElementById('test'))
第三章 创建组件的两种方式
使用函数式组件
执行了ReactDOM.render(<MyComponent./>,之后,发生了什么?
- React解析组件标签,找到了MyComponent组件。
- 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
javascript
function MyComponent() {
console.log(this)//这里的this是undefined,因为babel编译后开启了严格模式,严格模式下,不允许this指向window
return <h1>我是函数定义的组件,适用于【简单组件】的定义</h1>
}
//渲染虚拟DOM到真实DOM
ReactDOM.render(<MyComponent />, document.getElementById("test"))
使用类式组件
执行了ReactDOM.render(< MyComponent/>...,之后,发生了什么?
- React解析组件标签,找到了MyComponent组件。
- 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法。
- 将render返回的虚拟DOM转为真实DOM,随后呈现在页而中。
javascript
class MyComponent extends React.Component {
render() {
//render是放在哪里的?一MyComponent的原型对象上,供实例使用。
//render中的this是谁?一yComponent的实例对象<=>MyComponent组件实例对象。
console.log(this)
return <h1>我是类定义的组件,适用于【复杂组件】的定义</h1>
}
}
//渲染虚拟DOM到真实DOM
ReactDOM.render(<MyComponent />, document.getElementById("test"))
第四章 组件实例三大属性
state
- state 是组件对象最重要的属性,值是对象(可以包含多个 key-value 的组合)
- 组件被称为"状态机",通过更新组件的 state 来更新对应的页面显示(重新渲染组件)
注意:
-
组件中 render 方法中的 this 为组件实例对象
-
组件自定义的方法中 this 为 undefined,如何解决?
- 强制绑定 this: 通过函数对象的 bind()
- 箭头函数
- 状态数据,不能直接修改或更新
javascript
//state的基本使用
class Weather extends React.Component {
//构造器只有在初始化实例时调用一次,其他时候不会调用
constructor(props) {
super(props)
//修改状态
this.state = { isHot: false }
//解决changeWeather方法中的this指向问题
//这行代码是沿着实例的原型链找到changeWeather方法,绑定到实例上,返回一个新的函数之后,再赋值给该实例的changeWeather属性
this.changeWeather = this.changeWeather.bind(this)
}
//render调用1+n次,n为状态改变的次数
render() {
return <h1 onClick={this.changeWeather}>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h1>
}
//点击几次调用几次
changeWeather() {
//changeWeather放在了weather的原型对象上,供实例调用
//但changeWeather是通过onclick调用的,所以不是通过实例调用的,是直接调用
//类中的方法默认开启了严格模式,所以changeWeather中的this为undefine
console.log(this)
let isHot = this.state.isHot
//严重注意:状态的修改不能直接修改,需要调用setState方法
this.setState({ isHot: !isHot })
}
}
//渲染虚拟DOM到真实DOM
ReactDOM.render(<Weather />, document.getElementById("test"))
//state的精简写法
class Weather extends React.Component {
state = { isHot: false }
render() {
return <h1 onClick={this.changeWeather}>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h1>
}
//自定义方法要写成赋值语句+箭头函数
changeWeather = () => {
console.log(this)
let isHot = this.state.isHot
this.setState({ isHot: !isHot })
}
}
ReactDOM.render(<Weather />, document.getElementById("test"))
props
- 通过标签属性从组件外向组件内传递变化的数据
- 注意:组件内部不要修改 props 数据
内部读取某个属性值
对 props 中的属性值进行类型限制和必要性限制
第一种方式(React v15.5 开始已弃用):
Person.propTypes = { name: React.PropTypes.string.isRequired, age: React.PropTypes.number }
第二种方式(新):
使用 prop-types 库进限制(需要引入 prop-types 库)
Person.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number. }
扩展属性:将对象的所有属性通过 props 传递
<Person {...person}/>
默认属性值
Person.defaultProps = { age: 18, sex:'男' }
javascript
class Person extends React.Component {
//构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
constructor(props) {
super(props)
console.log('constructor', this.props)
}
//对标签属性进行类型、必要性的限制
//在Person类上加方法
static propTypes = {//React上的静态属性,React 组件在运行时(开发环境下)会检查这个对象,只要组件被调用,就会自动读取这个静态对象
name: PropTypes.string.isRequired,//这是利用PropTypes库对标签属性进行限制
sex: PropTypes.string,
age: PropTypes.number,
speak: PropTypes.func,
}
//制定默认标签属性值
static defaultProps = {
sex: "男",
age: 18,
}
render() {
//props是只读的,不可以修改
const { name, sex, age } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age + 1}</li>
</ul>
)
console.log(this)
}
}
const p = { name: "张三", sex: "男", age: 18 }
console.log(...p)//在类方法中,不允许用此方法展开对象,因为在js中,对象是不可以迭代的
ReactDOM.render(<Person name="李四" sex="男" age={18} speak={speak} />, document.getElementById("test2"))
ReactDOM.render(<Person name="王五" sex="女" age={30} />, document.getElementById("test3"))
ReactDOM.render(<Person {...p} />, document.getElementById("test"))
//注意:这里可以用...p展开对象,是因为react对此做了处理,而不是因为带了{}
// "{}"表达的只是这里是js表达式
function speak() {
console.log("我说话了")
}
refs
用于直接操作DOM和组件实例
- 字符串形式的ref 已经过时了
javascript
class Demo extends React.Component {
showData = () => {
const { input1 } = this.refs
alert(input1.value)
}
showData2 = () => {
const { input2 } = this.refs
alert(input2.value)
}
render() {
return (
<div>
<input type="text" ref="input1" placeholder="点击" />
<button onClick={this.showData}>点击点击</button>
<input onBlur={this.showData2} type="text" ref="input2" placeholder="失去焦点" />
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById("test"))
- 回调形式的ref
javascript
class Demo extends React.Component {
showData = () => {
const { input1 } = this
alert(input1.value)
}
showData2 = () => {
const { input2 } = this
alert(input2.value)
}
render() {
return (
<div>
<input type="text" ref={(c) => { this.input1 = c }} placeholder="点击" />
<button onClick={this.showData}>点击点击</button>
<input onBlur={this.showData2} type="text" ref={(c) => { this.input2 = c }} placeholder="失去焦点" />
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById("test"))
在页面更新时,内联回调会被调用两次,第一次是初始化(传入的参数为null),第二次是更新(传入的参数是结点),解决上方问题,用class的绑定函数
javascript
class Demo extends React.Component {
changeWeather = () => {
//获取原来的状态
const { isHot } = this.state
//更新状态
this.setState({ isHot: !isHot })
}
saveInput = (c) => {
this.input1 = c;
console.log('@', c);
}
showData = () => {
const { input1 } = this
alert(input1.value)
}
showData2 = () => {
const { input2 } = this
alert(input2.value)
}
render() {
const { isHot } = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
{/*在页面更新时,内联回调会被调用两次,第一次是初始化,第二次是更新,解决上方问题,用class的绑定函数*/}
{/*<input ref={(c) => { this.input1 = c; console.log('@', c); }} type="text"/> <br/><br/>*/}
<input ref={this.saveInput} type="text" /> <br /><br />
<button onClick={this.showInfo}>点我提示输入的数据</button>
<button onClick={this.changeWeather}>点我切换天气</button>
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById("test"))
- createRef
javascript
class Demo extends React.Component {
Myref = React.createRef()
Myref2 = React.createRef()
showData = () => {
alert(this.Myref.current.value)
}
showData2 = () => {
alert(this.Myref2.current.value)
}
render() {
return (
<div>
<input type="text" ref={this.Myref} placeholder="点击" />
<button onClick={this.showData}>点击点击</button>
<input onBlur={this.showData2} type="text" ref={this.Myref2} placeholder="失去焦点" />
</div>
)
}
}
ReactDOM.render(<Demo />, document.getElementById("test"))
事件处理
- 通过onXxx属性指定事件处理函数(注意大小写)
- React使用的是自定义(合成)事件,而不是使用的原生DOM事件 ------ 为了更好的兼容性
- React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) ------ 为了的高效
- 通过event.target得到发生事件的DOM元素对象
防止过度的使用ref,例如上述失去焦点的案例,可以不使用ref,通过event.target实现功能,但这种只能是发生时间的元素和操作的元素是同一个元素时,否则使用受控组件中的方法,后续会有。
第五章 收集表单数据
非受控组件
一切输入类组件,现用现取
javascript
//创建组件
class Login extends React.Component {
handleSubmit = (event) => {
event.preventDefault() //阻止表单提交
const { username, password } = this
alert(`你输入的用户名是:${username.value},你输入的密码是:${password.value}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input ref={c => this.username = c} type="text" name="username" />
密码:<input ref={c => this.password = c} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
//渲染组件
ReactDOM.render(<Login />, document.getElementById('test'))
受控组件
一切输入类组件,随着输入维护状态
javascript
//创建组件
class Login extends React.Component {
state = {
usename: '',
password: ''
}
saveUsername = (event) => {
this.setState({
usename: event.target.value
})
}
savePassword = (event) => {
this.setState({
password: event.target.value
})
}
handleSubmit = (event) => {
event.preventDefault() //阻止表单提交
// const { username, password } = this
alert(`你输入的用户名是:${username.value},你输入的密码是:${password.value}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" name="username" />
密码:<input onChange={this.savePassword} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
//渲染组件
ReactDOM.render(<Login />, document.getElementById('test'))
高阶函数和函数柯理化
高阶函数 :如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
常见的高阶函数有:Promise、setTimeout、arr.map()
函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
javascript
//创建组件
class Login extends React.Component {
state = {
usename: '',
password: ''
}
//柯理化函数 onchange绑定的事件是saveDate的返回值,因为加了小括号,savaDate已经调用了
saveDate = (dataType) => {
return (event) => {
this.setState({ [dataType]: event.target.value })
}
}
handleSubmit = (event) => {
event.preventDefault() //阻止表单提交
// const { username, password } = this
alert(`你输入的用户名是:${username.value},你输入的密码是:${password.value}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveDate('usename')} type="text" name="username" />
密码:<input onChange={this.saveDate('password')} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
//渲染组件
ReactDOM.render(<Login />, document.getElementById('test'))
不用柯理化实现
javascript
//创建组件
class Login extends React.Component {
state = {
username: '',
password: ''
}
//柯理化函数 onchange绑定的事件是saveDate的返回值,因为加了小括号,savaDate已经调用了
saveDate = (dataType, event) => {
this.setState({ [dataType]: event.target.value })
}
handleSubmit = (event) => {
event.preventDefault() //阻止表单提交
const { username, password } = this.state
alert(`你输入的用户名是:${username},你输入的密码是:${password}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={event => this.saveDate('username', event)} type="text" name="username" />
密码:<input onChange={event => this.saveDate('password', event)} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
//渲染组件
ReactDOM.render(<Login />, document.getElementById('test'))
第六章 组件的生命周期
- 组件从创建到死亡它会经历一些特定的阶段。
- React 组件中包含一系列勾子函数(生命周期回调函数),会在特定的时刻调用。
- 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作。

引出生命周期
javascript
//创建组件
//生命周期回调函数
class Life extends React.Component {
state = { opacity: 1 }
death = () => {
//卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
//组件挂载完毕
componentDidMount() {
this.timer = setInterval(() => {
let { opacity } = this.state
opacity -= 0.1
if (opacity <= 0) {
opacity = 1
}
this.setState({ opacity })
}, 200)
}
//组件将要卸载
componentWillUnmount() {
//清楚定时器
clearInterval(this.timer)
}
//初始化渲染、状态更新之后
render() {
return (
<div>
<h2 style={{ opacity: this.state.opacity }}>React学不会怎么办?</h2>
<button onClick={this.death}>不睡了</button>
</div>
)
}
}
//渲染组件
ReactDOM.render(<Life />, document.getElementById('test'))
生命周期回调函数(生命周期钩子)旧
javascript
//创建组件
//生命周期回调函数
class Life extends React.Component {
constructor(props) {
console.log("count---constructor")
super(props)
this.state = { count: 0 }
}
//加1按钮的回调
add = () => {
//获取原状态
const { count } = this.state
//更新状态
this.setState({ count: count + 1 })
}
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
force = () => {
this.forceUpdate()
}
//组件将要挂载的钩子
componentWillMount() {
console.log('Count---componentWillMount');
}
//组件挂载完毕的钩子
componentDidMount() {
console.log('Count---componentDidMount');
}
//组件将要卸载的钩子
componentWillUnmount() {
console.log("Count-componentWillUnmount")
}
//控制组件更新的阀门
shouldComponentUpdate(nextProps, nextState) {
console.log('Count-shouldComponentUpdate')
return true
}
//组件将要更新的钩子
componentWillUpdate(nextProps, nextState) {
console.log('Count-componentWillUpdate')
}
//组件更新完毕的钩子
componentDidUpdate(prevProps, prevState) {
console.log('Count-componentDidUpdate')
}
render() {
console.log('Count---render');
const { count } = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>点我死亡</button>
<button onClick={this.force}>强制更新</button>
</div>
)
}
}
class A extends React.Component {
//初始化状态
state = { carName: '奔驰' }
changeCar = () => {
this.setState({ carName: '奥拓' })
}
render() {
return (
<div>
<div>我是A组件</div>
<button onClick={this.changeCar}>换车</button>
<B carName={this.state.carName} />
</div>
)
}
}
class B extends React.Component {
//组件将要接收新的props的钩子 第一次不执行
componentWillReceiveProps(props) {
console.log('B---componentWillReceiveProps', props);
}
//控制组件更新的"阀门"
shouldComponentUpdate() {
console.log('B---shouldComponentUpdate');
return true
}
//组件将要更新的钩子
componentWillUpdate() {
console.log('B---componentWillUpdate');
}
//组件更新完毕的钩子
componentDidUpdate() {
console.log('B---componentDidUpdate');
}
render() {
console.log('B---render');
return (
<div>
我是B组件当前车名:{this.props.carName}
</div >
)
}
}
//渲染组件
ReactDOM.render(<A />, document.getElementById('test'))
总结生命周期的三个阶段(旧)
初始化阶段:由 ReactDOM.render()触发---初次渲染
constructor()
componentWillMount()
render()
componentDidMount()===>一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
更新阶段:由组件内部 this.setState()或父组件重新 render 触发
shouldComponentUpdate()
componentWillUpdate()
render()===>必须要用
componentDidUpdate()
卸载组件:由 ReactDOM.unmountComponentAtNode()触发
componentWillUnmount()==>一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
总结生命周期的三个阶段(新)
- 初始化阶段:由ReactDOM.render()触发---初次渲染
constructor()
getDerivedStateFromProps
render()
componentDidMount()
- 更新阶段:由组件内部this.setState()或父组件重新render触发
getDerivedStateFromProps
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate
componentDidUpdate()
卸载组件:由ReactDOM.unmountComponentAtNode()触发
componentWillUnmount()
第六章 DOM的diffing算法
Diffing 算法是 React、Vue 等前端框架中 虚拟 DOM(Virtual DOM)更新机制的核心,用来高效比对新旧虚拟 DOM 树,找出最小变更并映射到真实 DOM 上。
它的核心思路可以简单概括为三点:
- 同层比较,不跨层级:只对比同一层级的节点,一旦发现节点类型不同,直接销毁重建,不再深入比对子节点,降低复杂度。
- 通过 key 精准定位:在列表中,用稳定的 key标识节点,帮助算法复用同类型节点,避免不必要的销毁/创建。
- 最小化更新:只对有变动的节点进行属性更新、文本修改或节点增删,最终批量同步到真实 DOM。
本质上,它是一种 启发式、同层节点的树比对算法(并非传统 Diff 最长公共子序列算法),在"比对速度"和"更新精度"之间做了工程权衡,保证 UI 更新既快又高效。
经典面试题
1). react/vue中的key有什么作用?(key的内部原理是什么?)
2). 为什么遍历列表时,key最好不要用index?
虚拟DOM中key的作用:
- 简单的说:key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用。
- 详细的说:当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
- 旧虚拟DOM中找到了与新虚拟DOM相同的key:
(1). 若虚拟DOM中内容没变,直接使用之前的真实DOM
(2). 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM - 旧虚拟DOM中未找到与新虚拟DOM相同的key:
根据数据创建新的真实DOM,随后渲染到到页面
用index作为key可能会引发的问题:
若对数据进行:逆序添加、逆序删除等破坏顺序操作:
会产生没有必要的真实DOM更新 = = > 界面效果没问题,但效率低
如果结构中还包含输入类的DOM:
会产生错误DOM更新 ==> 界面有问题。
注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
开发中如何选择key?
最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值。
如果确定只是简单的展示数据,用index也是可以的。
第七章 React脚手架
使用 create-react-app 创建 react 应用
xxx 脚手架: 用来帮助程序员快速创建一个基于 xxx 库的模板项目包含了所有需要的配置 (语法检查、jsx 编译、devServer...)下载好了所有相关的依赖可以直接运行一个简单效果
react 提供了一个用于创建 react 项目的脚手架库: create-react-app
项目的整体技术架构为: react + webpack + es6 + eslint
使用脚手架开发的项目的特点: 模块化, 组件化, 工程化
3.1.2. 创建项目并启动
第一步, 全局安装: npm i -g create-react-app
第二步, 切换到想创项目的目录, 使用命令: create-react-app hello-react
第三步, 进入项目文件夹: cd hello-react
第四步, 启动项目: npm start
样式模块化
为了解决样式冲突
命名为index.module.css
引入时使用 import hello from './index.module.css'
使用样式时 {hello.样式名}
功能界面的组件化编码流程(通用)
- 拆分组件: 拆分界面,抽取组件
- 实现静态组件: 使用组件实现静态页面效果
- 实现动态组件:
动态显示初始化数据- 数据类型
- 数据名称
- 保存在哪个组件?
- 交互(从绑定事件监听开始)
todoList 案例
相关知识点
拆分组件、实现静态组件,注意:className、style的写法
动态初始化列表,如何确定将数据放在哪个组件的state中?
------ 某个组件使用:放在其自身的state中
------ 某些组件使用:放在他们共同的父组件state中(官方称此操作为:状态提升)
关于父子之间通信:
1.【父组件】给【子组件】传递数据:通过props传递
2.【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
注意defaultChecked 和 checked的区别,类似的还有:defaultValue 和 value
状态在哪里,操作状态的方法就在哪里
react脚手架配置代理总结
方法一
在package.json中追加如下配置
json
"proxy":"http://localhost:5000"
说明:
- 优点:配置简单,前端请求资源时可以不加任何前缀。
- 缺点:不能配置多个代理。
- 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)
方法二
-
第一步:创建代理配置文件
在src下创建配置文件:src/setupProxy.js -
编写setupProxy.js配置具体代理规则:
jsconst proxy = require('http-proxy-middleware') module.exports = function(app) { app.use( proxy('/api1', { //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000) target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址) changeOrigin: true, //控制服务器接收到的请求头中host字段的值 /* changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000 changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000 changeOrigin默认值为false,但我们一般将changeOrigin值设为true */ pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置) }), proxy('/api2', { target: 'http://localhost:5001', changeOrigin: true, pathRewrite: {'^/api2': ''} }) ) }
说明:
- 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
- 缺点:配置繁琐,前端请求资源时必须加前缀。
第八章 消息订阅与发布
订阅(Subscribe): 订阅者向消息中间件注册自己感兴趣的主题(Topic)或频道(Channel),并绑定一个事件处理回调函数。
发布(Publish): 发布者在特定事件发生时,将包含特定主题的消息推送到消息中间件。
适用于任意组件间通信
优势: 极大地提高了系统的可扩展性(Scalability)和灵活性。新增一个订阅者时,完全不需要修改发布者的任何代码,符合面向对象设计的"开闭原则"。
劣势: 牺牲了确定性。发布者无法实时得知消息是否成功送达、何时被处理;同时,过度依赖发布订阅会导致系统的数据流向变得隐蔽,增加调试和链路追踪的复杂度。
代码体现:
javascript
//订阅消息的组件(需要接收数据)
import { Component } from 'react'
import pubsub from 'pubsub-js'
import './index.css'
export default class List extends Component {
state = {
users: [],
isFirst: true,
isLoading: false,
err: ''
} //初始化
componentDidMount() {
this.token = pubsub.subscribe('search', (msg, stateObj) => {
this.setState(stateObj)
})
}
componentWillUnmount() {
pubsub.unsubscribe(this.token)
}
render() {
const { users, isFirst, isLoading, err } = this.state
return (
<div className="row">
{isFirst ? (
<h2>欢迎使用,输入关键字,随后点击搜索</h2>
) : isLoading ? (
<h2>Loading...</h2>
) : err ? (
<h2>{err}</h2>
) : (
users.map(userObj => {
return (
<div key={userObj.id} className="card">
<a rel="noreferrer" href={userObj.html_url} target="_blank">
<img alt="avatar" src={userObj.avatar_url} style={{ width: '100px' }} />
</a>
<p className="text-muted">{userObj.type}</p>
</div>
)
})
)}
</div>
)
}
}
//发布消息的组件(需要传递消息)
import pubsub from 'pubsub-js'
import { Component } from 'react'
export default class Search extends Component {
search = async () => {
//获取用户输入
const {
keyWordElement: { value: keyWord }
} = this
console.log(keyWord)
pubsub.publish('search', { isFirst: false, isLoading: true })
//发送网络请求
try {
const response = await fetch(`/api1/search/users?q=${keyWord}`)
const data = await response.json()
pubsub.publish('search', { users: data.items, isFirst: false, isLoading: false })
} catch (error) {
console.log('请求出错', error)
pubsub.publish('search', { users: [], isFirst: false, isLoading: false, err: error.message })
}
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">搜索github用户</h3>
<div>
<input ref={c => (this.keyWordElement = c)} type="text" placeholder="输入关键词点击搜索" />
<button onClick={this.search}>搜索</button>
</div>
</section>
)
}
}
Fetch请求数据
fetch 原生函数,不再使用 XmlHttpRequest 对象提交 ajax 请求
老版本浏览器可能不支持
javascript
search = async () => {
//获取用户输入
const {
keyWordElement: { value: keyWord }
} = this
console.log(keyWord)
pubsub.publish('search', { isFirst: false, isLoading: true })
//发送网络请求
try {
const response = await fetch(`/api1/search/users?q=${keyWord}`)
const data = await response.json()
pubsub.publish('search', { users: data.items, isFirst: false, isLoading: false })
} catch (error) {
console.log('请求出错', error)
pubsub.publish('search', { users: [], isFirst: false, isLoading: false, err: error.message })
}
}
第九章 React 路由
SPA 的理解
单页 Web 应用(single page web application, SPA)。
整个应用只有一个完整的页面。
点击页面中的链接不会刷新页面,只会做页面的局部更新。
数据都需要通过 ajax 请求获取,并在前端异步展现。
路由的理解
什么是路由?
一个路由就是一个映射关系(key:value)
key 为路径,value 可能是 function(后端) 或 component(前端)
路由分类
后端路由:
理解:value 是 function, 用来处理客户端提交的请求。
注册路由:router.get(path, function(req, res))
工作过程:当 node 接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
前端路由:
浏览器端路由,value 是 component,用于展示页面内容。
注册路由:
工作过程:当浏览器的 path 变为/test 时, 当前路由组件就会变为 Test 组件
路由的基本使用
- 明确好界面中的导航区和展示区;
- 导航区的 a 标签改为 Link 标签;
- 展示区编写 Route 标签进行路径匹配;
- app组件标签最外侧包裹一个 BrowserRouter 或 HashRouter。
路由组件与一般组件的区别:
-
写法不同:
一般组件:< Demo/>
路由组件:< Route path="/demo" component={Demo}/>
-
存放位置不同:
一般组件:components
路由组件:pages
-
接收到的props不同:
一般组件:写组件标签时传递了什么,就能收到什么
路由组件:接收到三个固定的属性
history:
go: f go(n)
goBack: f goBack()
goForward: f goForward()
push: f push(path, state)
replace: f replace(path, state)
location:
pathname: "/about"
search: ""
state: undefined
match:
params: {}
path: "/about"
url: "/about"
NavLink与封装NavLink
NavLink可以实现路由链接的高亮,通过activeClassName指定样式名
标签体内容是一个特殊的标签属性
通过this.props.children可以获取组件标签标签体内容
javascript
//MyNavLink组件
import { Component } from 'react'
import { NavLink } from 'react-router-dom'
export default class MynavLink extends Component {
render() {
/* 接收父组件传递过来的props包括标签体内容children */
console.log(this.props)
return <NavLink activeClassName="active" className="list-group-item " {...this.props} />
}
}
//app.jsx
<MynavLink to="/home" children="Home" />
<MynavLink to="/about" children="About" />
Switch的使用
通常情况下,path和component是一一对应的关系。
Switch可以提高路由匹配效率(单一匹配)。
javascript
<Switch>
<Route path="/atguigu/home" component={Home} />
<Route path="/atguigu/about" component={About} />
</Switch>
样式丢失问题
如果html文件中样式是由相对路径引入的,并且是多级路径,那么在页面刷新时样式会丢失,原因是:
首次访问根路径 http://localhost:3000/,浏览器请求 http://localhost:3000/css/bootstrap.css✅ 正常(public文件夹就是http://localhost:3000)
访问多级路由 http://localhost:3000/user/about,刷新页面时,浏览器误以为当前的基准路径为http://localhost:3000/user/,所以会去请求:
http://localhost:3000/user/css/bootstrap.css❌ 不存在
该请求匹配不到真实文件,React 脚手架开发服务器通常会返回 index.html,导致样式未加载------看起来就像"样式丢了"。
三种解决方法
方法一:改用绝对路径(最常用)
把 href="./css/xxx.css"改成以 /开头的绝对路径:
html
<link rel="stylesheet" href="/css/bootstrap.css">
这样无论路由有几级,浏览器永远从网站根目录 /下请求资源。
方法二:使用 %PUBLIC_URL%(CRA 脚手架专用)
html
<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">
%PUBLIC_URL%在构建时会被替换为 public 目录的绝对路径,同样不受路由层级影响。
方法三:改用 HashRouter
javascript
import { HashRouter } from 'react-router-dom';
HashRouter URL 形如 http://localhost:3000/#/user/about,#后的内容不参与服务器请求,刷新时静态资源路径永远是根路径。缺点是 URL 带 #,对 SEO 不友好,一般开发环境图省事可用。
路由的严格匹配与模糊匹配
默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
开启严格匹配:
严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
Redirect的使用
一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由
具体编码
javascript
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home} />
<Redirect to="/about"/>
</Switch>
嵌套路由
注册子路由时要写上父路由的path值
路由的匹配是按照注册路由的顺序进行的,先匹配一级路由,在匹配二级路由
注意:开启严格模式exact会导致一级组件显示不出来
路由组件的参数传递
- params参数
分为三步:首先,在导航链接中传递参数,其次在路由组件中生命接收,最后在组件中通过 this.props.match.params 获取
路由链接(携带参数): <Link to="/demo/test/tom/18" }>详情
注册路由(声明接收):
接收参数: const {id,title} = this.props.match.params
javascript
//message 父组件传递参数
import { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import Detail from './Detail'
export default class Message extends Component {
state = {
msgListArr: [
{
id: 1,
name: 'message001'
},
{
id: 2,
name: 'message002'
},
{
id: 3,
name: 'message003'
}
]
}
render() {
const { msgListArr } = this.state
return (
<div>
<ul>
{msgListArr.map(item => {
return (
<li key={item.id}>
<Link to={`/home/message/detail/${item.id}/${item.name}`}>{item.name}</Link>
</li>
)
})}
</ul>
<hr />
<Route path="/home/message/detail/:id/:title" component={Detail}></Route>
</div>
)
}
}
//Detail 子组件接收参数
import { Component } from 'react'
const DetailData = [
{ id: '01', content: '你好,中国' },
{ id: '02', content: '你好,尚硅谷' },
{ id: '03', content: '你好,未来的自己' }
]
export default class Detail extends Component {
render() {
/* 接收params参数 */
const { id, title } = this.props.match.params
const findResult = DeatilData.find(detailObj => {
return detailObj.id === id
})
return (
<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>
)
}
}
- search参数
路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'}>详情
注册路由(无需声明,正常注册即可):
接收参数:const {search} = this.props.location
备注:获取到的 search是 urlencoded 编码字符串,需要借助 querystring解析
javascript
{/* 向路由组件传递search参数 */}
<Link to={`/home/message/detail/?id=${item.id}&title=${item.name}`}>{item.name}</Link>
{/* search无需声明接收 */}
<Route path="/home/message/detail/" component={Detail}></Route>
//Deatil组件接收search参数
const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1))
- state参数
路由链接(携带参数):<Link to={{path:'/demo/test',state:{name:'tom',age:18}}}>详情
注册路由(无需声明,正常注册即可):
接收参数:this.props.location.state
备注:刷新也可以保留住参数
javascript
{/* 向路由组件传递state参数 */}
<Link to={{ pathname: '/home/message/detail', state: { id: item.id, title: item.name } }}>{item.name}</Link>
{/* state参数无需声明接收 */}
<Route path="/home/message/detail/" component={Detail}></Route>
//接收state参数
const { id, title } = this.props.location.state || {}
push模式与replace模式
javascript
<MynavLink replace to="/home/news" children="News" />
编程式路由导航
上方都是通过路由链接Link 或者 NavLink实现路由跳转,下面我们来看一下通过编程式路由导航实现的路由跳转,依旧能实现携带三种参数和两种模式
借助this.prosp.history对象上的API对操作路由跳转、前进、后退
javascript
-this.prosp.history.push()
-this.prosp.history.replace()
-this.prosp.history.goBack()
-this.prosp.history.goForward()
-this.prosp.history.go()
withRouter的使用
用withRouter加工一般组件,让一般组件具备路由组件所特有的API
withRouter的返回值是一个新组件
javascript
export default withRouter(Header)
BrowserRouter和hashRouter的区别
- 底层原理不一样:
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值。
- url表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test
- 刷新后对路由state参数的影响
-
BrowserRouter没有任何影响,因为state保存在history对象中。
-
HashRouter刷新后会导致路由state参数的丢失。
- 备注:HashRouter可以用于解决一些路径错误相关的问题。例如前边提到的样式丢失
第十章 React UI组件库
自定义主题
javascript
//index.js
import React from 'react'; // 确保引入了 React
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd'; // 1. 引入配置组件
import App from './App.jsx';
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
// 2. 用 ConfigProvider 包裹住你的根组件 App
<ConfigProvider
theme={{
token: {
colorPrimary: '#ff5500', // 在这里修改全局主题色(比如改成橙色)
borderRadius: 6, // 在这里修改全局组件的圆角
},
}}
>
<App />
</ConfigProvider>
)
第十一章 redux
redux理解
-
redux 是什么
redux 是一个专门用于做状态管理的 JS 库(不是 react 插件库)。
它可以应用在 react, angular, vue 等项目中,但基本与 react 配合使用。
作用:集中式管理 react 应用中多个组件共享的状态。
-
什么情况下需要使用 redux
某个组件的状态,需要让其他组件可以随时拿到(共享)。
一个组件需要改变另一个组件的状态(通信)。
总体原则:能不用就不用 如果不用比较吃力才考虑使用。
redux的三个核心概念

redux的核心API
- action
动作的对象
包含 2 个属性
type:标识属性, 值为字符串, 唯一, 必要属性
data:数据属性, 值类型任意, 可选属性
例子:{ type: 'ADD_STUDENT',data:{name: 'tom',age:18} }
-
reducer
用于初始化状态、加工状态。
加工时,根据旧的 state 和 action,产生新的 state 的纯函数。
-
store
将 state、action、reducer 联系在一起的对象
如何得到此对象?
javascript
import {createStore} from 'redux'
import reducer from './reducers'
const store = createStore(reducer)
此对象的功能?
javascript
getState(): 得到 state
dispatch(action): 分发 action, 触发 reducer 调用, 产生新的 state
subscribe(listener): 注册监听, 当产生了新的 state 时, 自动调用
redux 求和案例
求和案例_redux精简版
-
去除Count组件自身的状态
-
src下建立:
javascript
-redux
-store.js
-count_reducer.js
-
store.js:
引入redux中的 createStore 函数,创建一个store
createStore 调用时要传入一个为其服务的reducer
记得暴露store对象
-
count_reducer.js:
reducer的本质是一个函数,接收:preState, action,返回加工后的状态
reducer有两个作用:初始化状态,加工状态
reducer被第一次调用时,是store自动触发的,
传递的preState是undefined,
传递的action是:{type:'@@REDUX/INIT_a.2.b.4'}
-
在 index.js 中检测store中状态的改变,一旦发生改变重新渲染
备注: redux只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。
详细代码见
https://gitee.com/jing-xinlu/total_/tree/master/18_React
求和案例_redux异步action版
-
明确: 延迟的动作不想交给组件自身,想交给action
-
何时需要异步action: 想要对状态进行操作,但是具体的数据靠异步任务返回(非必须)。
-
具体编码:
-
yarn add redux-thunk,并配置在store中
-
创建action的函数不再返回一般对象,而是一个函数,该函数中写异步任务。
-
异步任务有结果后,分发一个同步的action去真正操作数据。
- 备注: 异步action不是必须要写的,完全可以自己等待异步任务的结果了再去分发同步action。
javascript
/*
该文件专门用于暴露一个store对象,整个应用只有一个store
*/
//引入createStore,专门用于创建redux中最为核心的store对象
import { createStore, applyMiddleware } from 'redux'
//引入为count组件服务的reducer
import countReducer from './count_reducer'
//引入redux-thunk,用于支持异步action
import { thunk } from 'redux-thunk'
//创建并暴露store对象
export default createStore(countReducer, applyMiddleware(thunk))
//创建action
import { DECREMENT, INCREMENT } from "./constant"
// import store from './store'
//同步action就是指action的值为Object类型的一般对象
export const createIncrementAction = (data) => ({ type: INCREMENT, data })
export const createDecrementAction = (data) => ({ type: DECREMENT, data })
//异步action就是指action的值为函数,异步action中一般都会调用同步action,且异步action不是必须要用的
//这个返回的函数是store调用的,但store只接收action对象,所以会用到同步action
export const createIncrementAsyncAction = (data, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data))
}, time)
}
}
react-reducx模型

求和案例react-redux基本使用
- 明确两个概念:
1).UI组件:不能使用任何redux的api,只负责页面的呈现、交互等。
2).容器组件:负责和redux通信,将结果交给UI组件。 - 如何创建一个容器组件一靠react-redux的connect函数
connect(mapstateToProps,mapDispatchToProps)
-mapStateToProps:映射状态,返回值是一个对象
-mapDispatchToProps:映射操作状态的方法,返回值是一个对象 - 备注1:容器组件中的store,是靠props传进去的,而不是在容器组件中直接引入
- 备注2:mapDispatchToProps也可以是一个对象,会自动分发action
求和案例_react-redux优化
(1). 容器组件和UI组件整合为一个文件
(2). 无需自己给容器组件传递store,给 包裹一个 即可。
(3). 使用了react-redux后也不用再自己检测redux中状态的改变了,容器组件可以自动完成这个工作。
(4). mapDispatchToProps也可以简单的写成一个对象
(5). 一个组件要和Redux"打交道"要经过哪几步?
-
定义好UI组件---不暴露
-
引入connect生成一个容器组件,并暴露,写法如下:
javascript
connect(
state => ({key:value}),
{key:xxxxxAction}
)(UI组件)
- 在UI组件中通过 this.props.xxxxxxx 读取和操作状态
纯函数
一类特殊的函数: 只要是同样的输入(实参),必定得到同样的输出(返回)
必须遵守以下一些约束
-
不得改写参数数据
-
不会产生任何副作用,例如网络请求,输入和输出设备
-
不能调用 Date.now() 或者 Math.random() 等不纯的方法
redux 的 reducer 函数必须是一个纯函数
react-redux开发者工具的使用
(1). yarn add redux-devtools-extension
(2). store中进行配置
javascript
import { composeWithDevTools } from 'redux-devtools-extension'
const store = createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
reducers文件夹中,编写index.js专门用于汇总并暴露所有的reducer
第十二章 react的扩展
1. setState
setState更新状态的2种写法
(1). setState(stateChange, callback)------对象式的setState
stateChange为状态改变对象(该对象可以体现出状态的更改)
callback是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用
(2). setState(updater, callback)------函数式的setState
updater为返回stateChange对象的函数。
updater可以接收到state和props。
callback是可选的回调函数,它在状态更新、界面也更新后(render调用后)才被调用。
总结:
对象式的setState是函数式的setState的简写方式(语法糖)
使用原则:
(1). 如果新状态不依赖于原状态 ===> 使用对象方式
(2). 如果新状态依赖于原状态 ===> 使用函数方式
(3). 如果需要在setState()执行后获取最新的状态数据,要在第二个callback函数中读取
javascript
add = ()=>{
//对象式的setState
/* //1.获取原来的count值
const {count} = this.state
//2.更新状态
this.setState({count:count+1},()=>{
console.log(this.state.count);
})
//console.log('12行的输出',this.state.count); //0 */
//函数式的setState
this.setState( state => ({count:state.count+1}),()=>{
console.log(this.state.count);
})
}
2.lazyLoad懒加载
一般用于路由组件,在点击路由链接时才去请求该组件,而不是一开始就全部请求完。
用法:
- 需要懒加载的用lazy函数引入
- 注册路由时外层用suspense包裹
javascript
const Home = lazy(()=> import('./Home') )
const About = lazy(()=> import('./About'))
<div className="panel-body">
<Suspense fallback={<h1>Loading......</h1>}>
{/* 注册路由 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</Suspense>
</div>
3. Hooks
-
React Hook/Hooks是什么?
(1). Hook是React 16.8.0版本增加的新特性/新语法
(2). 可以让你在函数组件中使用 state 以及其他的 React 特性
-
三个常用的Hook
(1). State Hook: React.useState()
(2). Effect Hook: React.useEffect()
(3). Ref Hook: React.useRef()
- State Hook
(1). State Hook让函数组件也可以有state状态,并进行状态数据的读写操作
(2). 语法: const xxx, setXxx = React.useState(initValue)
(3). useState()说明:
参数: 第一次初始化指定的值在内部作缓存
返回值: 包含2个元素的数组,第1个为内部当前状态值,第2个为更新状态值的函数xxx,setXxx
(4). setXxx()2种写法:
setXxx(newValue): 参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值
setXxx(value => newValue): 参数为函数,接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值
javascript
function Demo(){
console.log('Demo');
const [count,setCount] = React.useState(0)
const [name,setName] = React.useState('tom')
//加的回调
function add(){
//setCount(count+1) //第一种写法
setCount(count => count+1 )
}
function changeName(){
setName('jack')
}
return (
<div>
<h2>当前求和为: {count}</h2>
<h2>我的名字是: {name}</h2>
<button onClick={add}>点我+1</button>
<button onClick={changeName}>点我改名</button>
</div>
)
}
export default Demo
- Effect Hook
(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
(2). React中的副作用操作:
发ajax请求数据获取
设置订阅 / 启动定时器
手动更改真实DOM
(3). 语法和说明:
javascript
useEffect(() => {
// 在此可以执行任何带副作用操作
return () => { // 在组件卸载前执行
// 在此做一些收尾工作,比如清除定时器/取消订阅等
}
}, [stateValue]) // 如果指定的是[],回调函数只会在第一次render()后执行
(4). 可以把 useEffect Hook 看做如下三个函数的组合:
componentDidMount()
componentDidUpdate()
componentWillUnmount():返回值相当于这个钩子
stateValue的对应取值:
- Ref Hook
(1). Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据
(2). 语法: const refContainer = useRef()
(3). 作用:保存标签对象,功能与React.createRef()一样
javascript
const myRef=React.useRef()
<input type="text" ref={myRef}/>
- useReducer
javascript
const [state, dispatch] = useReducer(reducer, initialState)
state:当前状态(类似 useState 里的 state)
dispatch:发送"动作"的函数
reducer:一个纯函数,决定状态怎么变
initialState:初始状态
示例:计数器
javascript
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
return state
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 })
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
- useMemo 是干嘛的?
useMemo用来记住一个计算结果,避免在组件每次渲染时都重复执行开销较大的计算。它适合那种"根据某些数据算出一个新值,而这个计算过程比较慢,或者依赖很少变化"的场景。你可以把它理解成一个带缓存的计算器:只要依赖没变,它就直接把上次算好的结果拿回来用,不再重新算一遍。
基本用法
javascript
const value = useMemo(() => computeExpensiveValue(a, b), [a, b])
第一个参数(函数):你要执行的"计算逻辑",这个函数里返回的就是你要记住的结果。
第二个参数(依赖数组):告诉 React,"这些变量不变,我就不重新计算"。如果依赖没变,useMemo会直接返回上一次的计算结果。
返回值:就是那个被缓存的计算结果,可以直接拿来渲染或继续参与逻辑。
示例:统计待办事项
javascript
const stats = useMemo(() => {
const total = todos.length
const completed = todos.filter(todo => todo.completed).length
const pending = total - completed
const rate = total === 0 ? 0 : Math.round((completed / total) * 100)
return { total, completed, pending, rate }
}, [todos])
<p>完成率:{stats.rate}%</p>
这里发生的事情是:
初始渲染时,执行统计逻辑,算出 stats
之后只要 todos没变,哪怕组件因为其他原因重新渲染,stats依然是上一次算好的对象
只有 todos变化了,才会重新统计一次
- useCallback
useCallback用来记住一个函数本身,避免在组件每次渲染时都创建一个新的函数。它通常用在"把函数作为 props 传给子组件"的场景。你可以把它理解成一个函数版的 useMemo:只要依赖没变,传给子组件的就是同一个函数,从而避免子组件因为"函数变了"而跟着无谓地重新渲染。
基本用法
javascript
const handleClick = useCallback(() => {
doSomething(a, b)
}, [a, b])
第一个参数(函数):你要记住的那个函数,通常是事件处理函数。
第二个参数(依赖数组):告诉 React,"这些变量不变,我就不创建新的函数"。如果依赖没变,useCallback会一直返回第一次创建的那一个函数。
返回值:就是那个被缓存的函数本身,可以直接传给子组件或其他 Hook。
示例:切换待办状态
javascript
const handleToggle = useCallback((id) => {
dispatch({ type: 'TOGGLE', payload: id })
}, [])
<TodoList onToggle={handleToggle} />
这里发生的事情是:
组件第一次渲染时,创建 handleToggle函数
因为依赖数组是空的,handleToggle永远不会改变
即使父组件不断重新渲染,TodoList收到的 onToggle始终是同一个函数
子组件可以放心使用 React.memo,避免无效渲染
Hook 的本质(很重要)
Hook 是 React 在函数组件里的"能力注入点"

4. Fragment
为了防止外层的div包裹的层级太深
javascript
export default class Demo extends Component {
render() {
return (
//key用于遍历
<Fragment key={1}>
<input type="text"/>
<input type="text"/>
</Fragment>
)
}
}
5. context
一种组件间通信方式,常用于【祖组件】与【后代组件】间通信
使用:
创建Context容器对象:
javascript
const XxxContext = React.createContext()
渲染子组件时,外面包裹xxxContext.Provider,通过value属性给后代组件传递数据:
javascript
<xxxContext.Provider value={数据}>
子组件
</xxxContext.Provider>
后代组件读取数据:
javascript
//第一种方式:仅适用于类组件
static contextType = xxxContext // 声明接收context
this.context // 读取context中的value数据
//第二种方式: 函数组件与类组件都可以
<xxxContext.Consumer>
{
value => ( // value就是context中的value数据
要显示的内容
)
}
</xxxContext.Consumer>
注意
在应用开发中一般不用context,一般都用它的封装react插件
例子:
javascript
//类式组件
class C extends Component {
//声明接收context
static contextType = MyContext
render() {
const {username,age} = this.context
return (
<div className="grand">
<h3>我是C组件</h3>
<h4>我从A组件接收到的用户名:{username},年龄是{age}</h4>
</div>
)
}
}
//函数组件
function C(){
return (
<div className="grand">
<h3>我是C组件</h3>
<h4>我从A组件接收到的用户名:
<Consumer>
{value => `${value.username},年龄是${value.age}`}
</Consumer>
</h4>
</div>
)
}
6. PureComponent
Component的2个问题
只要执行 setState(), 即使不改变状态数据, 组件也会重新 render() ==> 效率低
只当前组件重新 render(), 就会自动重新 render 子组件, 纵使子组件没有用到父组件的任何数据 ==> 效率低
效率高的做法
只有当组件的 state 或 props 数据发生改变时才重新 render()
原因
Component 中的 shouldComponentUpdate() 总是返回 true
解决
办法1:
重写 shouldComponentUpdate() 方法
比较新旧 state 或 props 数据,如果有变化才返回 true, 如果没有返回 false
办法2:
使用 PureComponent
PureComponent 重写了 shouldComponentUpdate(), 只有 state 或 props 数据有变化才返回 true
注意:
只是进行 state 和 props 数据的浅比较,如果只是数据对象内部数据变了,对象地址没变,返回 false
不要直接修改 state 数据,而是要产生新数据
项目中一般使用 PureComponent 来优化
7.render props
如何向组件内部动态传入带内容的结构(标签)?
Vue中:使用 slot 技术,也就是通过组件标签体传入结构
**React中:
使用 children props:通过组件标签体传入结构
使用 render props:通过组件标签属性传入结构,而且可以携带数据,一般用 render 函数属性
- children props
javascript
<A>
<B>xxxx</B>
</A>
{this.props.children}//B组件通过这个属性接收
问题: 如果 B 组件需要 A 组件内的数据, ==> 做不到
- render props
javascript
<A render={(data) => <C data={data}/>}></A>
A组件: {this.props.render(内部state数据)}
C组件: 读取A组件传入的数据显示 {this.props.data}
代码示例
javascript
export default class Parent extends Component {
render() {
return (
<div className="parent">
<h3>我是Parent组件</h3>
<A render={(name)=><B name={name}/>}/>
</div>
)
}
}
class A extends Component {
state = {name:'tom'}
render() {
console.log(this.props);
const {name} = this.state
return (
<div className="a">
<h3>我是A组件</h3>
{this.props.render(name)}
</div>
)
}
}
class B extends Component {
render() {
console.log('B--render');
return (
<div className="b">
<h3>我是B组件,{this.props.name}</h3>
</div>
)
}
}
8. 错误边界
理解:
错误边界(Error boundary): 用来捕获后代组件错误,渲染出备用页面
特点:
只能捕获后代组件生命周期产生的错误 不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
使用方式:
getDerivedStateFromError 配合 componentDidCatch
javascript
// 生命周期函数,一旦后台组件报错,就会触发
static getDerivedStateFromError(error) {
console.log(error);
// 在render之前触发
// 返回新的state
return {
hasError: true,
};
}
componentDidCatch(error, info) {
// 统计页面的错误。发送请求发送到后台去
console.log(error, info);
}
9. 组件通信方式总结
组件间的关系:
- 父子组件
- 兄弟组件(非嵌套组件)
- 祖孙组件(跨级组件)
几种通信方式:
- props:
(1). children props
(2). render props - 消息订阅-发布:
pubs-sub、event等等 - 集中式管理:
redux、dva等等 - conText:
生产者-消费者模式
比较好的搭配方式:
父子组件:props
兄弟组件:消息订阅-发布、集中式管理
祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)**