四、React组件化开发

124 阅读10分钟

一、React的组件化开发

1. 什么是组件化开发?

  • 组件化是一种分而治之的思想
    • 将一个页面中所有的处理逻辑全部放在一起,处理起来会变得非常复杂,不利于后续的管理以及扩展
    • 将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易
  • 通过组件化思想来思考整个应用程序
    • 将一个完整的页面分成很多个组件
    • 没个组件都用于实现页面的一个功能块
    • 每一个组件又可以进行细分
    • 组件本身又可以在多个地方进行复用

2. React组件化

  • 组件化思想的应用
    • 尽可能将页面拆分成一个个小的、可复用的组件
    • 如此代码更加方便组织和管理,并且扩展性也更强
  • React组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件
    • 根据组件的定义,分为
      • 函数组件(Functional Component)
      • 类组件(Class Component)
    • 根据组件内部是否有状态需要维护,分为
      • 无状态组件(Stateless Compoennt)
      • 容器型组件(Container Component)
    • 根据组件的不同职责,分为
      • 展示型组件(Presentational Component)
      • 容器型组件(Container Component)
  • 关注数据逻辑和UI展示的分离
    • 函数组件、无状态组件、展示型组件主要关注UI展示
    • 类组件、有状态组件、容器型组件主要关注数据逻辑
  • 其它概念:异步组件、高阶组件等

3. 类组件

  • 类组件定义如下
    • 组件名称必须大写字符开头(无论是类组件还是函数组件)
    • 类组件需要继承自 React.Component
    • 类组件必须实现render函数
  • 在ES6之前,可以通过 create-react-class 模块来定义类组件,但是目前官网建议我们使用 ES6 的class类定义
  • 使用class定义一个组件
    • constructor是可选的,通常在constructor中初始化一些数据
    • this.state中维护组件内部的数据
    • render()方法是class组件中唯一必须实现的方法

4. render函数的返回值

  • 当render被调用时,他会检查 this.props 和 this.state 的变化并返回一下类型之一
  • React元素
    • 通过JSX创建
    • 如,<div>会被React渲染成DOM节点,<MyComponent>会被React渲染为自定义组件
    • 无论是<div>还是<MyComponent>均为React元素
  • 数组或fragments:使得render方法可以反悔多个元素
  • Portals:可以渲染子节点到不同的DOM子树中
  • 字符串或数值类型:在DOM中会被渲染为文本节点
  • 布尔类型或null:什么都不渲染

5. 函数组件

  • 函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容
  • 函数组件特点
    • 没有生命周期,也会被更新并挂载,但是没有生命周期函数
    • this关键字不能指向组件实力(因为没有组件实例)
    • 没有内部状态(state)
  • Hooks
  • 定义函数组件
export default function App() {
    return (
        <div>Hello React</div>
    )
}

二、React组件生命周期

1. 认识生命周期

  • 很多事物都有从创建到销毁的整个过程,这个过程称之为生命周期
  • React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能
  • 生命周期和生命周期函数的关系
  • 生命周期是一个抽象的概念,在生命周期的整个过程,分成很多个阶段
    • 如装载阶段(Mount),组件第一次在DOM树中被渲染的过程
    • 如更新阶段(Update),组件状态发生变化,重新更新渲染的过程
    • 如卸载过程(Unmount),组件从DOM树中被移除的过程
  • React内部为了告诉我们当前组件处于哪个阶段,会对组件内部实现某些函数进行回调,这些函数就是生命周期函数
    • 如实现函数conponentDidMount:组件已经挂载到DOM上时,就会回调
    • 如实现函数componentDidUpdate:组件已经发生了更新时,就会回调
    • 如实现函数componentWillUnmount:组件即将被移除时,就会回调
    • 可以在这些回调函数中编写自己的逻辑代码,完成功能需求
  • 谈React生命周期时,主要谈类的生命周期,因为函数式组件是没有生命周期函数的(通过hooks来模拟一些生命周期的回调)

2. 生命周期解析

  • 学习最常用的生命周期函数

react生命周期.png

3. 生命周期函数

  • Constructor
  • 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
  • constructor中通常做两件事
    • 通过给 this.state 赋值对象来初始化内部的state
    • 为事件绑定实例(this)
  • componentDidMount
  • componentDidMount()会在组件挂载后(charuDOM树中)立即调用
  • componentDidMount中通常进行哪里操作呢?
    • 依赖于DOM的操作可以在这里进行
    • 在此处发送网络请求
    • 添加一些订阅(会在componentWillUnmount取消订阅)
  • componentDidUpdate
  • componentDidUpdate()会在更新后被立即调用,首次渲染不会执行次方法
    • 当组件更新后,可以在此处对DOM进行操作
    • 如果对更新前后对props进行了比较,也可以选择在此处进行网络请求(如,当props未发生变化时,则不会执行网络请求)
  • componentWillUnmount
  • componentWillunmount()会在组件卸载及销毁之前直接调用
    • 在此方法中执行必要的清理操作
    • 如,清楚timer,取消网络请求或者清楚在componentDidMount()中创建的订阅等

