React组件化开发(二)

19 阅读8分钟

React中获取DOM的方式有哪些?

ref 获取DOM

  1. 方式一:传入字符串
    • 使用时通过 this.refs.传入的字符串格式获取对应的元素;
  2. 方式二:传入一个对象
    • 对象是通过 React.createRef() 方式创建出来的;
    • 使用时获取到创建的对象其中有一个current属性就是对应的元素;
  3. 方式三:传入一个函数
    • 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;
    • 使用时,直接拿到之前保存的元素对象即可;
   getDOM() {
   // 方式一: 在react元素上绑定ref字符串 - 这种方式react已经不推荐了
   // console.log(this.refs.http)// 方式二: 提前创建好ref对象, createRef(), 将创建出来的对象绑定到元素(推荐)
   // console.log(this.titleRef.current)// 方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入(16.3之前的版本)
   // console.log(this.titleEl)
   }
   <h3 ref="http">大大怪将军</h3>
   <h3 ref={this.titleRef}>小小怪下士</h3>
   <h3 ref={el => this.titleEl = el}>瑞克</h3>
   <button onClick={() => this.getDOM()}>获取DOM</button>

ref 的类型

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

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

    • 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
    • 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref;

ref 获取组件实例 -- createRef

    import React, { PureComponent, createRef } from 'react'

    constructor() {
        super()
        this.state = {}
        this.HWRef = createRef()
    }

    getComponent() {
        console.log(this.HWRef.current)
        this.HWRef.current.test()
    }

    <HelloWorld ref={this.HWRef} />
    <button onClick={() => this.getComponent()}>获取组件实例</button>

函数组件 -- 函数式组件是没有实例的,所以无法通过ref获取他们的实例 -- React.forwardRef

    import React, { PureComponent, createRef, forwardRef } from 'react'

    const HelloWorld = forwardRef(function(props, ref) {
      return (
        <div>
          <h2 ref={ref}>函数组件</h2>
          <h4>大大怪将军</h4>
        </div>
      )
    })
    constructor() {
        super()
        this.state = {}
        this.HWRef = createRef()
    }

    getComponent() {
        console.log(this.HWRef.current)
    }

    render() {
        return (
          <div>
            <HelloWorld ref={this.HWRef} />
            <button onClick={() => this.getComponent()}>获取DOM</button>
          </div>
        )
    }

受控组件和非受控组件

受控组件与非受控组件的主要区别就在于数据的来源受控组件的数据源是React 组件的 state 属性,而非受控组件的数据源是DOM 元素本身

受控组件

  • 在 React 中,可变状态通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新
    • 我们将两者结合起来,使React的state成为“唯一数据源”
    • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作
    • 被 React 以这种方式控制取值的表单输入元素就叫做"受控组件"
    this.state = {
      message: ""
    }
    changeInput(event) {
      console.log(event.target.value)
      this.setState({ message: event.target.value })
    }

    render(){
       <
           input type="text" 
           value={message} 
           onChange={(event) => this.changeInput(event)} 
       />
    }

image.png

非受控组件

  • 在受控组件中,表单数据是由 React 组件来管理的
  • 非受控组件中,表单数据将交由 DOM 节点来处理
    • 在非受控组件中通常使用defaultValue来设置默认值;
    • <input type="checkbox"><input type="radio"> 支持defaultChecked<select> <textarea> 支 持 defaultValue
    this.messageRef.current.value

    // 在非受控组件中通常使用defaultValue来设置默认值
    render(){
        <input type="text" defaultValue={message} ref={this.messageRef} />
    }

处理多个输入和默认行为

1.多个输入

  • 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:
  • 这里我们可以使用ES6的一个语法:计算属性名(Computed property names)
    • [event.target.name]: event.target.value 方便进行事件监听

