likes
comments
collection
share

React18 - 组件化开发(四)

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

受控/非受控组件

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

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

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

我们将两者结合起来,使React的state成为“唯一数据源”, 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作

被 React 以这种方式控制取值的表单输入元素就叫做“受控组件“

由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源

import React, { PureComponent } from 'react'

export class App extends PureComponent {
	constructor() {
		super()

		this.state = {
			value: ''
		}
	}

	render() {
		return (
			<div>
				{/*
					对于这类input input输入的内容是由原生的input进行维护的
					并不是由react的state来进行管理的
					所以这类组件就被称之为非受控组件
				*/}
				<input onChange={e => this.getInputVaule(e)} />
				<div>{ this.state.value }</div>
			</div>
		)
	}

	getInputVaule(e) {
		this.setState({
			// 虽然react事件中的e是react的合成事件对象
			// 但是依旧可以通过e.target.value获取到对应输入的值
			value: e.target.value
		})
	}
}

export default App
import React, { PureComponent } from 'react'

export class App extends PureComponent {
	constructor() {
		super()

		this.state = {
			value: 'default value'
		}
	}

	render() {
		return (
			<div>
				{/*
					在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源
					我们修改value值只能通过触发对应的onChange事件,如果只设置了value属性值但是没有绑定onChange事件,表单组件将变成只读组件
					在 React 中,可变状态(mutable state)保存在组件的 state 属性中,并且只能通过使用 setState()来更新
					这类表单组件就被称之为受控组件
				*/}
				<input value={this.state.value} onChange={e => this.getInputVaule(e)} />
				<div>{ this.state.value }</div>
			</div>
		)
	}

	getInputVaule(e) {
		this.setState({
			value: e.target.value
		})
	}
}

export default App
import React, { PureComponent } from 'react'

export class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      name: 'Klaus',
      isAgree: false,
      singleFruit: '',
      multFruits: [],
      hobbies: [
        { value: 'basketball', label: '篮球', isChecked: false },
        { value: 'football', label: '足球', isChecked: false },
        { value: 'tennis', label: '网球', isChecked: false },
      ],
      fruits: [
        { value: 'apple', label: 'apple', isSelected: false },
        { value: 'orange', label: 'orange', isSelected: false },
        { value: 'banana', label: 'banana', isSelected: false },
      ]
    }
  }

  handleSubmit(e) {
    // 阻止表单的默认提交行为
    e.preventDefault()

    const { name, isAgree, hobbies, singleFruit, multFruits } = this.state

    console.log('name', name)
    console.log('isAgree', isAgree)
    console.log('singleFruit', singleFruit)
    console.log('multFruits', multFruits)

    const checkedHobbies = hobbies.filter(hobby => hobby.isChecked).map(hobby => hobby.value)
    console.log('checkedHobbies', checkedHobbies)
  }

  handleChange(e) {
    // 通过e.target.type来判断input是文本输入框还是单选按钮
    // 如果是文本输入框和下拉单选列表 对应的值 位于 e.target.value
    // 对于单选按钮 对应的值 位于 e.target.checked
    // 对应的属性名可以通过e.target.name来获取,并通过计算属性名来进行设置
    this.setState({
      [e.target.name]: e.target.type === 'checkbox' ? e.target.checked : e.target.value
    })
  }

  handleHobbyChange(index) {
    const hobbies = [...this.state.hobbies]
    hobbies[index].isChecked = !hobbies[index].isChecked
    this.setState({ hobbies })
  }

  handleSelectChange(e) {
    this.setState({
      // e.target.selectedOptions 可以获取多选下拉列表中,所有被选中的元素项 类别为类数组对象
      // 所有的类数组对象 都是可迭代对象
      // Array.from(可迭代对象,mapFn)
      multFruits: Array.from(e.target.selectedOptions, item => item.value)
    })

  }

  render() {
    const { name, isAgree, hobbies, singleFruit, multFruits, fruits } = this.state

    return (
      <form onSubmit={ e => this.handleSubmit(e) }>
        <div>
          <label htmlFor="name">
            姓名<input type="text" id='name' name='name' value={ name } onChange={ e => this.handleChange(e) } />
          </label>
        </div>

        <div>
          <label htmlFor="isAgree">
            <input type="checkbox" id='isAgree' name='isAgree' checked={ isAgree } onChange={ e => this.handleChange(e) } />
            同意协议
          </label>
        </div>

        <div>
          {/* 多选框 */}
          爱好:
            {
              hobbies.map((hobby, index) => (
                <label
                  htmlFor={ hobby.value }
                  key={hobby.value}
                >
                  <input
                    type="checkbox"
                    id={ hobby.value }
                    checked={ hobby.isChecked }
                    onChange={ () => this.handleHobbyChange(index) }
                  />
                  { hobby.label }
                </label>
              ))
            }
        </div>

        <div>
          <select name='singleFruit' value={singleFruit} onChange={ e => this.handleChange(e) }>
            {
              fruits.map(fruit => (
                <option
                  key={ fruit.value }
                  value={ fruit.value }
                >
                  { fruit.label }
                </option>
              ))
            }
          </select>
        </div>

        {/* 多选下拉列表 */}
        <div>
          <select multiple value={multFruits} onChange={ e => this.handleSelectChange(e) }>
            {
              fruits.map(fruit => (
                <option
                  key={ fruit.value }
                  value={ fruit.value }
                >
                  { fruit.label }
                </option>
              ))
            }
          </select>
        </div>

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

export default App

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

因为在一个受控组件中,表单数据是由 React 组件来统一进行管理

而非受控组件,表单数据将交由 DOM 节点来处理,并不是交给React来统一进行管理

非受控组件在绑定默认值的时候

标签默认值
<select> 和 <textarea>defaultValue
<input type="checkbox"> 和 <input type="radio">defaultChecked
import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      name: "Klaus"
    };
  }

  handleChange(e) {
    this.setState({
      name: e.target.value
    });

    console.log(this.state.name);
  }

  render() {
    const { name } = this.state;

    return (
      <div>
        <input
          type="text"
          defaultValue={name}
          onChange={(e) => this.handleChange(e)}
        />
      </div>
    );
  }
}