4. 不常用的生命周期函数

  • 除了上述生命周期之外,还有一些不常用的生命周期函数
    • getDerivedStateFromProps:state的值在任何时候都依赖于props时使用;该方法返回一个对象来更新state
    • getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(如滚动位置)
    • shouldComponentUpdate:该生命周期函数很常用,常用来性能优化
  • 其它过期的生命周期函数,不推荐使用

react详细生命周期.png

三、React组件间的通信

1. 认识组件的嵌套

  • 组件之间存在嵌套关系
    • 如果一个应用程序中所有的逻辑都放在一个组件中,那么这个组件就会变得非常的臃肿和难以维护
    • 组件化的核心思想就是对组件进行拆分,拆分成一个个小的组件
    • 再将这些组件组合嵌套起来,最终形成完整的应用程序
  • 可做如下拆分
    • App组件是Header、Main、Footer组件的父组件
    • Main组件是Banner、ProductList组件的父组件

组件拆分.png

2. 认识组件间的通信

  • 开发中,经常需要组件间互相通信
    • 在父组件中请求到了list数据,需要下发给子组件展示
    • 子组件中发生事件,需要由父组件来完成操作,需要子组件向父组件传递事件
  • React项目中,组件之间的通信是非常重要的
  • 父组件在展示子组件,可能会传递一些数据给子组件
    • 父组件通过 属性=值 的形式来传递给子组件数据
    • 子组件通过 props 参数获取父组件传递过来的数据
// 父组件传参
<BookList books={books} movies={movies}/>

// 子组件接收
class BookList extends React.Component {
    // 可以省略
    // constructor (props) {
    //     super(props)
    // }
    render () {
        const {books, movies} = this.props
        console.log(this.prop, books, movies);

        return (
            <div>
                <h1>BookList</h1>
                <ul>
                    {books.map(i => {
                        return <li key={i}>{i}</li>
                    })}
                </ul>
                <h1>MoviesList</h1>
                <ul>
                    {movies.map(i => {
                        return <li key={i}>{i}</li>
                    })}
                </ul>
            </div>
        )
    }
}

3. 参数propTypes

  • 对于传递给子组件的数据,有时候希望进行验证,特别针对大型项目
    • 当然,如果项目已经默认继承了Flow或TypeScript,可直接进行类型验证
    • 即使没有使用Flow、TypeScript,也可以通过 prop-types 库来进行参数验证
  • 从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
  • 更多验证方式参考官方文档:zh-hans.reactjs.org/docs/typech…
    • 如验证数组,并且数组中包含哪些元素
    • 如验证对象,并且对象中包含哪些key以及value是什么类型
    • 如某个原生是必须的,使用 requireFunc:PropTypes.func.isRequired
  • 如果没有传递,希望有默认值
    • 使用 defaultProps 即可

4. 子组件传递父组件

  • 子向父传递消息
    • vue中通过自定义事件来完成
    • react中通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数
  • 案例
    • 将计数器案例进行拆解
    • 将按钮封装到子组件中
    • 子组件中的按钮发生点击事件,将内容传递到父组件中,修改 count 值
// 父组件
class HelloWorld extends React.Component {
    constructor () {
        super()
        this.state = {
            count: 0
        }
    }
    changeCount(i) {
        this.setState({
            count: this.state.count + i
        })
    }
    render() {
        const { count } = this.state
        return (
            <div>
                <CountCom count={count} changeCount={(i) => this.changeCount(i)}></CountCom>
            </div>
        )
    }
}

// 子组件
class CountCom extends React.Component {
    // 可省略
    // constructor (props) {
    //     super(props)
    // }
    changeCount(i) {
        this.props.changeCount(i)
    }
    render () {
        const { count } = this.props
        return (
            <div>
                <h3>{count}</h3>
                <button onClick={() => this.changeCount(1)}>+1</button>
                <button onClick={() => this.changeCount(5)}>+5</button>
                <button onClick={() => this.changeCount(10)}>+10</button>
            </div>
        )
    }
}

5. 组件通信案例联系

react-tab案例.png

// 父组件
class HelloWorld extends React.Component {
    constructor () {
        super()
        this.state = {
            tabList: ['tab1', 'tab2', 'tab3'],
            currIndex: 0
        }
    }
    changeTab(n) {
        this.setState({
            currIndex: n
        })
    }
    render() {
        const { tabList, currIndex } = this.state
        return (
            <div>
                <TabPractice tabList={tabList} changeTab={(n) => this.changeTab(n)}></TabPractice>
                <h1>{tabList[currIndex]}</h1>
            </div>
        )
    }
}

