React 组件化开发高级
一、ref
1.1 ref获取DOM
-
在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:
- 管理焦点,文本选择或媒体播放
- 触发强制动画
- 集成第三方 DOM 库
-
目前可以通过 refs 来获取对应的DOM,有三种方式:
-
传入字符串(不推荐)
- 使用时通过
this.refs.
传入的字符串格式获取对应的元素
- 使用时通过
-
传入一个对象(推荐)
- 对象是通过
React.createRef()
方式创建出来的 - 使用时获取到创建的对象其中有一个current属性就是对应的元素
- 对象是通过
-
传入一个函数
- 该函数会在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 属性,因为他们没有实例
- 当 ref 属性用于 HTML 元素时,构造函数中使用
-
类组件的实例
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
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
- 官方的定义:高阶组件是参数为组件,返回值为新组件的函数
-
分析:
- 高阶组件 本身不是一个组件,而是一个函数
- 这个函数的参数是一个组件,返回值也是一个组件
-
写法:
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)
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> ) } }
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
-
严格模式检查什么?
-
识别不安全的生命周期
-
使用过时的ref API
- 使用 ref 绑定 string,会报错
-
检查意外的副作用
- 这个组件的constructor会被调用两次
- 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
- 在生产环境中,是不会被调用两次的
-
使用废弃的findDOMNode方法
- 在之前的React API中,可以通过
findDOMNode
来获取DOM,不过已经不推荐使用
- 在之前的React API中,可以通过
-
检测过时的context API
- 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的(已不推荐使用)
-
转载自:https://juejin.cn/post/7144544817609981983