React详细笔记

第一章 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的两种创建方式

  1. 使用jsx创建,可以直接写标签名,不会很繁琐
  2. 使用js创建,通过React.createElement()创建,在标签嵌套时会很繁琐
  3. 最后都通过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:

  1. 本质是Object类型的对象(一般对象)
  2. 虚拟DOM比较"轻",真实D0M比较"重",因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。
  3. 虚拟以DOM最终会被React转化为真实DOM,呈现在页面上。

jsx语法规则

  1. 定义虚拟DOM时,不要写引号。
  2. 标签中混入JS表达式时要用 {}。
  3. 样式的类名指定不要用 class,要用 className。
  4. 内联样式,要用 style={{key:value}} 的形式去写。
  5. 只有一个根标签
  6. 标签必须闭合
  7. 标签首字母
    (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. 表达式:
    一个表达式会产生一个值,可以放在任何一个需要值的地方
    下面这些都是表达式:
    (1). a
    (2). a + b
    (3). demo(1)
    (4). arr.map()
    (5). function test () {}
  2. 语句(代码):
    下面这些都是语句(代码):
    (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./>,之后,发生了什么?

  1. React解析组件标签,找到了MyComponent组件。
  2. 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟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/>...,之后,发生了什么?

  1. React解析组件标签,找到了MyComponent组件。
  2. 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法。
  3. 将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

  1. state 是组件对象最重要的属性,值是对象(可以包含多个 key-value 的组合)
  2. 组件被称为"状态机",通过更新组件的 state 来更新对应的页面显示(重新渲染组件)

注意:

  1. 组件中 render 方法中的 this 为组件实例对象

  2. 组件自定义的方法中 this 为 undefined,如何解决?

  • 强制绑定 this: 通过函数对象的 bind()
  • 箭头函数
  1. 状态数据,不能直接修改或更新
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

  1. 通过标签属性从组件外向组件内传递变化的数据
  2. 注意:组件内部不要修改 props 数据

内部读取某个属性值

this.props.name

对 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和组件实例

  1. 字符串形式的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"))
  1. 回调形式的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"))
  1. 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"))

事件处理

  1. 通过onXxx属性指定事件处理函数(注意大小写)
  • React使用的是自定义(合成)事件,而不是使用的原生DOM事件 ------ 为了更好的兼容性
  • React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) ------ 为了的高效
  1. 通过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'))

第六章 组件的生命周期

  1. 组件从创建到死亡它会经历一些特定的阶段。
  2. React 组件中包含一系列勾子函数(生命周期回调函数),会在特定的时刻调用。
  3. 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作。

引出生命周期

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()==>一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

总结生命周期的三个阶段(新)

  1. 初始化阶段:由ReactDOM.render()触发---初次渲染

constructor()

getDerivedStateFromProps

render()

componentDidMount()

  1. 更新阶段:由组件内部this.setState()或父组件重新render触发

getDerivedStateFromProps

shouldComponentUpdate()

render()

getSnapshotBeforeUpdate

componentDidUpdate()

卸载组件:由ReactDOM.unmountComponentAtNode()触发

componentWillUnmount()

第六章 DOM的diffing算法

Diffing 算法是 React、Vue 等前端框架中 虚拟 DOM(Virtual DOM)更新机制的核心,用来高效比对新旧虚拟 DOM 树,找出最小变更并映射到真实 DOM 上。

它的核心思路可以简单概括为三点:

  1. 同层比较,不跨层级:只对比同一层级的节点,一旦发现节点类型不同,直接销毁重建,不再深入比对子节点,降低复杂度。
  2. 通过 key 精准定位:在列表中,用稳定的 key标识节点,帮助算法复用同类型节点,避免不必要的销毁/创建。
  3. 最小化更新:只对有变动的节点进行属性更新、文本修改或节点增删,最终批量同步到真实 DOM。

本质上,它是一种 启发式、同层节点的树比对算法(并非传统 Diff 最长公共子序列算法),在"比对速度"和"更新精度"之间做了工程权衡,保证 UI 更新既快又高效。

经典面试题

1). react/vue中的key有什么作用?(key的内部原理是什么?)

2). 为什么遍历列表时,key最好不要用index?