// 子组件
class TabPractice extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            currentIndex: 0
        }
    }
    changeTab(n) {
        this.setState({
            currentIndex: n
        })
        this.props.changeTab(n)
    }
    render() {
        const { currentIndex } = this.state
        const { tabList } = this.props
        return (
            <div>
                <div className="tab-list">
                    {
                        tabList.map((i, n) => {
                            return (
                                 <div 
                                    className={`tab-list-item ${currentIndex === n ? 'active' : ''}`}
                                    key={i}
                                    onClick={() => this.changeTab(n)}
                                >
                                    {i}
                                </div>
                            )
                        })
                    }
                </div>
            </div>
        )
    }
}
.tab-list {
    display: flex;
    justify-content: space-between;
    width: 200px;
    background-color: #f2f2f2;
}

.tab-list-item {
    color: skyblue;
    cursor: pointer;
}

.tab-list-item.active {
    color: red;
    border-bottom: 1px solid red;
    background-color: pink;
}

四、React组件插槽用法

1. React中的插槽(slot)

  • 开发中,抽取一个组件,为了让这个组件具备更强的通用性,不能将组件中的内容限制为固定的div、span等元素
  • 应该让开发者决定某一块区域到底存放什么内容(元素)

插槽.png

  • 在vue中通过 slot 来完成
  • react对于这种情况非常灵活,两种方案如下
    • 组件的children子元素(不推荐使用)
      • 传入一个元素,children就是当前元素对象;传入多个,children就是数组
    • props属性传递React元素

2. children实现插槽

  • 每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容

react-插槽实现.png

// 父组件
class HelloWorld extends React.Component {
    render() {
        return (
            <div>
                {/* 使用children实现插槽 */}
                <NavBar>
                    <i>哈哈</i>
                    <input type="text" />
                    <button>按钮</button>
                </NavBar>
            </div>
        )
    }
}
// 子组件
class NavBar extends React.Component {
    render () {
        console.log(this);
        const { children, leftSlot, centerSlot, rightSlot } = this.props
        return (
            <div className='nav-bar'>
                <div className="left">{children[0]}</div>
                <div className="center">{children[1]}</div>
                <div className="right">{children[2]}</div>
            </div>
        )
    }
}

3. props实现插槽

  • 通过children实现的方案虽然可行,但是弊端明显:通过缩影值获取传入的元素很容易出错,不能精准的获取传入的元素
  • props实现,通过具体的属性名,可以让我们在传入和获取时更加精准

react-插槽实现.png

// 父组件
class HelloWorld extends React.Component {
    render() {
        return (
            <div>
                {/* 使用props实现插槽 */}
                <NavBar 
                    leftSlot={<i>哈哈</i>}
                    centerSlot={<input type="text" />}
                    rightSlot={<button>按钮</button>}
                />
            </div>
        )
    }
}
// 子组件
class NavBar extends React.Component {
    render () {
        console.log(this);
        const { children, leftSlot, centerSlot, rightSlot } = this.props
        return (
            <div className='nav-bar'>
                <div className="left">{leftSlot}</div>
                <div className="center">{centerSlot}</div>
                <div className="right">{rightSlot}</div>
            </div>
        )
    }
}

4. 作用域插槽

// 父组件
class HelloWorld extends React.Component {
    constructor () {
        super()
        this.state = {
            scopeSlot: ['left', 'center', 'right']
        }
    }
    render() {
        const { scopeSlot } = this.state
        return (
            <div>
                <ScopeSlots scopeSlot={scopeSlot} tagType={(i) => <button>{i}</button>}></ScopeSlots>
            </div>
        )
    }
}
// 子组件
export class ScopeSlots extends Component {
  render() {
    const { scopeSlot, tagType } = this.props
    return (
      <div>
        {
            scopeSlot.map(i => {
                return (
                  <div>
                    {tagType(i)}
                  </div>
                )
            })
        }
      </div>
    )
  }
}

五、React非父子的通信

1. Context应用场景

  • 非父子组件数据的共享
    • 开发中,常见的数据传递方式通过props属性自上而下(由父到子)进行传递
    • 但如果一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)
    • 如果在顶层App中定义这些信息,之后层层传递下去,那么对于一些中间层不需要数据等组件来说,是一种冗余的操作
  • Spread Attributes => {...info}
  • 层级很多会很麻烦
    • react提供了API:Context
    • Context提供了一种在组件之间共享此类数值的方式,不必显式地通过组件树逐层传递props
    • Context设计目的就是为了共享那些对于一个组件树而言是全局的数据,如当前认证的用户、主题或首选语言

