React学习[三]

news/2024/7/15 17:31:54 标签: react.js

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自身特点的编码技巧,演化而成的固定模式(写法)

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} />
	}
}

使用步骤

  1. 创建一个函数,名称约定以with开头
function withMouse(){}
  1. 指定函数参数,参数应该以大写字母开头
function withMouse(WrappedComponent){}
  1. 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
function withMouse(WrappedComponent){
	class Mouse extends React.Component {}
	return Mouse
}
  1. 在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
return <WrappedComponent {...this.state} />
  1. 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面
// 创建组件
const MousePosition = withMouse(Position)
// 渲染组件
<MousePosition />
  1. 流程
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时,将statethis.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 钩子,不需要手动比较
原理:纯组件内部通过分别 对比 前后两次 propsstate 的值,来决定是否重新渲染组件

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算法

执行过程

  1. 初次渲染时,React 会根据初始state(Model),创建一个虚拟 DOM 对象(树)。
  2. 根据虚拟 DOM 生成真正的 DOM,渲染到页面中。
  3. 当数据变化后(setState()),重新根据新的数据,创建新的虚拟DOM对象(树)。
  4. 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容。
  5. 最终,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
  • 导入路由的三个核心组件: 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: HashRouterBrowserRouter
    • HashRouter: 使用URL的哈希值实现 (localhost:3000/#/first)
    • 推荐 BrowserRouter:使用H5的history API实现(localhost3000/first)
/* 
  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属性,让其变为精准匹配模式
  • 精确匹配:只有当pathpathname完全匹配时才会展示改路由
// 此时,该组件只能匹配 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'))

http://www.niftyadmin.cn/n/458285.html

相关文章

如何挑选合格的在线教育解决方案?

现在市面上的知识付费系统繁多&#xff0c;你可以说百花齐放&#xff0c;也可以说良莠不齐&#xff0c;如果不具备一定的专业素养&#xff0c;根本就无法从中挑选出真正的好产品&#xff0c;劣币驱逐良币反而成为常态。 本文将从几个常见维度分析一个好产品应该具备的基本要素…

【Kubernetes资源篇】StatefulSet无状态服务管理入门实战详解

文章目录 一、StatefulSet理论知识1、StatefulSet Pod控制器特性2、什么是有状态服务和无状态服务&#xff1f;3、Deployment和StatefulSet区别 二、案例&#xff1a;StatefulSet资源实战演示1、创建WEB站点并验证StatefulSet特点2、StatefulSet滚动更新 三、总结 一、Stateful…

通过Python+Selenium查询文章质量分

文章目录 前言一、环境准备二、查询文章质量分1.引入模块2.引入浏览器配置3.设置无头模式4.启动浏览器实例&#xff0c;添加配置信息5.访问质量分地址6.窗口最大化7.定位到输入框并输入博文地址8.定位到查询按钮并点击9.强制等待1s10.获取博文标题11.获取博文发布时间12.获取博…

神经网络:前向传播和后向传播

在计算机视觉中&#xff0c;前向传播和后向传播是神经网络训练的核心步骤&#xff0c;它们的作用、原理和意义如下&#xff1a; 1. 前向传播的作用&#xff1a; 计算预测值&#xff1a;前向传播通过将输入数据在网络中从输入层到输出层进行一系列的运算&#xff0c;计算出网络…

内存不足的原因及解决方法

一、剪贴板占用了太多的内存 实际上&#xff0c;剪贴板是内存中的一块临时区域&#xff0c;当你在程序中使用了“复制”或“剪切”命令后&#xff0c;Windows将把复制或剪切的内容及其格式等信息暂时存储在剪贴板上&#xff0c;以供“粘贴”使用。如果当前剪贴板中存放的是一幅…

css基础(一)

目录 思维导图 ​一、css简介 1.1 css语法规范 1.2 css代码规格 1. 样式格式书写 2. 样式大小写 3. 空格规范 二、css选择器 2.1 CSS 选择器的作用 2.2 选择器分类 2.3 标签选择器 2.4 类选择器 2.4 类选择器-多类名 2.5 id 选择器 2.6 通配符选择器 2.7 基础选择器总结 三、CS…

算法-双指针-秋招算法冲刺

秋招冲刺算法 双指针 数组划分&#xff0c;数组分块 常⻅的双指针有两种形式&#xff0c;⼀种是对撞指针&#xff0c;⼀种是左右指针。 快慢指针 基本思想&#xff1a;使用两个移动速度不同的指针在数组或链表等序列结构上移动。通常处理结构类型&#xff1a;环形链表或数组…

Spring Boot 如何使用 @Validated 注解进行数据校验

Spring Boot 如何使用 Validated 注解进行数据校验 在开发应用程序时&#xff0c;数据校验通常是不可避免的。Spring Boot 提供了许多选项来验证应用程序中的数据&#xff0c;其中一个选项是使用 Validated 注解。本文将介绍如何使用 Validated 注解进行数据校验&#xff0c;并…