export default App;

模拟实现v-lazy效果

在react中的onChange事件,会在输入内容发生改变的时候,实时更新对应的状态

类似于原生的oninput事件,和原生的onchange事件行为正好相反

import React, { PureComponent } from 'react'

export class App extends PureComponent {
	constructor(props) {
		super(props)

		this.state = {
			username: ''
		}
	}

	render() {
		return (
			<div>
				<input type="text" value={this.state.username} onChange={e => this.handleChange(e)} />
				<div>{ this.state.username }</div>
			</div>
		)
	}

	handleChange(e) {
		this.setState({
			username: e.target.value
		})
	}
}

export default App

如果我们希望在输入的时候可以实现类似于vue中lazy修饰符的功能的时候

就不可以将对应表单元素设置为受控组件,因为受控组件必须实现onChange事件

而为了实现类似于lazy修饰符的功能,我们需要使用onBlur事件

import React, { PureComponent } from 'react'

export class App extends PureComponent {
	constructor(props) {
		super(props)

		this.state = {
			username: ''
		}
	}

	render() {
		return (
			<div>
				<input type="text" defaultValue={this.state.username} onBlur={e => this.handleChange(e)} />
				<div>{ this.state.username }</div>
			</div>
		)
	}

	handleChange(e) {
		this.setState({
			username: e.target.value
		})
	}
}

export default App

HOC

高阶组件的英文是 Higher-Order Components,简称为 HOC

高阶组件本质上就是一个函数,但该函数需要满足如下两点:

  1. 接收一个组件作为参数
  2. 返回一个新的组件

所以高阶组件可以对我们传入的组件,进行拦截,进行增强处理后再返回

所以高阶组件也是react中代码抽离并复用的一种方式

高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式

HOC的意义

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

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

所以React提供了HOC来实现代码的复用

但是HOC也存在如下缺点:

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

所以在实际开发中,在不使用react hook的情况下,推荐使用HOC来抽离公用代码,

但是如果使用react hook,推荐使用react hook 来替代 HOC,从而实现对应的功能

