React学习[三]
- 组件的props
- props的基本使用
- props的特点
- 组件通讯的三种方式
- 父组件传递数据给子组件
- 子组件传递数据给父组件
- 兄弟组件传递
- Context
- props进阶
- children属性
- props校验
- 约束规则
- props的默认值
- 组件的生命周期
- 生命周期三个阶段
- 创建时(挂载阶段)
- 更新时(更新阶段)
- 卸载时
- 不常用的生命周期
- render-props和高阶组件
- render props模式
- 使用步骤
- 演示Mouse组件复用
- children代替render模式
- 代码优化
- 高阶组件
- 使用步骤
- 设置`displayName`
- 传递props
- React原理
- setState()的说明
- 更新数据
- 推荐语法
- 第二个参数
- JSX语法的转换过程
- 组件更新机制
- 组件性能优化
- 减轻state
- 避免不必要的重新渲染
- 纯组件
- 作用及使用
- 实现原理
- 虚拟DOM和Diff算法
- 虚拟 DOM
- Diff算法
- React路由
- React路由介绍
- 路由的基本使用
- 常用组件说明
- 路由执行过程
- 编程式导航
- 默认路由
- 匹配模式
- 模糊匹配模式
- 精准匹配
组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据。在组件化过程中,我们将一个完整的功能拆分成多个组件,以更好的完成整个应用的功能。而在这个过程中,多个组件之间不可避免的要共享某些数据为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯。
组件的props
props的基本使用
组件是封闭的,要接收外部数据应该通过props来实现
props的作用:接收传递给组件的数据
- 传递数据:给组件标签添加属性
- 接收数据:函数组件通过
参数props
接收数据,类组件通过this.props
接收数据
函数组件中
/* props */
// 2接收数据
const Hello = props => {
// props是个对象
console.log(props)
return (
<div>
<h1>props:{props.name}</h1>
</div>
)
}
// 1传递数据
ReactDOM.render(<Hello name="jack" age={19}/>, document.getElementById('root' ))
类组件
class Hello extends React.Component {
render(){
return (
<div>接收到的数据: {this.props.age}</div>
)
}
}
ReactDOM.render(< Hello name="rose" age={19} />, document.getElementById('root'))
props的特点
1.可以给组件传递任意类型的数据
2. props 是只读的对象,只能读取属性的值,无法修改对象
const Hello = props => {
// props是个对象
console.log(props)
props.fn();
// 修改props属性的值,错误演示! props只能读不能修改
props.name = 'tom'
return (
<div >
<h1> props: {props.name} </h1>
</div>
)
}
3.注意:使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取到props!
class Hello extends React.Component {
//推荐将props传递给父类构造函数
constructor(props) {
super(props)
console.log(props)
}
render() {
return <div>接收到的数据: {this.props.age}</div>
}
}
组件通讯的三种方式
父组件传递数据给子组件
- 父组件提供要传递的state数据
- 给子组件标签添加属性,值为state中的数据
- 子组件中通过props接收父组件中传递的数据
class Parent extends React.Component {
state= { lastName:'王'}
render () {
return (
<div>
传递数据给子组件: <Child name={this.state.lastName} />
</div>
)
}
}
子组件
function Child(props) {
return <div>子组件接收到数据: {props.name}</div>
}
子组件传递数据给父组件
利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数
- 父组件提供一个回调函数,用来接收数据
- 将该函数作为属性的值,传递给子组件
- 子组件通过props调用回调函数
- 将子组件的数据作为参数传递给回调函数
class Parent extends React.Component {
state = {
parentMsg: ''
}
// 提供一个回调函数
getChildMsg = (data) => {
console.log('接收到子组件中传递过来的数据:', data)
this.setState({
parentMsg: data
})
}
render() {
return (
<div className='parent' >
父组件:{this.state.parentMsg}
< Child getMsg={this.getChildMsg} />
</div>
)
}
}
// 子组件
class Child extends React.Component {
state = { msg: 'React' }
// 子组件调用父组件中传递过来的回调函数
handleClick = () => {
this.props.getMsg(this.state.msg)
}
render() {
return (
<div className='child'>
子组件:
< button onClick={this.handleClick} > 给父组件传递数据</ button>
</div>
)
}
}
ReactDOM.render(< Parent />, document.getElementById('root'))
兄弟组件传递
- 将
共享状态(数据)
提升到最近的公共父组件中,由公共父组件
管理这个状态 - 思想:
状态提升
- 公共父组件职责:
-
- 提供共享状态;
-
- 提供操作共享状态的方法。
- 要通讯的子组件只需要通过
props
接收状态或操作状态的方法
class Counter extends React.Component {
// 提供共享状态
state = {
count: 0
}
// 提供修改状态的方法
onIncrement = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div >
<Child1 count={this.state.count} />
<Child2 onIncrement={this.onIncrement} />
</div>
)
}
}
// 子组件
const Child1 = props => {
return (
<h1>计数器:{props.count}</h1>
)
}
const Child2 = (props) => {
return (
<button onClick={props.onIncrement}>+1</button>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'))
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"></Provider>
4、调用Consumer组件接收数据。
<Consumer>
{ data => <span>data参数表示接收到的数据 -- {data} </span> }
</Consumer>
/*
context
1.如果两个组件是远方亲戚(比如,嵌套多层)可以使用Context实现组件通讯
2. Context提供 了两个组件: Provider 和Consumer
3. Provider组件 :用来提供数据
4. Consumer组件 :用来消费数据
*/
// 创建context得到两个组件
const { Provider, Consumer } = React.createContext()
class App extends React.Component {
render() {
return (
<Provider value="pink">
<div className="app">
<Node />
</div>
</Provider>
)
}
}
const Node = props => {
return (
<div className="node">
<SubNode />
</div>
)
}
const SubNode = props => {
return (
<div className="subnode">
<Child />
</div>
)
}
const Child = props => {
return (
<div className="child">
<Consumer>
{data => <span>我是子节点 -- {data} </span>}
</Consumer>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
props进阶
children属性
- children属性: 表示组件标签的子节点,当组件标签有子节点时,props就会有该属性
- children属性与普通的props一样,值可以使任意值(文本、react元素、组件、甚至是函数)
/* // children为文本节点
const App = props => {
console.log(props)
return (
<div>
<h1>组件标签的子节点: </h1>
{props.children}
</div>
)
}
ReactDOM.render(<App>我是子节点</App>, document.getElementById('root')) */
// // children为 jsx或react组件
// const Test = () => <button>我是button组件</button>
// const App = props => {
// console.log(props)
// return (
// <div>
// <h1>组件标签的子节点: </h1>
// {props.children}
// </div>
// )
// }
// ReactDOM.render(<App>
// <Test />
// {/* <p>我是子节点,是一个p标签</p> */}
// </App>, document.getElementById('root'))
// children是函数
const App = props => {
console.log(props)
props.children()
return (
<div>
<h1>组件标签的子节点: </h1>
</div>
)
}
ReactDOM.render(<App>
{
() => { console.log('这是一个函数子节点') }
}
</App>, document.getElementById('root'))
props校验
- 对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据,简单来说就是组件调用者可能不知道组件封装着需要什么样的数据
// props校验
// 错误演示
const App = props => {
const arr = props.colors
const lis = arr.map((item, index) => <li key={index}>{item.name}</1i>)
return <ul>{lis}</ul>
}
ReactDOM.render(<App colors={19} />, document.getElementById('root'))
- 如果传入的数据不对,可能会导致报错
- 关键问题:组件的使用者不知道需要传递什么样的数据
- props校验:允许在创建组件的时候,指定props的类型、格式等
- 作用:捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性
App.propTypes = {
colors: Proptypes.array
}
使用步骤
- 安装包
prop-types (yarn add prop-types | npm i props-types)
- 导入prop-types 包
- 使用
组件名.propTypes={}
来给组件的props添加校验规则 - 校验规则通过PropTypes对象来指定
import PropTypes from 'prop-types'
function App (props) {
return (
<h1>Hi, {props. colors}</h1>
)
}
App.propTypes = {
// 约定colers属性为array类型
// 如果类型不对,则报出明确错误,便于分析错误原因
colors: PropTypes.array
}
约束规则
- 常见类型:array、bool、func、number、object、string
- React元素类型:element
- 必填项:isRequired
- 特定结构的对象:shape({ })
// 常见类型
optionalFunc: PropTypes.func,
// 必选
requiredFunc: PropTypes.func.isRequired,
// 特定结构的对象
optionalObjectWithShape: PropTypes.shape ({
color: PropTypes.string,
fontSize: PropTypes.number
})
约束规则大全
props的默认值
场景:分页组件→每页显示条数
作用:给props设置默认值,在未传入props时生效
// props默认值
const App = props => {
console.log(props)
return (
<div>
<h1>此处展示props的默认值: {props.pageSize}</h1>
</div>
)
}
// 添加props默认值
App.defaultProps = {
pageSize: 10
}
// <App pageSize={20} />传值后生效
ReactDOM.render(<App pageSize={20} />, document.getElementById('root'))
组件的生命周期
意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等;
组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程。
生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数。
钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。
只有类组件才有生命周期。
生命周期三个阶段
- 每个阶段的执行时机
- 每个阶段钩子函数的执行顺序
- 每个阶段钩子函数的作用
创建时(挂载阶段)
执行时机:创建组件时(页面加载时)
执行顺序:
class App extends React.Component {
render() {
return (
<div>
<h1>统计豆豆被打的次数: </h1>
<button id="btn">打豆豆</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root')
钩子函数 | 触发时机 | 作用 |
---|---|---|
constructor | 创建组件时,最先执行 | 1、初始化state;2、为事件处理程序绑定this |
render | 每次组件渲染都会触发 | 渲染UI(注意:不能调用setState() ) |
componentDidMount | 组件挂载(完成DOM渲染)后 | 1、发送ajax网络请求,获取远程数据;2、DOM操作 |
class App extends React.Component {
constructor(props) {
super(props)
// 初始化state
this.state = {
count: 0
}
// 处理this指向程序
console.warn('生命周期钩子函数:constructor')
}
componentDidMount() {
const title = document.getElementById('title')
console.log(title);
console.warn('生命周期钩子函数:componentDidMount')
}
render() {
// 错误演示, 不要在render中调用setState方法
/* this.setState({
count: 1
}) */
console.warn('生命周期钩子函数:render')
return (
<div>
<h1 id='title'>统计豆豆被打的次数: </h1>
<button id="btn" >打豆豆</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
更新时(更新阶段)
执行时机:1、seState();2、forceUpdate();3、组件接收到新的props
说明:以上三者任意一种变化,组件就会重新渲染
执行顺序
钩子函数 | 触发时机 | 作用 |
---|---|---|
render | 每次组件渲染都会触发 | 渲染UI(注意:不能调用setState() ) |
componentDidUpdate | 组件更新,完成DOM后 | 1、发送ajax网络请求,获取远程数据;2、DOM操作(注意:如果要setSate() ,必须放在if条件中) |
// 更新时
class App extends React.Component {
constructor(props) {
super(props)
// 初始化state
this.state = {
count: 0
}
// 处理this指向程序
}
// 打豆豆
handleClick = () => {
this.setState({
count: this.state.count + 1
})
// 演示强制更新
// this.forceUpdate()
}
render() {
console.warn('生命周期钩子函数:render')
return (
<div>
<Counter count={this.state.count} />
<button onClick={this.handleClick}>打豆豆</button>
</div>
)
}
}
class Counter extends React.Component {
render() {
console.warn('--子组件--生命周期钩子函数:render')
return (
<div>
<h1 id='title'>统计豆豆被打的次数: {this.props.count}</h1>
</div>
)
}
// 注意:如果要`setSate()`,必须放在if条件中)
// 因为:如果直接调用setState()更新状态,也会导致递归更新
componentDidUpdate(prevProps) {
console.warn('--子组件--生命周期钩子函数:componentDidUpdate')
console.log('上一次的props:', prevProps.count, ',当前的props', this.props.count);
// 做法: 比较更新前后的props是否相同,来决定是否重新渲染组件
if (prevProps.count !== this.props.count) {
this.setState({
// count: this.props.count
// 发送ajax请求
})
}
// 获取DOM
const title = document.getElementById('title')
console.log(title);
}
}
ReactDOM.render(<App />, document.getElementById('root'))
卸载时
执行时机:组件从页面中消失时
钩子函数 | 触发时机 | 作用 |
---|---|---|
componentWillUnmount | 组件卸载,从页面中消失 | 执行清理工作(比如清理定时器等) |
// 卸载时
class App extends React.Component {
constructor(props) {
super(props)
// 初始化state
this.state = {
count: 0
}
// 处理this指向程序
}
// 打豆豆
handleClick = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
{
this.state.count > 3 ? <p>豆豆被打死了~</p> : <Counter count={this.state.count} />
}
<button onClick={this.handleClick}>打豆豆</button>
</div>
)
}
}
class Counter extends React.Component {
componentDidMount() {
// 开启定时器
this.timerId = setInterval(() => {
console.log("定时器正在执行~");
}, 500)
}
render() {
return (
<div>
<h1 id='title'>统计豆豆被打的次数: {this.props.count}</h1>
</div>
)
}
componentWillUnmount() {
console.warn('--子组件--生命周期钩子函数:componentWillUnmount')
clearInterval(this.timerId);// 清理定时器
}
}
ReactDOM.render(<App />, document.getElementById('root'))
不常用的生命周期
getDerivedStateFromProps()
getDerivedStateFromProps
会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容- 不管原因是什么,都会在每次渲染前触发此方法
shouldComponentUpdate()
- 根据
shouldComponentUpdate()
的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染 - 当 props 或 state 发生变化时,
shouldComponentUpdate()
会在渲染执行之前被调用。返回值默认为 true
getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate()
在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给componentDidUpdate()
- 此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等
render-props和高阶组件
处理方式:如果两个组件中的部分功能相似或相同,复用相似的功能(联想函数封装)
复用:1、state;2、操作state的方法(组件状态逻辑)
- 两种方式:
-
- render-props
-
- 高阶组件(HOC)
注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)
- 高阶组件(HOC)
render props模式
如何拿到该组件中复用的state:
- 在使用组件时,添加一个值为函数的prop,通过函数参数来获取(需要组件内部实现)
如何渲染任意的UI
- 使用该函数的返回值作为要渲染的UI内容(需要组件内部实现)
<Mouse render= { (mouse) => {}/>
使用步骤
- 创建Mouse组件 ,在组件中提供复用的状态逻辑代码( 1.状态;2. 操作状态的方法)
- 将要复用的状态作为
props.render(state)
方法的参数,暴露到组件外部 - 使用
props.render()
的返回值作为要渲染的内容
// 创建mouse
class Mouse extends React.Component {
state = {
x: 0,
y: 0
}
// 鼠标移动事件的事件处理程序
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 监听鼠标移动事件
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
render() {
return this.props.render(this.state)
}
}
class App extends React.Component {
render() {
return (
<div >
<h1 > render props模式 </h1>
<Mouse render={(mouse) => {
return (<p>鼠标位置:{mouse.x} {mouse.y}</p>)
}} />
</div>
)
}
}
ReactDOM.render(< App />, document.getElementById('root'))
演示Mouse组件复用
Mouse组件负责:封装复用的状态逻辑代码( 1.状态;2.操作状态的方法)
状态:鼠标坐标(xy)
操作状态的方法:鼠标移动事件
传入的render prop负责:使用复用的状态来渲染UI结构
class Mouse extends React.Component {
state = {
x: 0,
y: 0
}
// 鼠标移动事件的事件处理程序
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 监听鼠标移动事件
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
render() {
return this.props.render(this.state)
}
}
<Mouse render={ (mouse) => <p>鼠标当前位置{mouse.x} ,{mouse.y}</p>}/>
children代替render模式
注意:并不是该模式叫render props就必须使用名为render的prop,实际上可以使用任意名称的prop;
把prop是一个函数并且告诉组件要渲染什么内容的技术叫做:render props模式;
推荐:使用children代替render属性
<Mouse >
{({x, y}) => <p>鼠标的位置是{x},{y}</p> }
</Mouse>
// 组件内部:
this.props.children(this.state)
代码优化
推荐:给render props模式添加props校验;
Mouse.propTypes = {
chidlren: PropTypes.func.isRequired
}
应该在组件卸载时解除mousemove事件绑定;
componentWillUnmount() {
window.removeEventListener ('mousemove',this.handleMouseMove)
}
高阶组件
目的:实现状态逻辑复用;
高阶组件就相当于手机壳,通过包装组件,增强组件功能;
高阶组件(HOC,Higher-Order Component) 是一个函数,接收要包装的组件,返回增强后的组件;
const EnhancedComponent = withHOC(WrappedComponent)
高阶组件内部创建了一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给被包装组件WrappedComponent
//高阶组件内部创建的类组件:
class Mouse extends React.Component {
render(){
return <WrappedComponent {...this.state} />
}
}
使用步骤
- 创建一个函数,名称约定以
with开头
function withMouse(){}
- 指定函数参数,参数应该以
大写字母开头
function withMouse(WrappedComponent){}
- 在函数内部创建一个类组件,
提供复用的状态逻辑代码
,并返回
function withMouse(WrappedComponent){
class Mouse extends React.Component {}
return Mouse
}
- 在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
return <WrappedComponent {...this.state} />
- 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面
// 创建组件
const MousePosition = withMouse(Position)
// 渲染组件
<MousePosition />
- 流程
function withMouse(WrappedComponent) {
// 该组件提供复用的逻辑
class Mouse extends React.Component {
// 鼠标状态
state = {
x: 0,
y: 0
}
// 鼠标移动事件的事件处理程序
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 控制鼠标状态逻辑
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
//
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
return <WrappedComponent {...this.state}></WrappedComponent>
}
}
return Mouse
}
//用来测试高阶组件
const Position = props => (
<p>
鼠标当前位置: (x: {props.x}, y: {props.y})
</p>
)
const MousePosition = withMouse(Position)
class App extends React.Component {
render() {
return (
<div>
<h1>高阶组件</h1>
{/* 渲染组件 */}
< MousePosition></MousePosition>
</div>
)
}
}
ReactDOM.render(< App />, document.getElementById('root'))
设置displayName
使用高阶组件存在的问题:得到两个组件的名称相同
原因:默认情况下,React使用组件名称作为displayName
解决方式:为高阶组件设置displayName
,便于调试时区分不同的组件
displayName的作用:用于设置调试信息(React Developer Tools信息)
设置方式:
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
传递props
问题:如果没有传递props,会导致props丢失问题
解决方式: 渲染WrappedComponent
时,将state
和this.props
一起传递给组件
传递方式:
<WrappedComponent {...this.state} {...this.props} />
React原理
setState()的说明
更新数据
setState()
是异步
更新数据的;
注意:使用该语法时,后面的setState()
不要依赖于前面的setState()
;
可以多次调用setState()
,只会触发一次重新渲染。
class App extends React.Component {
state = { count: 1 }
// 异步更新数据
handleClick = () => {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 打印出来是1
}
render() {
return (
<div>
<h1>计数器:{this.state.count}</h1>
<button onClick={this.handleClick}>+1</button>
</div>
)
}
}
ReactDOM.render(< App />, document.getElementById('root'))
推荐语法
推荐:使用setState((state, props)=>{})
;
参数state:表示最新的state;
参数props:表示最新的props;
class App extends React.Component {
state = { count: 1 }
// 异步更新数据
handleClick = () => {
// 这种语法也是异步更新
this.setState((state, props) => {
return {
count: state.count + 1
}
})
console.log(this.state.count)
}
render() {
return (
<div>
<h1>计数器:{this.state.count}</h1>
<button onClick={this.handleClick}>+1</button>
</div>
)
}
}
ReactDOM.render(< App />, document.getElementById('root'))
第二个参数
场景:在状态更新(页面完成重新渲染)后立即执行某个操作
语法:setState(updater[, callback])
this.setstate(
(state, props)=>{} ,
()=>{
console.log ('这个回调函数会在状态更新后立即执行')
}
)
案例:
class App extends React.Component {
state = { count: 1 }
// 异步更新数据
handleClick = () => {
// 这种语法也是异步更新
this.setState(
(state, props) => {
return {
count: state.count + 1
}
},
() => {
console.log('状态更新完成:', this.state.count);
console.log(document.getElementById('title').innerText);
document.title = '更新后的title为:' + this.state.count
}
)
console.log(this.state.count) // 打印出来是1
}
render() {
return (
<div>
<h1 id='title'>计数器:{this.state.count}</h1>
<button onClick={this.handleClick}>+1</button>
</div>
)
}
}
ReactDOM.render(< App />, document.getElementById('root'))
JSX语法的转换过程
JSX仅仅是createElement()
方法的语法糖(简化语法)
JSX语法被@babel/preset-react
插件编译为createElement()
方法
React元素:是一个对象,用来描述你希望在屏幕上看到的内容
JSX语法
const element =(
<h1 className="greeting">
Hello JSX!
</h1>
)
createElement()
const element = React.createElement(
"h1",
{
className: "greeting"
} ,
'Hello JSX!'
);
React元素
// 注意:这是简化过的结构
const element = {
type: 'h1' ,
props: {
className: 'greeting',
children: 'Hello JSX!'
}
}
组件更新机制
-
setState() 的两个作用:
-
- 修改state
-
- 更新组件UI
过程:父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件以其所有子组件)
// 根组件
class App extends React.Component {
state = {
color: '#369'
}
getColor() {
return Math.floor(Math.random() * 256)
}
changeBG = () => {
this.setState(() => {
return {
color: `rgb(${this.getColor()}, ${this.getColor()}, ${this.getColor()})`
}
})
}
render() {
console.log('根组件')
return (
<div className="app" style={{ backgroundColor: this.state.color }}>
<button onClick={this.changeBG}>根组件 - 切换颜色状态</button>
<div className="app-wrapper">
<Parent1 />
<Parent2 />
</div>
</div>
)
}
}
// ------------------------左侧---------------------------
class Parent1 extends React.Component {
state = {
count: 0
}
handleClick = () => {
this.setState(state => ({ count: state.count + 1 }))
}
render() {
console.log('左侧父组件')
return (
<div className="parent">
<h2>
左侧 - 父组件1
<button onClick={this.handleClick}>点我({this.state.count})</button>
</h2>
<div className="parent-wrapper">
<Child1 />
<Child2 />
</div>
</div>
)
}
}
class Child1 extends React.Component {
render() {
console.log('左侧子组件 - 1')
return <div className="child">子组件1-1</div>
}
}
class Child2 extends React.Component {
render() {
console.log('左侧子组件 - 2')
return <div className="child">子组件1-2</div>
}
}
// ------------------------右侧---------------------------
class Parent2 extends React.Component {
state = {
count: 0
}
handleClick = () => {
this.setState(state => ({ count: state.count + 1 }))
}
render() {
console.log('右侧父组件')
return (
<div className="parent">
<h2>
右侧 - 父组件2
<button onClick={this.handleClick}>点我({this.state.count})</button>
</h2>
<div className="parent-wrapper">
<Child3 />
<Child4 />
</div>
</div>
)
}
}
class Child3 extends React.Component {
render() {
console.log('右侧子组件 - 1')
return <div className="child">子组件2-1</div>
}
}
class Child4 extends React.Component {
render() {
console.log('右侧子组件 - 2')
return <div className="child">子组件2-2 </div>
}
}
ReactDOM.render(<App />, document.getElementById('root'))
组件性能优化
减轻state
只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading等)
注意:不用做渲染的数据不要放在state中
对于这种需要在多个方法中用到的数据,应该放到this中
class Hello extends Component {
componentDidMount() {
// timerId存储到this中,而不是state中
this.timerId = setInterval(() => {}, 2000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
render() { … }
}
避免不必要的重新渲染
- 组件更新机制:父组件更新会引起子组件也被更新,这种思路很清晰
- 问题:子组件没有任何变化时也会重新渲染
- 如果避免不必要的重新渲染?
-
- 解决方式:使用钩子函数
shouldComponentUpdate(nextProps, nextState)
- 解决方式:使用钩子函数
-
-
- 在这个函数中,nextProps和nextState是最新的状态以及属性
-
- 作用:这个函数有返回值,如果返回true,代表需要重新渲染,如果返回false,代表不需要重新渲染
- 触发时机:更新阶段的钩子函数,组件重新渲染前执行
(shouldComponentUpdate -> render)
class Hello extends Component {
shouldComponentUpdate() {
// 根据条件,决定是否重新渲染组件
return false
}
render() {…}
}
随机数案例:利用nextState参数来判断当前组件是否需要更新
// 生成随机数
class App extends React.Component {
state = {
number: 0
}
handleClick = () => {
this.setState(() => {
return {
number: Math.floor(Math.random() * 3)
}
})
}
// 因为两次生成的随机数可能相同,如果相同,此时,不需要重新渲染
shouldComponentUpdate(nextProps, nextState) {
console.log('最新状态:', nextState, ', 当前状态:', this.state)
return nextState.number !== this.state.number
// if (nextState.number !== this.state.number) {
// return true
// }
// return false
// if (nextState.number === this.state.number) {
// return false
// }
// return true
}
render() {
console.log('render')
return (
<div>
<h1>随机数:{this.state.number}</h1>
<button onClick={this.handleClick}>重新生成</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
随机数案例:利用props参数来判断是否需要进行更新
class App extends React.Component {
state = {
number: 0
}
handleClick = () => {
this.setState(() => {
return {
number: Math.floor(Math.random() * 3)
}
})
}
// 因为两次生成的随机数可能相同,如果相同,此时,不需要重新渲染
// shouldComponentUpdate(nextProps, nextState) {
// console.log('最新状态:', nextState, ', 当前状态:', this.state)
// return nextState.number !== this.state.number
// }
render() {
// console.log('render')
return (
<div>
<NumberBox number={this.state.number} />
<button onClick={this.handleClick}>重新生成</button>
</div>
)
}
}
class NumberBox extends React.Component {
shouldComponentUpdate(nextProps) {
console.log('最新props:', nextProps, ', 当前props:', this.props)
// 如果前后两次的number值相同,就返回false,不更新组件
return nextProps.number !== this.props.number
// if (nextProps.number === this.props.number) {
// return false
// }
// return true
}
render() {
console.log('子组件中的render')
return <h1>随机数:{this.props.number}</h1>
}
}
ReactDOM.render(<App />, document.getElementById('root'))
纯组件
作用及使用
纯组件:PureComponent
与 React.Component
功能相似
区别:PureComponent
内部自动实现了 shouldComponentUpdate
钩子,不需要手动比较
原理:纯组件内部通过分别 对比 前后两次 props
和 state
的值,来决定是否重新渲染组件
class Hello extends React.PureComponent {
render() {
return (
<div>纯组件</div>
)
}
}
实现原理
说明:纯组件内部的对比是 shallow compare(浅层对比)
对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)
let number = 0
let newNumber = number
newNumber = 2
console.log(number === newNumber) // false
state = { number: 0 }
setState({
number: Math.floor(Math.random() * 3)
})
// PureComponent内部对比:
最新的state.number === 上一次的state.number // false,重新渲染组件
说明:纯组件内部的对比是 shallow compare(浅层对比)
对于引用类型来说:只比较对象的引用(地址)是否相同
const obj = { number: 0 }
const newObj = obj
newObj.number = 2
console.log(newObj === obj) // true
state = { obj: { number: 0 } }
// 错误做法
state.obj.number = 2
setState({ obj: state.obj })
// PureComponent内部比较:
最新的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算法
React 更新视图的思想是:只要 state 变化就重新渲染视图
特点:思路非常清晰
问题:组件中只有一个 DOM 元素需要更新时,也得把整个组件的内容重新渲染到页面中?理想状态:部分更新
,只更新变化的地方。
虚拟 DOM
本质上就是一个 JS 对象,用来描述你希望在屏幕上看到的内容(UI)
Diff算法
执行过程
- 初次渲染时,React 会根据初始state(Model),创建一个虚拟 DOM 对象(树)。
- 根据虚拟 DOM 生成真正的 DOM,渲染到页面中。
- 当数据变化后(setState()),重新根据新的数据,创建新的虚拟DOM对象(树)。
- 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容。
- 最终,React 只将变化的内容更新(patch)到 DOM 中,重新渲染到页面。
/*
虚拟DOM 和 Diff算法
*/
class App extends React.PureComponent {
state = {
number: 0
}
handleClick = () => {
this.setState(() => {
return {
number: Math.floor(Math.random() * 2)
}
})
}
// render方法调用并不意味着浏览器中的重新渲染!!!
// render方法调用仅仅说明要进行diff
render() {
const el = (
<div>
<h1>随机数:</h1>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>重新生成</button>
</div>
)
console.log(el)
return el
}
}
ReactDOM.render(<App />, document.getElementById('root'))
React路由
React路由介绍
现代的前端应用大多数是SPA(单页应用程序),也就是只有一个HTML页面的应用程序。因为它的用户体验更好、对服务器压力更小,所以更受欢迎。为了有效的使用单个页面来管理多页面的功能,前端路由应运而生。
- 前端路由功能:让用户从一个视图(页面)导航到另一个视图(页面)
- 前端路由是一套映射规则,在React中,是URL路径与组件的对应关系
- 使用React路由简单来说,就是配置路径和组件
路由的基本使用
- 安装:
yarn add react-router-dom
- 如果没有安装yarn工具的,需要先全局安装一下
yarn:npm install -g yarn
- 如果没有安装yarn工具的,需要先全局安装一下
- 导入路由的三个核心组件: Router / Route / Link
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
- 使用Router 组件包裹整个应用
<Router>
<div className="App">
// … 省略页面内容
</div>
</Router>
- 使用Link组件作为导航菜单(路由入口)
<Link to="/first">页面一</Link>
- 使用Route组件配置路由规则和要展示的组件(路由出口)
const First = () => <p>页面一的页面内容</p>
<Router>
<div className="App">
<Link to="/first">页面一</Link>
<Route path="/first" component={First}></Route>
</div>
</Router>
常用组件说明
- Router组件:包裹整个应用,一个React应用只需要使用一次
- 两种常用的Router:
HashRouter
和BrowserRouter
HashRouter
: 使用URL的哈希值实现 (localhost:3000/#/first)- 推荐
BrowserRouter
:使用H5的history API
实现(localhost3000/first)
- 两种常用的Router:
/*
react-router-dom 的基本使用:
1 安装: yarn add react-router-dom
*/
// 2 导入组件:
// import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
import { HashRouter as Router, Route, Link } from 'react-router-dom'
const First = () => <p>页面一的内容</p>
// 3 使用Router组件包裹整个应用
const App = () => (
<Router>
<div>
<h1>React路由基础</h1>
{/* 4 指定路由入口 */}
<Link to="/first">页面一</Link>
{/* 5 指定路由出口 */}
<Route path="/first" component={First} />
</div>
</Router>
)
ReactDOM.render(<App />, document.getElementById('root'))
- Link组件:用于指定导航链接(a标签)
- 最终Link会编译成a标签,而to属性会被编译成 a标签的href属性
// to属性:浏览器地址栏中的pathname(location.pathname)
<Link to="/first">页面一</Link>
- Route组件:指定路由展示组件相关信息
- path属性:路由规则,这里需要跟Link组件里面to属性的值一致
- component属性:展示的组件
- Route写在哪,渲染出来的组件就在哪
<Route path="/first" component={First}></Route>
路由执行过程
- 当我们点击Link组件的时候,修改了浏览器地址栏中的url
- React路由监听地址栏url的变化
- React路由内部遍历所有的Route组件,拿着Route里面path规则与pathname进行匹配
- 当路由规则(path)能够匹配地址栏中的pathname时,就展示该Route组件的内容
/*
路由的执行过程
*/
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
const First = () => <p>页面一的内容</p>
const Home = () => <h2>这是Home组件的内容</h2>
// 使用Router组件包裹整个应用
const App = () => (
<Router>
<div>
<h1>React路由基础</h1>
<div>
{/* 指定路由出口 */}
<Route path="/first" component={First} />
<Route path="/home" component={Home} />
</div>
{/* 指定路由入口 */}
<Link to="/first">页面一</Link>
<br />
<Link to="/home">首页</Link>
</div>
</Router>
)
ReactDOM.render(<App />, document.getElementById('root'))
编程式导航
- 场景:点击登陆按钮,登陆成功后,通过代码跳转到后台首页,如何实现?
- 编程式导航:通过JS代码来实现页面跳转
- history是React路由提供的,用于获取浏览器历史记录的相关信息
- push(path):跳转到某个页面,参数path表示要跳转的路径
- go(n):前进或后退功能,参数n表示前进或后退页面数量
class Login extends Component {
handleLogin = () => {
// ...
this.props.history.push('/home')
}
render() {...省略其他代码}
}
功能演示
/*
编程式导航
*/
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
class Login extends React.Component {
handleLogin = () => {
// 使用编程式导航实现路由跳转
// ...省略其他功能代码
this.props.history.push('/home')
}
render() {
return (
<div>
<p>登录页面:</p>
<button onClick={this.handleLogin}>登录</button>
</div>
)
}
}
const Home = props => {
const handleBack = () => {
// go(-1) 表示返回上一个页面
props.history.go(-1)
}
return (
<div>
<h2>我是后台首页</h2>
<button onClick={handleBack}>返回登录页面按钮</button>
</div>
)
}
const App = () => (
<Router>
<div>
<h1>编程式导航:</h1>
<Link to="/login">去登录页面</Link>
<Route path="/login" component={Login} />
<Route path="/home" component={Home} />
</div>
</Router>
)
ReactDOM.render(<App />, document.getElementById('root'))
默认路由
- 现在的路由都是通过点击导航菜单后展示的,如果进入页面的时候就主动触发路由呢
- 默认路由:表示
进入页面时就会匹配的路由
- 默认路由:只需要把path设置为
/
<Route path="/" component={Home} />
匹配模式
模糊匹配模式
- 当Link组件的to属性值为 ‘/login’ 时候,为什么默认路由也被匹配成功?
- 默认情况下,React路由是
模糊匹配模式
- 模糊匹配规则:只要pathname以
path
开头就会匹配成功
<Link to="/login">登录页面</Link>
<Route path="/" component={Home} /> 匹配成功
- path 代表Route组件的path属性
- pathname 代表Link组件的to属性(也就是 location.pathname)
path | 能够匹配的pathname |
---|---|
/ | 所有 pathname |
/first | /first 或 /first/a 或 /first/a/b/… |
/*
模糊匹配模式
*/
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
const Home = () => <p>进入页面的时候,你能看到我吗?</p>
const Login = () => <p>我是Login组件的内容</p>
const App = () => (
<Router>
<div>
<h1>默认路由</h1>
<Link to="/first/a/b/c">登录页面</Link>
{/* 默认路由 */}
<Route path="/" component={Home} />
<Route path="/first" component={Login} />
</div>
</Router>
)
ReactDOM.render(<App />, document.getElementById('root'))
精准匹配
- 默认路由认可情况下都会展示,如果避免这种问题?
- 给Route组件添加
exact
属性,让其变为精准匹配模式 - 精确匹配:只有当
path
和pathname
完全匹配时才会展示改路由
// 此时,该组件只能匹配 pathname=“/” 这一种情况
<Route exact path="/" component=... />
推荐:给默认路由添加 exact 属性
/*
精准匹配模式
*/
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
const Home = () => <p>进入页面的时候,你能看到我吗?</p>
const Login = () => <p>我是Login组件的内容</p>
const App = () => (
<Router>
<div>
<h1>默认路由</h1>
<ul>
<li>
<Link to="/">首页</Link>
</li>
<li>
<Link to="/login">登录页面</Link>
</li>
</ul>
{/* 默认路由,添加 exact 属性,就会让当前路由变为精确匹配 */}
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
</div>
</Router>
)
ReactDOM.render(<App />, document.getElementById('root'))