虚拟DOM中key的作用:

  1. 简单的说:key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用。
  2. 详细的说:当状态中的数据发生变化时,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.样式名}

功能界面的组件化编码流程(通用)

  1. 拆分组件: 拆分界面,抽取组件
  2. 实现静态组件: 使用组件实现静态页面效果
  3. 实现动态组件:
    动态显示初始化数据
    • 数据类型
    • 数据名称
    • 保存在哪个组件?
  4. 交互(从绑定事件监听开始)

todoList 案例

相关知识点

拆分组件、实现静态组件,注意:className、style的写法

动态初始化列表,如何确定将数据放在哪个组件的state中?

------ 某个组件使用:放在其自身的state中

------ 某些组件使用:放在他们共同的父组件state中(官方称此操作为:状态提升)

关于父子之间通信:

1.【父组件】给【子组件】传递数据:通过props传递

2.【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数

注意defaultChecked 和 checked的区别,类似的还有:defaultValue 和 value

状态在哪里,操作状态的方法就在哪里

react脚手架配置代理总结

方法一

在package.json中追加如下配置

json 复制代码
"proxy":"http://localhost:5000"

说明:

  1. 优点:配置简单,前端请求资源时可以不加任何前缀。
  2. 缺点:不能配置多个代理。
  3. 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)

方法二

  1. 第一步:创建代理配置文件

    复制代码
    在src下创建配置文件:src/setupProxy.js
  2. 编写setupProxy.js配置具体代理规则:

    js 复制代码
    const 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': ''}
        })
      )
    }

说明:

  1. 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
  2. 缺点:配置繁琐,前端请求资源时必须加前缀。

第八章 消息订阅与发布

订阅(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="输入关键词点击搜索" />
          &nbsp;<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 组件

路由的基本使用

  1. 明确好界面中的导航区和展示区;
  2. 导航区的 a 标签改为 Link 标签;
  3. 展示区编写 Route 标签进行路径匹配;
  4. app组件标签最外侧包裹一个 BrowserRouter 或 HashRouter。

路由组件与一般组件的区别:

  1. 写法不同:

    一般组件:< Demo/>

    路由组件:< Route path="/demo" component={Demo}/>

  2. 存放位置不同:

    一般组件:components

    路由组件:pages

  3. 接收到的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可以实现路由链接的高亮,通过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会导致一级组件显示不出来

路由组件的参数传递

  1. 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>
                &nbsp;&nbsp;
              </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>
    )
  }
}
  1. 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))
  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的区别

  1. 底层原理不一样:

BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。

HashRouter使用的是URL的哈希值。

  1. url表现形式不一样

BrowserRouter的路径中没有#,例如:localhost:3000/demo/test

HashRouter的路径包含#,例如:localhost:3000/#/demo/test

  1. 刷新后对路由state参数的影响
  • BrowserRouter没有任何影响,因为state保存在history对象中。

  • HashRouter刷新后会导致路由state参数的丢失。

  1. 备注:HashRouter可以用于解决一些路径错误相关的问题。例如前边提到的样式丢失

第十章 React UI组件库

https://ant.design/index-cn

自定义主题

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理解

  1. redux 是什么

    redux 是一个专门用于做状态管理的 JS 库(不是 react 插件库)。

    它可以应用在 react, angular, vue 等项目中,但基本与 react 配合使用。

    作用:集中式管理 react 应用中多个组件共享的状态。

  2. 什么情况下需要使用 redux

    某个组件的状态,需要让其他组件可以随时拿到(共享)。

    一个组件需要改变另一个组件的状态(通信)。

总体原则:能不用就不用 如果不用比较吃力才考虑使用。

redux的三个核心概念

redux的核心API

  1. action
    动作的对象
    包含 2 个属性
    type:标识属性, 值为字符串, 唯一, 必要属性
    data:数据属性, 值类型任意, 可选属性

例子:{ type: 'ADD_STUDENT',data:{name: 'tom',age:18} }

  1. reducer

    用于初始化状态、加工状态。

    加工时,根据旧的 state 和 action,产生新的 state 的纯函数。

  2. 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精简版

  1. 去除Count组件自身的状态

  2. src下建立:

javascript 复制代码
-redux

-store.js