注入props

with-props.jsx

import { PureComponent } from 'react';

export default function enchangePropsFn(OriginCpn) {
	return class enhancedCpn extends PureComponent {
		constructor(props) {
			super(props)

			this.state = {
				// 在这里定义一些需要注入的数据
				userInfo: {
					name: 'Klaus',
					age: 23
				}
			}
		}

		render() {
			// 在进行props注入的时候,也需要将原本传入的props一起传递进去
			return <OriginCpn {...this.props} {...this.state.userInfo} />
		}
	}
}

使用

import React, { PureComponent } from 'react'
import withProps from '../../../utils/with-props'

class Foo extends PureComponent {
	render() {
		return (
			<div>Foo: {this.props.name} - {this.props.age}</div>
		)
	}
}

export default withProps(Foo)
import React, { memo } from 'react'
import withProps from '../../../utils/with-props'

const Home = memo(props => {
	return (
		<div>Home - {props.name} - {props.age}</div>
	)
})

export default withProps(Home)

共享context

import { ThemeContext } from '../context/themeContext'

export default function enhance(OriginCpn) {
	return function enhanceCpn(props) {
		return (
			<ThemeContext.Consumer>
				{
					value => <OriginCpn {...value} {...props} />
				}
			</ThemeContext.Consumer>
		)
	}
}

portals

通常来讲,当执行组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点

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

如在实现弹出框的时候,一般推荐将弹出框挂载到body下边,而不是离其最近的父节点下边

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

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

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
		<div id="app"></div>
		<div id="modal"></div>
  </body>
</html>

App.jsx

import React, { PureComponent } from 'react'
// createPortal 是在 react-dom 中导出的
import { createPortal } from 'react-dom'

export class index extends PureComponent {
	render() {
		return (
			<div>
				<div>something in div.root</div>
				{
					// createPortal本质上也是一个高阶组件
					// createPortal接收两个参数
					// 参数一 => 需要挂载的元素内容
					// 			 => 只要是JSX中支持挂载的内容,都可以作为该方法的第一个参数被传入
					// 参数二 => 需要挂载的新的挂载点 - DOM节点
					createPortal(
						<div>something in div.app</div>,
						document.getElementById('app')
					)
				}
        {
          // createPortal方法的第一个参数可以是children数组
					createPortal(
						(
							<div>
								<div>something in div.modal</div>
								<div>other thing in div.modal</div>
							</div>
						),
						document.getElementById('modal')
					)
				}
			</div>
		)
	}
}

export default index

fragment

在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素

如果希望可以不渲染这样一个div, 可以使用fragment

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

React还提供了Fragment的短语法, 它看起来像空标签 <> </>

但是,如果我们需要在Fragment中添加key,那么就不能使用短语法

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

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

export default App
import React, { PureComponent } from 'react'

export class App extends PureComponent {
  render() {
    // 使用短语法的时候,可以不引入Fragment
    // 在编译的时候,会自动引入Fragment
    return (
      <>
        App
      </>
    )
  }
}

export default App

StrictMode

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

与 Fragment 一样,StrictMode 不会渲染任何可见的 UI

它为其后代元素触发额外的检查和警告

严格模式检查仅在开发模式下运行;它们不会影响生产构建

import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

// 全局开启严格模式
ReactDOM.createRoot(document.querySelector('#root')).render((
	<StrictMode>
    <App />
  </StrictMode>
))
import React, { Fragment, PureComponent, StrictMode } from 'react'
import Foo from './components/Foo'
import Home from './components/Home'

export class App extends PureComponent {
  render() {
    return (
      <Fragment>
        {/* 局部开启严格模式 */}
				<StrictMode>
					<Home />
				</StrictMode>

				<Foo />
      </Fragment>
    )
  }
}

export default App

作用

  1. 识别不安全的生命周期
  2. 使用过时的ref API
  3. 检查意外的副作用
    • 这个组件的constructor会被调用两次
    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
    • 在生产环境中,是不会被调用两次的
  4. 使用废弃的findDOMNode方法
    • 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
转载自:https://juejin.cn/post/7140008840052867108
评论
请登录