likes
comments
collection
share

React 组件化开发高级

作者站长头像
站长
· 阅读数 26

一、ref

1.1 ref获取DOM

  • 在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:

    • 管理焦点,文本选择或媒体播放
    • 触发强制动画
    • 集成第三方 DOM 库
  • 目前可以通过 refs 来获取对应的DOM,有三种方式:

    1. 传入字符串(不推荐)

      • 使用时通过 this.refs. 传入的字符串格式获取对应的元素
    2. 传入一个对象(推荐

      • 对象是通过 React.createRef() 方式创建出来的
      • 使用时获取到创建的对象其中有一个current属性就是对应的元素
    3. 传入一个函数

      • 该函数会在DOM被挂载时进行回调,这个函数会传入一个元素对象,可以自己保存
      • 使用时,直接拿到之前保存的元素对象即可
    import React, { PureComponent, createRef } from 'react'
    
    export class App extends PureComponent {
      constructor() {
        super()
    
        this.state = {}
    
        this.titleRef = createRef()
        this.titleEl = null
      }
    
      getNativeDOM() {
        // 1.在React元素上绑定一个ref字符串
        console.log(this.refs.title)
    
        // 2.提前创建好ref对象, createRef(), 将创建出来的对象绑定到元素
        console.log(this.titleRef.current)
    
        // 3.传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入
        console.log(this.titleEl)
      }
    
      render() {
        return (
          <div>
            <h2 ref="title">Hello World</h2>
            <h2 ref={this.titleRef}>你好啊,李银河</h2>
            <h2 ref={el => this.titleEl = el}>你好啊, 师姐</h2>
            <button onClick={e => this.getNativeDOM()}>获取DOM</button>
          </div>
        )
      }
    }
    
    export default App
    

1.2 ref获取组件

  • ref 的值根据节点的类型而有所不同:

    • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
    • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
    • 不能在函数组件上使用 ref 属性,因为他们没有实例
  • 类组件的实例

    • React.createRef()
    import React, { PureComponent, createRef } from 'react'
    
    class HelloWorld extends PureComponent {
      test() {
        console.log("test------")
      }
    
      render() {
        return <h1>Hello World</h1>
      }
    }
    
    export class App extends PureComponent {
      constructor() {
        super()
    
        this.hwRef = createRef()
      }
    
      getComponent() {
        console.log(this.hwRef.current)
        // 调用组件内部的方法
        this.hwRef.current.test()
      }
    
      render() {
        return (
          <div>
            <HelloWorld ref={this.hwRef}/>
            <button onClick={e => this.getComponent()}>获取组件实例</button>
          </div>
        )
      }
    }
    
    export default App
    
  • 函数组件:

    • forwardRef(高阶组件) => ref => 内部绑定ref
    • 绑到内部的 某一个元素
    import React, { PureComponent, createRef, forwardRef } from 'react'
    
    // 使用 forwardRef 对函数式组件进行包裹
    const HelloWorld = forwardRef(function(props, ref) {
      return (
        <div>
          <h1 ref={ref}>Hello World</h1>
          <p>哈哈哈</p>
        </div>
      )
    })
    
    
    export class App extends PureComponent {
      constructor() {
        super()
    
        this.hwRef = createRef()
      }
    
      getComponent() {
        console.log(this.hwRef.current)
      }
    
      render() {
        return (
          <div>
            <HelloWorld ref={this.hwRef}/>
            <button onClick={e => this.getComponent()}>获取组件实例</button>
          </div>
        )
      }
    }
    
    export default App
    

二、受控组件和非受控组件

2.1 受控组件

  • 在 HTML 中,表单元素(如<input><textarea><select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新

  • 在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新

    • 我们将两者结合起来,使React的state成为“唯一数据源”
    • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作
    • 被 React 以这种方式控制取值的表单输入元素就叫做 “受控组件”
    import React, { PureComponent } from 'react'
    
    export default class App extends PureComponent {
    
      constructor() {
        super()
    
        this.state = {
          text: '我是文本'
        }
      }
    
      inputChange(event) {
        console.log(event.target.value)
        this.setState({ text: event.target.value })
      }
    
      render() {
        const { text } = this.state
    
        return (
          <div>
            {/* 受控组件 */}
            <input type="text" value={text} onChange={e => this.inputChange(e)} />
            
            {/* 非受控组件 */}
            <input type="text" />
            <h2>text: {text}</h2>
          </div>
        )
      }
    }
    

2.2 各种表单的受控处理

  • 阻止原生 <form> 的默认提交,通过 React 的 state 来保存数据

  • 对于用户名和密码的 change 事件函数封装到一起

    import React, { PureComponent } from 'react'
    
    export class App extends PureComponent {
    
      constructor() {
        super()
    
        this.state = {
          username: "",
          password: "",
          isAgree: false,
          hobbies: [
            { value: "sing", text: "唱", isChecked: false },
            { value: "dance", text: "跳", isChecked: false },
            { value: "rap", text: "rap", isChecked: false }
          ],
          fruit: ["orange"]
        }
      }
    
      handleSubmitClick(event) {
        // 1.阻止默认的行为
        event.preventDefault()
    
        // 2.获取到所有的表单数据, 对数据进行组件
        console.log("获取所有的输入内容")
        console.log(this.state.username, this.state.password)
        const hobbies = this.state.hobbies.filter(item => item.isChecked).map(item => item.value)
        console.log("获取爱好: ", hobbies)
    
        // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
      }
    
      handleInputChange(event) {
        this.setState({
          // 计算属性的写法
          [event.target.name]: event.target.value
        })
      }
    
      // 多选框单选处理
      handleAgreeChange(event) {
        this.setState({ isAgree: event.target.checked })
      }
    
      handleHobbiesChange(event, index) {
        const hobbies = [...this.state.hobbies]
        hobbies[index].isChecked = event.target.checked
        this.setState({ hobbies })
      }
    
      // select 的处理
      handleFruitChange(event) {
        const options = Array.from(event.target.selectedOptions)
        const values = options.map(item => item.value)
        this.setState({ fruit: values })
    
        // 额外补充: Array.from(可迭代对象)
        // Array.from(arguments)
        // Array.from() 接受两个参数:一个是可迭代对象,另一个是数组的map函数
        const values2 = Array.from(event.target.selectedOptions, item => item.value)
        console.log(values2)
      }
    
      render() {
        const { username, password, isAgree, hobbies, fruit } = this.state
    
        return (
          <div>
            <form onSubmit={e => this.handleSubmitClick(e)}>
              {/* 1.用户名和密码 */}
              <div>
                <label htmlFor="username">
                  用户: 
                  <input 
                    id='username' 
                    type="text" 
                    name='username' 
                    value={username} 
                    onChange={e => this.handleInputChange(e)}
                  />
                </label>
                <label htmlFor="password">
                  密码: 
                  <input 
                    id='password' 
                    type="password" 
                    name='password' 
                    value={password} 
                    onChange={e => this.handleInputChange(e)}
                  />
                </label>
              </div>
    
              {/* 2.checkbox单选 */}
              <label htmlFor="agree">
                <input 
                  id='agree' 
                  type="checkbox" 
                  checked={isAgree} 
                  onChange={e => this.handleAgreeChange(e)}
                />
                同意协议
              </label>
    
              {/* 3.checkbox多选 */}
              <div>
                您的爱好:
                {
                  hobbies.map((item, index) => {
                    return (
                      <label htmlFor={item.value} key={item.value}>
                        <input 
                          type="checkbox"
                          id={item.value} 
                          checked={item.isChecked}
                          onChange={e => this.handleHobbiesChange(e, index)}
                        />
                        <span>{item.text}</span>
                      </label>
                    )
                  })
                }
              </div>
    
              {/* 4.select */}
              <select value={fruit} onChange={e => this.handleFruitChange(e)} multiple>
                <option value="apple">苹果</option>
                <option value="orange">橘子</option>
                <option value="banana">香蕉</option>
              </select>
    
              <div>
                <button type='submit'>注册</button>
              </div>
            </form>
          </div>
        )
      }
    }
    
    export default App
    

    React 组件化开发高级

2.3 非受控组件

  • React推荐大多数情况下使用 受控组件 来处理表单数据:

    • 一个受控组件中,表单数据是由 React 组件来管理的
    • 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理
  • 如果要使用非受控组件中的数据,那么就需要使用 ref 来从DOM节点中获取表单数据

    • 使用ref来获取input元素
import React, { createRef, PureComponent } from 'react'

export class App extends PureComponent {

  constructor() {
    super()

    this.state = {}

    this.introRef = createRef()
  }

  componentDidMount() {
    // 监听变化需要在此处监听
    // this.introRef.current.addEventListener
  }

  handleSubmitClick(event) {
    // 1.阻止默认的行为
    event.preventDefault()

    // 2.获取到所有的表单数据, 对数据进行组件
    console.log("获取结果:", this.introRef.current.value)
  }

  render() {
    const { username, password, isAgree, hobbies, fruit, intro } = this.state

    return (
      <div>
        <form onSubmit={e => this.handleSubmitClick(e)}>
        
          <input type="text" defaultValue={intro} ref={this.introRef} />

          <div>
            <button type='submit'>注册</button>
          </div>
        </form>
      </div>
    )
  }
}

export default App
  • 在非受控组件中通常使用defaultValue来设置默认值;同样,<input type="checkbox"><input type="radio"> 支持 defaultChecked<select><textarea> 支持 defaultValue

三、高阶组件

3.1 认识高阶组件

  • 什么是高阶组件呢?

    • 高阶组件的英文是 Higher-Order Components,简称为 HOC
    • 官方的定义:高阶组件是参数为组件,返回值为新组件的函数
  • 分析:

    1. 高阶组件 本身不是一个组件,而是一个函数
    2. 这个函数的参数是一个组件,返回值也是一个组件
  • 写法:

function higherOrderComponent(WrapperComponent) {
  class NewComponent extends PureComponent {
    render() {
      return <WrapperComponent />
    }
  }
  // 修改组件名称
  NewComponent.displayName = 'HocComponent'
  return NewComponent
}
  • 高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式

  • 高阶组件在一些React第三方库中非常常见:

    • 比如 redux 中的connect
    • 比如 react-router 中的 withRouter

3.2 应用一:props增强

不修改原来的代码,给组件加上props

  • 封装一个 HOC 组件

    import { PureComponent } from 'react'
    
    // 定义组件: 给一些需要特殊数据的组件, 注入props
    function enhancedUserInfo(OriginComponent) {
      class NewComponent extends PureComponent {
        constructor() {
          super()
          this.state = {
            userInfo: {
              name: '独孤月',
              level: 77
            }
          }
        }
        render() {
          return <OriginComponent {...this.props} {...this.state.userInfo} />
        }
      }
    
      return NewComponent
    }
    
    export default enhancedUserInfo
    
  • 在App组件中,引入其他组件

    import React, { PureComponent } from 'react'
    import enhancedUserInfo from './hoc/enhanced_props'
    import About from './pages/About'
    
    const Home = enhancedUserInfo(function(props) {
      return <h2>Home: {props.name}-{props.level}-{props.banners}</h2>
    })
    
    const Profile = enhancedUserInfo(function(props) {
      return <h2>Profile: {props.name}-{props.level}</h2>
    })
    
    export default class App extends PureComponent {
      render() {
        return (
          <div>
            <Home banners={['轮播图1', '轮播图2']}/>
            <Profile/>
    
            <About/>
          </div>
        )
      }
    }
    
  • About组件可以在导出的时候,通过HOC组件包裹进行增强

    import React, { PureComponent } from 'react'
    import enhancedUserInfo from '../hoc/enhanced_props'
    
    export class About extends PureComponent {
      render() {
        return (
          <div>
            <h2>About: {this.props.name}-{this.props.level}</h2>
          </div>
        )
      }
    }
    
    export default enhancedUserInfo(About)
    

    React 组件化开发高级

3.3 应用二:context数据共享

  • 创建一个 ThemeContext

    import { createContext } from "react"
    
    const ThemeContext = createContext()
    
    export default ThemeContext
    
  • 编写 withTheme 高阶组件

    import ThemeContext from "../context/ThemeContext"
    
    function withTheme(OriginComponent) {
      return (props) => {
        return (
          <ThemeContext.Consumer>
            {
              value => {
                return <OriginComponent {...props} {...value} />
              }
            }
          </ThemeContext.Consumer>
        )
      }
    }
    
    export default withTheme
    
  • Product组件中使用 context

    • 相比较于直接使用context,通过 withTheme 高阶组件生成组件更优雅
    import React, { PureComponent } from 'react'
    // import ThemeContext from '../context/ThemeContext'
    import withTheme from '../hoc/with_theme'
    
    // export class Product extends PureComponent {
    //   render() {
    //     return (
    //       <div>
    //         <h2>Product</h2>
    //         <ThemeContext.Consumer>
    //           {
    //             value => {
    //               return <h2>theme: {value.color}-{value.size}</h2>
    //             }
    //           }
    //         </ThemeContext.Consumer>
    //       </div>
    //     )
    //   }
    // }
    
    // export default Product
    
    export class Product extends PureComponent {
      render() {
        return (
          <div>
            <h2>Procduct</h2>
            <h2>theme: {this.props.color}-{this.props.size}</h2>
          </div>
        )
      }
    }
    
    export default withTheme(Product)
    

3.4 应用三:登录鉴权

  • 判断用户是否登录,登录后才能够访问 Cart 组件

  • 编写鉴权 HOC

    function loginAuth(OriginComponent) {
      return props => {
        // 从localStorage中获取token(登录后存储)
        const token = localStorage.getItem('token')
    
        if (token) {
          return <OriginComponent {...props}/>
        } else {
          return <h2>请先登录, 再进行跳转到对应的页面中</h2>
        }
      }
    }
    
    export default loginAuth
    
  • 对 Cart 组件进行处理

    import React, { PureComponent } from 'react'
    import loginAuth from '../hoc/login_auth'
    
    export class Cart extends PureComponent {
      render() {
        return (
          <div>
            <h2>Cart Page</h2>
          </div>
        )
      }
    }
    
    export default loginAuth(Cart)
    

3.5 应用四:拦截生命周期

  • 想要计算页面的渲染时间,可以在页面渲染前后分别计算时间,然后计算时间差

    • 注意componentWillMount在 16.3 版本已弃用,此处仅作为练习
  • 封装打印页面渲染时间 HOC

    import { PureComponent } from "react"
    
    function logRenderTime(OriginComponent) {
      // 可以省略名字,类似直接返回函数
      return class extends PureComponent {
        componentWillMount() {
          this.beginTime = new Date().getTime()
        }
        componentDidMount() {
          this.endTime = new Date().getTime()
          const interval = this.endTime - this.beginTime
          console.log(`当前页面渲染耗时${interval}ms`);
        }
        render() {
          return <OriginComponent {...this.props}/>
        }
      }
    }
    
    export default logRenderTime
    

3.6 高阶组件的意义

  • 我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理

  • 其实早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用

    • Mixin 可能会相互依赖,相互耦合,不利于代码维护
    • 不同的Mixin中的方法可能会相互冲突
    • Mixin非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
  • HOC也有一些缺陷:

    • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难
    • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突
  • Hooks的出现,是开创性的,它解决了很多React之前的存在的问题

    • 比如this指向问题、比如hoc的嵌套复杂度问题等等(后续详细补充

四、其他补充

4.1 Portals

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的)。

  • Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案 createPortal()

    • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment
    • 第二个参数(container)是一个 DOM 元素

Modal案例

  • Modal.jsx

    import { PureComponent } from 'react'
    import { createPortal } from 'react-dom'
    
    export default class Modal extends PureComponent {
      render() {
        return createPortal(this.props.children, document.querySelector('#modal'))
      }
    }
    
  • App.jsx

    import React, { PureComponent } from 'react'
    import Modal from './Modal'
    
    export default class App extends PureComponent {
      render() {
        return (
          <div className='app'>
            <h1>App H1</h1>
            <Modal>
              <h2>我是标题</h2>
              <p>
                我是内容,我是内容,我是内容
              </p>
            </Modal>
          </div>
        )
      }
    }
    

    React 组件化开发高级

4.2 Fragment

  • 一般情况下,我们写组件的render函数返回时通常会包裹一个 <div>,但是如果又不想渲染这样的一个 div,此时就可以使用 <Fragment>

  • Fragment 允许将子列表分组,而无需向 DOM 添加额外节点

    import React, { Fragment, PureComponent } from 'react'
    
    export default class App extends PureComponent {
      render() {
        return (
          // <Fragment>
          //   <h2>我是App的标题</h2>
          //   <p>我是App的内容</p>
          // </Fragment>
          <>
            <h2>我是App的标题</h2>
            <p>我是App的内容</p>
          </>
        )
      }
    }
    
  • React 提供了短语法, <></>,但是,在列表循环时,需要在Fragment中添加key就不能使用短语法

4.3 StrictMode

  • StrictMode 是一个用来突出显示应用程序中潜在问题的工具

    • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI
    • 它为其后代元素触发额外的检查和警告
    • 严格模式检查仅在开发模式下运行,它们不会影响生产构建
  • 可以为应用程序的任何部分启用严格模式

    • App.jsx
    import React, { PureComponent, StrictMode } from 'react'
    // import { findDOMNode } from "react-dom"
    import Home from './pages/Home'
    import Profile from './pages/Profile'
    
    export class App extends PureComponent {
      render() {
        return (
          <div>
            <StrictMode>
              <Home/>
            </StrictMode>
            <Profile/>
          </div>
        )
      }
    }
    
    export default App
    
    • Home.jsx
    import React, { PureComponent } from 'react'
    
    export class Home extends PureComponent {
      // UNSAFE_componentWillMount() {
      //   console.log("Home UNSAFE_componentWillMount")
      // }
    
      constructor(props) {
        super(props)
    
        console.log("Home Constructor")
      }
    
      componentDidMount() {
        console.log("Home componentDidMount")
      }
    
      render() {
        console.log("Home Render")
    
        return (
          <div>
            {/* <h2 ref="title">Home Title</h2> */}
    
            <h2>Home</h2>
          </div>
        )
      }
    }
    
    export default Home
    
  • 严格模式检查什么?

    1. 识别不安全的生命周期

    2. 使用过时的ref API

      • 使用 ref 绑定 string,会报错
    3. 检查意外的副作用

      • 这个组件的constructor会被调用两次
      • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
      • 在生产环境中,是不会被调用两次的
    4. 使用废弃的findDOMNode方法

      • 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用
    5. 检测过时的context API

      • 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的(已不推荐使用)