-count_reducer.js
  1. store.js:

    引入redux中的 createStore 函数,创建一个store

    createStore 调用时要传入一个为其服务的reducer

    记得暴露store对象

  2. count_reducer.js:

    reducer的本质是一个函数,接收:preState, action,返回加工后的状态

    reducer有两个作用:初始化状态,加工状态

    reducer被第一次调用时,是store自动触发的,

    传递的preState是undefined,

    传递的action是:{type:'@@REDUX/INIT_a.2.b.4'}

  3. 在 index.js 中检测store中状态的改变,一旦发生改变重新渲染

备注: redux只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。

详细代码见

https://gitee.com/jing-xinlu/total_/tree/master/18_React

求和案例_redux异步action版

  1. 明确: 延迟的动作不想交给组件自身,想交给action

  2. 何时需要异步action: 想要对状态进行操作,但是具体的数据靠异步任务返回(非必须)。

  3. 具体编码:

  • yarn add redux-thunk,并配置在store中

  • 创建action的函数不再返回一般对象,而是一个函数,该函数中写异步任务。

  • 异步任务有结果后,分发一个同步的action去真正操作数据。

  1. 备注: 异步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. 明确两个概念:
    1).UI组件:不能使用任何redux的api,只负责页面的呈现、交互等。
    2).容器组件:负责和redux通信,将结果交给UI组件。
  2. 如何创建一个容器组件一靠react-redux的connect函数
    connect(mapstateToProps,mapDispatchToProps)
    -mapStateToProps:映射状态,返回值是一个对象
    -mapDispatchToProps:映射操作状态的方法,返回值是一个对象
  3. 备注1:容器组件中的store,是靠props传进去的,而不是在容器组件中直接引入
  4. 备注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 读取和操作状态
纯函数

一类特殊的函数: 只要是同样的输入(实参),必定得到同样的输出(返回)

必须遵守以下一些约束

  1. 不得改写参数数据

  2. 不会产生任何副作用,例如网络请求,输入和输出设备

  3. 不能调用 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懒加载

一般用于路由组件,在点击路由链接时才去请求该组件,而不是一开始就全部请求完。

用法:

  1. 需要懒加载的用lazy函数引入
  2. 注册路由时外层用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

  1. React Hook/Hooks是什么?

    (1). Hook是React 16.8.0版本增加的新特性/新语法

    (2). 可以让你在函数组件中使用 state 以及其他的 React 特性

  2. 三个常用的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. 组件通信方式总结

组件间的关系:

  • 父子组件
  • 兄弟组件(非嵌套组件)
  • 祖孙组件(跨级组件)
    几种通信方式:
  1. props:
    (1). children props
    (2). render props
  2. 消息订阅-发布:
    pubs-sub、event等等
  3. 集中式管理:
    redux、dva等等
  4. conText:
    生产者-消费者模式
    比较好的搭配方式:
    父子组件:props
    兄弟组件:消息订阅-发布、集中式管理
    祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)**
相关推荐
安冬的码畜日常1 小时前
【工欲善其事】深入理解 Node.js 带并发上限的异步任务批量执行逻辑
javascript·设计模式·node.js·ai编程·异步编程·并发执行
meilindehuzi_a2 小时前
Vue3组件样式隔离与组件通信实战:从 scoped、props 校验到记事本应用
前端·javascript·vue.js
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-07-28
前端·神经网络·react.js·搜索引擎·前端框架
奋发向前wcx9 小时前
y1,y2总复习笔记10 2026.7.24
笔记·哈希算法·散列表
Highcharts.js10 小时前
教程:基于 React + Highcharts 构建一个单页应用程序、按需数据拉取与图表渲染
开发语言·前端·数据结构·react.js·前端框架·highcharts·页面应用
To_OC10 小时前
我被 useState 坑了两次之后,终于把它的脾气摸透了
前端·javascript·react.js
MuMuMu122310 小时前
智慧监测的“火眼金睛”:当AIoT重构环保监管体系
笔记
浮生望11 小时前
React useState 深度解析:异步更新、闭包陷阱与惰性初始化的底层逻辑
react.js
一只小菜鸡..12 小时前
南京大学 操作系统 (JYY) 学习笔记:进程地址空间与内存“外挂”魔法
笔记·学习