2. Context相关API

  • React.createContext
    • 创建一个需要共享的Context对象
    • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的Provider中读取到当前的context值
    • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就会使用默认值
// 创建一个带默认值的Context
const UserContext = React.createContext({a: 1, b: 2})
  • Context.Provider
    • 每个Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化
    • Provider接受一个value属性,传递给消费组件
    • 一个Provider可以和多个消费组件有对应关系
    • 多个Provider也可以嵌套使用,里层的会覆盖外层的数据
    • 当Provider的value值发生变化时,它内部的所有消费组件都会重新渲染
<UserContext.Provider value={{a: 1}}>
    <HelloWorld />
<UserContext.Provider/>
  • Class.contextType
    • 挂载在class上的contextType属性会被重新赋值为一个由React.createContext()创建的Context对象
    • 允许使用 this.context 来消费最近的 Context 上的那个值
    • 可以在任意生命周期中访问它,包括 render 函数中
ScopeSlots.contextType = UserContext
  • Context.Consumer
    • react组件也可以订阅到context变更。可以让 函数式组件 中完成订阅context
    • 需要函数作为子元素(function as child)
    • 这个函数接收当前的context值,返回一个React节点
<UserContext.Consumer>
    {value => {value.a}}
<UserContext.Consumer/>

3. Context代码演练

  • 什么时候使用 Context.Consumer ?
    • 当使用value的组件时函数式组件时
    • 当组件中需要使用多个Context时
import React from 'react'

// 1.创建Context
const ThemeContext = React.createContext()

export default ThemeContext
// 父组件
import ThemeContext from './theme-context'

 {/* 2.通过ThemeContext中Provider中value属性为后代提供数据 */}
<ThemeContext.Provider value={{bgc: 'pink', color: 'red'}}>
    <HelloWorld />
</ThemeContext.Provider>

// 孙组件
import ThemeContext from './theme-context'

import React, { Component } from 'react'
import ThemeContext from '../theme-context'

export class ScopeSlots extends Component {
  render() {
    // 4.使用context
    console.log('context', this.context);
    const { bgc, color } = this.context
    return (
      <div>
          <div style={{backgroundColor:bgc, color: color }}>测试context</div>
      </div>
    )
  }
}

// 3.设置组件的contextType为某一个Context
ScopeSlots.contextType = ThemeContext

export default ScopeSlots

4. 事件总线

  • event.js
import { EventEmitter } from 'events'

// 导入事件总线,利用这个对象发射和监听事件,这个对象是全局的
const eventBus = new EventEmitter()

export default eventBus
  • 监听移除触发
// 触发
// 发送事件eventBus.emit('事件名', 参数)
eventBus.emit('submit', { uname, pwd, hobbies, address })

// 监听移除
submitData({ uname, pwd, hobbies, address }) {
    console.log(uname, pwd, hobbies, address);
}
componentDidMount() {
    eventBus.addListener('submit', this.submitData)
    console.log('HelloWorld componentDidMount', eventBus);
}
// 组件被移除
componentWillUnmount() {
    eventBus.removeListener('submit', this.submitData)
    console.log('HelloWorld componentWillUnmount', eventBus);
}

六、setState的使用详解

1. 为什么使用setState

  • 开发中并不能直接通过修改state的值来让界面发生更新
    • 修改state后,希望react根据最新的state来重新渲染界面,但这种方式的修改react并不知道数据发生了变化
    • react没有类似vue2中的Object.defineProperty或Vue3中的Proxy的方式来监听数据的变化
    • 必须得通过setState来告诉react数据已经发生了变化
  • 在组件中并没有实现setState方法,为什么可以调用?
    • setState方法时从Component中继承过来的
    • 源码

2. setState的三种调用方式

// 1.基本使用
this.setState({
    msg: "Hello World"
})

// 2.setState可以传入一个回调函数
// 可以在回调函数中编写新的state的逻辑
// 当前的回调函数会将之前的state和props传递进来
this.setState((state, props) => {
    // 编写一些对新state处理逻辑
    // 可以获取之前的state和props值
    return {
        msg: "Hello React"
    }
})

// 3.setState在React的事件处理中是一个异步调用
// 如果希望在数据更新之后(数据合并),获取到对应的结果执行一些逻辑代码
// 那么可以在setState中传入第二个参数:callback
this.setState({ msg: "Hello Vue" }, () => {
    console.log('=======', this.state.msg)
})
console.log('-------', this.state.msg)