2.默认行为

  • DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
  • 在React中,并没有禁止这个行为,依然是有效的
    • event.preventDefault() 阻止
    export class App extends PureComponent {

      constructor() {
        super()

        this.state = {
          username: "",
          password: ""
        }
      }

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

        // 2.获取到所有的表单数据, 对数据进行组件
        console.log("获取所有的输入内容")
        console.log(this.state.username, this.state.password)

        // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
      }

      handleInputChange(event) {
        this.setState({
          [event.target.name]: event.target.value
        })
      }

      render() {
        const { username, password } = this.state

        return (
          <div>
            <form onSubmit={e => this.handleSubmitClick(e)}>
              {/* 1.用户名和密码 */}
              <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>

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

高阶函数

  • 至少满足以下条件之一:

    • 接受一个或多个函数作为输入;
    • 输出一个函数;
  • 常见的filter、map、reduce都是高阶函数

高阶组件(HOC)

高阶组件是参数为组件,返回值为新组件的函数

就是传入一个组件,对这个组件进行一些功能的增强,再返回出来新的组件

  • 注意: 首先 高阶组件 本身不是一个组件,而是一个函数 其次,这个函数的参数是一个组件,返回值也是一个组件
  • HOC 是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式
    • 高阶组件在一些React第三方库中非常常见:
      • 比如redux中的connect;
      • 比如react-router中的withRouter;
    // 定义一个高阶组件
    function hoc(Cpn) {
      // 1.定义类组件
      class NewCpn extends PureComponent {
        render() {
          return <Cpn name="why"/>
        }
      }
      //组件名称可以通过 displayName 来修改
      NewCpn.displayName = "NewComponet"
      return NewCpn

      // 定义函数组件
      // function NewCpn2(props) {

      // }
      // return NewCpn2
    }

    class HelloWorld extends PureComponent {
      render() {
        return <h1>Hello World</h1>
      }
    }
    //调用过程
    const HelloWorldHOC = hoc(HelloWorld)

    export class App extends PureComponent {
      render() {
        return (
          <div>
            <HelloWorldHOC/>
          </div>
        )
      }
    }

高级组件应用的场景

  • props的增强

image.png

    // 定义组件: 给一些需要特殊数据的组件, 注入props
    function enhancedUserInfo(OriginComponent) {
      class NewComponent extends PureComponent {
        constructor(props) {
          super(props)

          this.state = {
            userInfo: {
              name: "coderwhy",
              level: 99
            }
          }
        }

        render() {
          return <OriginComponent {...this.props} {...this.state.userInfo}/>
        }
      }

      return NewComponent
    }
//调用
const Home = enhancedUserInfo(function(props) {
  return <h1>Home: {props.name}-{props.level}-{props.banners}</h1>
})
//传入数据
export class App extends PureComponent {
  render() {
    return (
      <div>
        <Home banners={["轮播1", "轮播2"]}/>
      </div>
    )
  }
}

  • 利用高阶组件来共享Context

//1.高阶组件-实现数据共享
function withTheme(OriginComponment) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {
          value => {
            return <OriginComponment {...value} {...props}/>
          }
        }
      </ThemeContext.Consumer>
    )
  }
}

export default withTheme

//2.调用
export class Product extends PureComponent {
  render() {
    const { color, size } = this.props

    return (
      <div>
        <h2>Product: {color}-{size}</h2>
      </div>
    )
  }
}

export default withTheme(Product)

//传入数据
export class App extends PureComponent {
  render() {
    return (
      <div>
        <ThemeContext.Provider value={{color: "red", size: 30}}>
          <Product/>
        </ThemeContext.Provider>
      </div>
    )
  }
}

  • 渲染判断鉴权

    //封装实现鉴权
    function loginAuth(OriginComponent) {
      return props => {
        // 从localStorage中获取token
        const token = localStorage.getItem("token")
    
        if (token) {
          return <OriginComponent {...props}/>
        } else {
          return <h2>请先登录, 再进行跳转到对应的页面中</h2>
        }
      }
    }
    
    export default loginAuth
    
-   生命周期劫持

```js
    /**
     * 高阶组件:用于计算 React 组件的渲染时间,并将结果打印到控制台中。
     *在 `UNSAFE_componentWillMount` 钩子函数中,我们记录了组件渲染开始的时间戳。
     *在 `componentDidMount` 中,我们记录了组件渲染结束的时间戳,并计算出渲染时间的间隔。
     * @param {React.ComponentType} OriginComponent - 要增强的组件
     * @returns {React.ComponentType} 增强后的组件
     */
    function logRenderTime(OriginComponent) {
      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(`当前${OriginComponent.name}页面花费了${interval}ms渲染完成!`)
        }

        render() {
          // 渲染原始组件,并传递所有 props 属性
          return <OriginComponent {...this.props}/>
        }
      }
    }

    export default logRenderTime

  • ....

HOC也有自己的一些缺陷:

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;

ref的转发

  • 在开发中我们可能想要获取函数式组件中某个元素的DOM
    • 通过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>
        )
      }
    }

Portals的使用

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的)。
  • Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
    • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment
    • 第二个参数(container)是一个 DOM 元素;

image.png

image.png

    //modal组件
    export class Modal extends PureComponent {
      render() {
        return createPortal(this.props.children, document.querySelector("#modal"))
      }
    }

    export default Modal

    // 在app组件中使用 Portals
    export class App extends PureComponent {
      render() {
        return (
          <div className='app'>
            <h1>App H1</h1>
            {
              createPortal(<h2>App H2</h2>, document.querySelector("#why"))
            }

            {/* 2.Modal组件 */}
            <Modal>
              <h2>我是标题</h2>
              <p>我是内容, 哈哈哈</p>
            </Modal>
          </div>
        )
      }
    }

fragment

  • 可以不适用 div 包裹在最外层,使用 fragment代替
     render() {
        const { sections } = this.state

        return (
          <>
            <h2>我是App的标题</h2>
            <p>我是App的内容, 哈哈哈哈</p>
            <hr />

            {
              sections.map(item => {
                return (
                  <Fragment key={item.title}>
                    <h2>{item.title}</h2>
                    <p>{item.content}</p>
                  </Fragment>
                )
              })
            }
          </>
        )
      }

StrictMode

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

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

    • 识别不安全的生命周期:
    • 使用过时的ref API
    • 检查意外的副作用 这个组件的constructor会被调用两次; 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用; 在生产环境中,是不会被调用两次的;
    • 使用废弃的findDOMNode方法 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了,可以自行学习演练一下
    • 检测过时的context API 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的
  • 可以为应用程序的任何部分启用严格模式:

    • 不会对 Header 和 Footer 组件运行严格模式检查;
    • 但是,ComponentOneComponentTwo 以及它们的所有后代元素都将进行检查

image.png