3. setState异步更新

  • setState的更新是异步的
    • 不能在执行完setState之后立马拿到最新的state的结果
  • 为什么setState设计为异步?
    • React核心成员(Redux作者)Dan Abramov
  • setState设计为异步,可以显著提升性能
    • 如果每次调用setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率很低
    • 应该获取多个更新,之后进行批量更新
  • 如果同步更新了state,但还没有执行render函数,那么state和props不能保持同步
    • state和props不能保持一致性会在开发中产生很多的问题

4. 如何获取异步的结果

  • 方式一:setState的回调
    • setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行
    • 格式如下:setState(partialState, callback)
  • 生命周期函数中
componentDidUpdate(prevProps, provState, snapshot) {
    console.log(this.state.msg)
}

5. setState一定是异步的么(React18之前)

  • 两种情况
    • 在组件生命周期或React合成事件中,setState是异步
    • 在setTimeout或原生DOM事件中,setState是同步

6. setState默认是异步的(React18之后)

  • 在React18之后,默认所有的操作都被放到了批处理中(异步处理)
  • 如果希望代码可以同步拿到,则需要执行特殊的flushSync操作
    • flushSync(() => {})

七、React性能优化SCU

1. React更新机制

  • React渲染流程
    • JSX => 虚拟DOM => 真实DOM
  • React的更新流程
    • props/state改变 => render函数重新执行 => 产生新的DOM树 => 新旧DOM树进行diff => 计算出差异进行更新 => 更新到真实的DOM

2. React更新流程

  • React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树
  • React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI
    • 如果一棵树参考另外一棵树进行完全比较更新,那么即使最先进的算法,该算法的复杂度为O(n2),其中 n 是树中元素的数量
    • 如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将会在十亿的量级范围
    • 这个开销过于昂贵,React的更新性能会变得非常低效
  • 于是,React对这个算法进行了优化,将其优化成了O(n)
    • 同层节点之间相互比较,不会垮节点比较
    • 不同类型的节点,产生不同的树结构
    • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定

3. keys的优化

  • 在遍历列表时,总会提示警告,让我们加入key属性
    • Warning:Each child in a list should have a unique "key" prop
  • 方式一:在最后位置插入数据
    • 有无key意义不大
  • 方式二:在前面插入数据
    • 在没key的情况下,所有的列表都要进行修改
  • 当子元素(li)拥有key时,React使用key来匹配原有树上的子元素以及最新树上的子元素
    • key为a和b的元素仅仅进行了位移,不需要进行任何修改
    • 将key为c的元素插入到最前面的位置即可
  • key的注意事项
    • key应该是唯一的
    • key不要使用随机数
    • 使用index作为key对性能没有优化

4. render函数被调用

  • 嵌套案例
    • 在App中增加一个计数器的代码
    • 点击+1时,会重新调用App的render函数
    • 当App的render函数被调用时,所有的子组件的render函数都会被重新调用
  • 只是修改了App中的数据,所有的组件都需要重新render,进行diff算法,性能必然是很低的
    • 事实上,很多的组件没有必须要重新render
    • 它们调用render应该有一个前提,就是依赖的数据(state、props)发生了改变时,再调用自己的render方法
  • 如何控制render方法是否被调用呢
    • 通过shouldComponentUpdate方法即可

5. shouldComponentUpdate

  • React给我们提供了一个生命周期方法shouldComponentUpdate(简称SCU),这个方法接受参数,并且需要有返回值
  • 该方法有两个参数
    • 参数一:nextProps修改后,最新的props属性
    • 参数二:nextState修改后,最新的state属性
  • 该方法返回值是一个boolean类型
    • 返回值为true,需要调用render方法
    • 返回值为false,不需要调用render方法
    • 默认返回的是true,也就是只要state发生改变,就会调用render方法
  • 比如在App中增加一个msg属性
    • jsx中并没有依赖这个msg,那么它的改变不应该引起重新渲染
    • 但因为render监听到state的改变,就会重新render,最后render方法还是被重新调用了

6. PureComponent

  • 如果所有的类都需要手动来实现shouldComponentUpdate,那么会给我们开发者增加非常多的工作量
    • 设想shouldComponentUpdate中的各种判断的目的是什么
    • props或state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false
  • 事实上React已经考虑到这点,默认帮我们实现了
    • 将class继承自 PureComponent

7. 高阶组件memo

  • 类组件可以使用PureComponent,那么函数式组件呢?
    • 事实上函数式组件在props没有改变时,也不希望其重新渲染其DOM树结构
  • 需要使用一个高阶组件memo
import { memo } from 'react'
const Profile = memo(function(props) {
    console.log('profile render')
    return <h2>Profile: {props.msg}</h2>
})
export default Profile

8. 不可变的力量案例

八、获取DOM方式refs

1. 如何使用ref

  • 在React开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊情况下,确实需要获取DOM进行操作
    • 管理焦点,文本选择或者媒体模仿
    • 触发强制动画
    • 集成第三方DOM库
    • 可以通过refs获取DOM
  • 如何创建refs来获取对应的DOM呢?三种方式
    • 1⃣️ 在React元素上绑定一个ref字符串
    • 2⃣️ 提前创建好ref对象,createRef(),将创建出来的对象绑定到元素上
    • 3⃣️ 在ref上传入一个回调函数(参数即dom),在对应元素被渲染后,回调函数被执行,并将元素传入

2. ref的类型

  • ref的值根据节点的类型而有所不同
    • 当ref属性用于HTML元素时,构造函数中使用React.createRef()创建的ref接受底层DOM元素作为其current属性
    • 当ref属性用于自定义class组件时,ref对象接收组件的挂载实例作为其current属性
    • 不能在函数组件上使用ref属性,函数式组件没有实例
  • 函数式组件是没有实例的,所以无法通过ref获取他们的实例
    • 可以通过React.forwardRef获取或其它hooks获取ref

3. ref的转发

  • ref不能用于函数式组件
    • 函数式组件没有实例,不能获取对应的组件对象
  • 获取函数式组件中某个元素的DOM
    • 通过forwardRef高阶函数
import React, { PureComponent, createRef, forwardRef } from 'react'

class HelloReact extends PureComponent {
    func() {
        console.log('HelloReact class');
    }
    render() {
        return (
        <div>HelloReact</div>
        )
    }
}

const FuncComponent = forwardRef(function(props, ref) {
    return (
        <div>
            <h3 ref={ref}>哈哈</h3>
        </div>
    )
})

export class RefCom extends PureComponent {
    constructor () {
        super()
        this.domRef = createRef()
        this.domEle = null
        this.componentRef = createRef()
        this.funcComponentRef = createRef()
    }
    getRef() {
        console.log(this.refs.dom)
        console.log(this.domRef.current)
        console.log(this.domEle)
        console.log(this.componentRef.current);
        this.componentRef.current.func()
        console.log(this.funcComponentRef.current);
    }
    render() {
        return (
        <div>
            <span ref="dom">Hello React</span>
            <span ref={this.domRef}>Hello Vue</span>
            <span ref={ele => this.domEle = ele}>Hello World</span>
            <HelloReact ref={this.componentRef}></HelloReact>
            <FuncComponent ref={this.funcComponentRef}></FuncComponent>
            <button onClick={e => this.getRef()}>获取ref</button>
        </div>
        )
    }
}

export default RefCom

九、受控和非受控组件

1. 认识收控组件

  • react中,HTML表单的处理方式和普通的DOM元素不太一样;表单元素通常会保持在一些内部的state

  • 如下面的HTML表单元素

    • 这种处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面
    • 在react中,并没有禁止该行为,依然有效
    • 但通常会使用JavaScript函数来处理表单提交,同时还可以访问用户填写的表单数据
    • 实现这种效果的标准方式是使用-受控组件
<form>
    <label>
        名字:
        <input type="text" name="name" />
    </label>
    <input type="submit" value="提交"/>
</form>

2. 受控组件基本演练

  • html中,表单元素(input\textarea\select)通常自己维护state,并根据用户输入进行更新
  • 在React中,可变状态(mutable state)通常保存在组件的state属性中,并且只能通过使用setState()来更新
    • 二者结合,使React的state成为“唯一数据源”
    • 渲染表单的React组件还控制着用户输入过程中表单发生的操作
    • 被React以这种方式控制取值的表单输入元素称为“受控组件”
  • 由于在表单元素上设置了value属性,因此显示的值将始终为 this.state.value,这使得react的state成为唯一数据源
  • 由于 handleUsernameChange在每次按键时都会执行并更新React的state,因此显示的值将随着用户输入而更新
ElementValue propertyChange callbackNew value in the callback
<input type="text" />value="string"onChangeevent.target.value
<input type="checkbox" />checked={boolean}onChangeevent.target.checked
<input type="radio" />checked={boolean}onChangeevent.target.checked
<textarea />value="string"onChangeevent.target.value
<select />value="option value"onChangeevent.target.value

3. 受控组件的其它演练

  • textarea标签
    • texteare标签和inpit相似
  • select标签
    • select标签的使用非常简单,不需要通过selected属性控制哪个被选中,他可以匹配state的value来选中
  • 处理多个输入
    • 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法
    • ES6新语法:计算属性名(Computed property names)

4. 非受控组件

  • React推荐大多数情况下使用受控组件来处理表单数据
    • 一个受控组件中,表单数据是由React组件来管理的
    • 使用非受控组件,这时表单数据将由DOM节点来处理
  • 如果要使用非受控组件中的数据,那么需要使用ref来从DOM节点中获取表单数据
    • 使用ref来获取input元素
  • 在非受控组件中通常使用 defaultValue 来设置默认值
  • 同样,<input type="checkbox"> 和 <input type="radio"> 支持 defaultChecked,<select> 和 <textarea> 支持 defaultValue
import React, { PureComponent } from 'react'

export class FormCom extends PureComponent {
    constructor (props) {
        super(props)
        this.state = {
            uname: '',
            pwd: '',
            hobbies: [
                {
                    value: 'sing',
                    name: '唱',
                    checked: false
                },
                {
                    value: 'dance',
                    name: '跳',
                    checked: false
                },
                {
                    value: 'rap',
                    name: 'rap',
                    checked: false
                }
            ],
            address: []
        }
    }
    onSubmit = (e) => {
        const { uname, pwd, hobbies, address } = this.state
        e.preventDefault()
        console.log('姓名', uname);
        console.log('密码', pwd);
        console.log('爱好', hobbies.filter(i => i.checked).map(i => i.value));
        console.log('地址', address);
    }
    changeInfo (e) {
        console.log(e.target.value);
        this.setState({
            [e.target.name]: e.target.value
        })
    }
    changechecked (e, n) {
        let hobbies = [...this.state.hobbies]
        hobbies[n].checked = e.target.checked
        this.setState({
            hobbies
        })
    }
    changeAddress (e) {
        const options = Array.from(e.target.selectedOptions)
        const values = options.map(i => i.value)
        console.log(options, values);
        this.setState({
            address: values
        })
    }
    render() {
        const { uname, pwd, hobbies, address } = this.state
        return (
        <div>
            <form onSubmit={this.onSubmit}>
                <div>
                    姓名:
                    <input 
                        type="text" 
                        name="uname" 
                        value={uname} 
                        onChange={e => this.changeInfo(e)}
                    />
                </div>
                <div>
                    密码:
                    <input 
                        type="password" 
                        name="pwd" 
                        value={pwd} 
                        onChange={e => this.changeInfo(e)} 
                    />
                </div>
                <div>
                    您的爱好:
                    {
                        hobbies.map((i, n) => {
                            return (
                                <label htmlFor={i.value} key={i.value}>
                                    <input 
                                        type="checkbox" 
                                        name={i.value} 
                                        id={i.value}
                                        checked={i.checked}
                                        onChange={e => this.changechecked(e, n)}
                                    />
                                    {i.name}
                                </label>
                            )
                        })
                    }
                </div>
                <select 
                    value={address} 
                    multiple
                    onChange={e => this.changeAddress(e)}
                >
                    <option value="北京">北京</option>
                    <option value="上海">上海</option>
                    <option value="广州">广州</option>
                </select>
                <input type="submit" />
            </form>
        </div>
        )
    }
}

export default FormCom

十、React的高阶组件

1. 认识高阶函数

  • 什么是高阶函数?满足以下条件之一
    • 接受一个或多个函数作为输入
    • 输出一个函数
  • JavaScript中比较常见的filter、map、reduce都是高阶函数
  • 高阶组件呢?
    • 高阶组件(Higher-Order Components),简称HOC
    • 高阶组件是参数为组件,返回值为新组件的函数
  • 解析
    • 高阶组件本身不是组件,而是一个函数
    • 这个函数的参数是一个组件,返回值也是一个组件

2. 高阶组件的定义

  • 高阶组件的调用过程类似如下
const EnhancedComponent = higherOrderComponent(WrappedComponent)
  • 高阶函数的编写过程类似如下
function higherOrderComponent(WrapperComponent) {
    class NewComponent extends PureComponent {
        render() {
            return <WrapperComponent/>
        }
    }
    NewComponent.displayName = "React"
    return NewComponent
}
  • 组件的名称问题
    • 在ES6中,类表达式中类名是可以省略的
    • 组件的名称都可以通过displayName来修改
  • 高阶组件并不是react api 的一部分,它基于react的组合特性而形成的设计模式
  • 高阶组件在一些react第三方库中非常常见
    • 如redux中的connect
    • 如react-router中的withRouter

3. 高阶组件的意义

  • 利用高阶组件可以针对某些react代码进行更加优雅的处理
  • 早期react有提供组件之间的一种复用方式是mixin(目前不建议使用)
    • Mixin可能会相互依赖,互相耦合,不利于代码维护
    • 不同的Mixin中的方法可能会互相冲突
    • Mixin非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
  • HOC也有缺陷
    • 需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,让调试变得非常困难
    • 劫持props,在不遵守约定的情况下可能造成冲突
  • Hooks的出现,是开创性的,解决了很多react之前存在的问题
    • 如this指向问题,如HOC嵌套复杂度问题等

应用一:props的增强

  • 不修改原有代码的情况下,添加新的props
  • 利用高阶组件来共享Context
import ThemeContext from "../theme-context";

function HeighterCom(OldCom) {
    return props => {
        return (
            <div>
                <ThemeContext.Consumer>
                    {
                        value => {
                            return <OldCom {...props} {...value} />
                        }
                    }
                </ThemeContext.Consumer>
            </div>
        )
    }
}

export default HeighterCom

应用二:渲染判断鉴权

  • 开发中常用场景
    • 某些页面需要登陆才能进入
    • 未登录直接跳转到登陆页面
  • 使用高阶组件来完成鉴权操作
    • this.forceUpdate()强制刷新界面
function loginAuth(OldCom) {
    return props => {
        const token = localStorage.getItem('token')
        if(token) {
            return <OldCom {...props} />
        } else {
            return <h2>请先登录</h2>
        }
    }
}

export default loginAuth
class App extends React.Component {
    loginClick() {
        localStorage.setItem('token', '123456')
        this.forceUpdate() // 强制更新
    }
    outLoginClick() {
        localStorage.removeItem('token')
        this.forceUpdate()
    }
    render () {
        return (
            <div>
                <button onClick={e => this.loginClick()}>登录</button>
                <button onClick={e => this.outLoginClick()}>退出登录</button>
            </div>            
        )
    }
}

应用三:生命周期劫持

  • 可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑
import { PureComponent } from "react";

function logRenderTime(OldComponent) {
    return class extends PureComponent {
        UNSAFE_componentWillMount() {
            this.beginTime = new Date().getTime()
        }
        componentDidMount() {
            this.endTime = new Date().getTime()
            const interval = this.endTime - this.beginTime
            console.log(`当前${OldComponent.name}页面,render time: ${interval}ms`)
        }
        render() {
            return <OldComponent {...this.props} />
        }
    }
}

export default logRenderTime

十一、protals和fragment

1. Portals的使用

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到DOM元素中(默认挂载到id为root的DOM元素上)
  • Portals提供了一种将自节点渲染到存在于父组件以外的DOM节点的优秀的案例
    • 参数一(child)是任何可渲染到React子元素,例如一个元素,字符串或fragment
    • 参数二(container)是一个DOM元素
ReactDOM.createPortal(child, container)
  • 通常从组件的render方法返回一个元素时,该元素将被挂载到DOM节点中离其最近的父节点
  • 然而,有时候将子元素插入到DOM节点中的不同位置也有好处

2. Modal组件案例

  • 开发一个Modal组件,可以将它的子组件渲染到屏幕的中间位置
    • 1⃣️ 修改index.html添加新的节点
    • 2⃣️ 编写这个节点的样式
    • 3⃣️ 编写组件代码
<div id="root"></div>
<div id="modal"></div>
class Modal extends PureComponent {
    render() {
        return ReactDOM.createPortal(
            this.props.children,
            document.getElementById("modal")
        )
    }
}

3. fragment

  • 之前总是在一个组件返回内容时,包裹一个div元素
  • 希望不渲染这个div该如何操作?
    • 使用Fragment
    • Fragment允许你将子列表分组,无需向DOM添加额外节点
  • react还提供Fragment的短语法
    • 看起来像空标签<></>
    • 但,如果渲染列表需要在Fragment中添加key,那么就不能使用短语法了

十二、StrictMode严格模式

1. StrictMode

  • StictMode是一个用来凸显应用程序中潜在问题等工具
    • 与Fragment一样,StrictMode不会渲染任何可见的UI
    • 它为其后代元素触发额外的检查和警告
    • 严格模式检查仅在开发模式下运行,不会影响生产构建
  • 可以为应用程序的任何部分启用严格模式
    • 如下案例,不会对Header和Footer组件运行严格模式检查
    • 但,ComponentOne和ConponentTwo以及它们所有后代元素都将进行检查
<Header/>
<React.StrictMode>
    <div>
        <ComponentOne />
        <ComponentTwo />
    </div>
</React.StrictMode>
<Footer/>

2. 严格模式检查的是什么?

  • 1⃣️ 识别不安全的生命周期(过期)
  • 2⃣️ 使用过时的ref API
  • 3⃣️ 检查意外的副作用
    • 这个组件的constructor会被调用两次
    • 严格模式下故意如此,让你查看这里写的逻辑代码被调用多次,是否会产生bug
    • 在生产环境中,是不会被调用两次的
  • 4⃣️ 使用废弃的findDOMNode方法
    • 之前的react api中,可以通过findDOMNode来获取DOM,已不推荐使用
  • 5⃣️ 检测过时的context API
    • 早起的Context通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context
    • 已不推荐使用