1. Portal

  • 官方解释:Portals 提供了一个最好的在父组件包含的DOM结构层级外的DOM节点渲染组件的方法。其实它的作用我们可以认为是传送门, 它能够帮助我们在父组件的外部渲染出子组件,应用场景例如:当子组件需要从视觉上“跳出”其容器时,譬如对话框、悬浮卡、提示框等。

  • 例如antUI组件库中的弹出框:(在点击的瞬间干组件下弹出一个新的div)

image

  • 我们也许会说,使用css样式中的定位就可以实现,但其实这种方法是存在弊端的,因为在使用css样式的过程中必然会存在父子之间的层级关系,那么此时当父组件的dom元素有 overflow:hidden 或者z-inde 样式,子层级就会受到它的影响从而出现问题,但是Portal不会有这种问题,因为他是可以指定位置的渲染html结构,也就是所可以避免出现父子层级结构的出现!

案例展示:

  • 使用Portal创建一个遮罩层!

APP.js(根组件)

/* ----------------- 根组件 ---------------------- */
import React, { Component } from 'react'
import './App.css'
import PortalDialog from './compoents/PortalDialog'// 引入我们封装好的portal组件
export default class App extends Component {
// 创建状态维护是否展示portal组件(遮罩层)
state = {
isShow: false// 是否展示遮罩层
}
render() {
return (
<div className="box" onClick={() => {
// portal支持事件冒泡
/*
虽然通过portal渲染的元素在父组件的盒子之外,
但是渲染的dom节点仍在React的元素树上,在那个dom元素上
的点击事件仍然能在dom树中监听到。
*/
console.log("box身上监听的事件")
}}>
{/* 左边盒子 */}
<div className="left"></div>
{/* 右边盒子 */}
<div className="right">
<button onClick={() => {
this.setState({
isShow: true
})
}}>点击</button>
{
this.state.isShow && <PortalDialog onClose={() => {
// 子传父通信使用props传递回调函数
this.setState({
isShow: false
})
}}>
<div>插槽元素1</div>
<div>插槽元素2</div>
<div>插槽元素3</div>
</PortalDialog>
}
</div>
</div>
)
}
}

Portal组件(遮罩层)

// 使用 portal 设置传送门组件
import React, { Component } from 'react'
import { createPortal } from 'react-dom'// 引入createPortal

/*
createPortal(参数1,参数2)接收两个参数:
1. 需要渲染的html结构(比如元素,字符串或者html片段等)
2. 传送的位置(DOM元素)
*/

export default class Dialog extends Component {
render() {
return createPortal(
// 需要渲染的html片段
<div style={{
width: '100%', height: '100%', position: 'fixed',
left: 0, top: 0, background: 'rgba(0,0,0,0.7)',
color: "white"
}}>
Dialog-
<div>loading-正在加载中</div>
{/* 支持使用插槽 */}
{this.props.children}
<button onClick={this.props.onClose}>close</button>
</div>,
// 渲染到页面的body标签内
document.body)
}
}

样式代码:

*{/* 清除浏览器默认样式 */
margin:0;
padding:0;
}

.left,.right{/* 左右盒子基本样式 */
height: 100vh;
}

.box{
display:flex;
}

.left{
width: 200px;
background:yellowgreen;
position: relative;
}

.right{
flex:1;
background:skyblue;
position: relative;
}

结果展示:

image

2. 懒加载(LazySuspense)

  • 懒加载是什么?预加载又是什么? 我们在项目中正常的引入一些资源或者组件的时候通过是使用import或者require来直接引入的,这会导致项目在启动的时候就会全部加载我们使用的资源或组件,这就是预加载,但是当资源或者组组件过大的时候就会出现项目启动时间过程,用户体验就不好了,这时我们就可以使用React中的懒加载,所谓懒加载其实我们可以认为是动态加载资源或组件,当我们用到它的时候就加载它,这就可以优化当项目的内容丰富,页面多的时候,首屏同时加载过多的内容,会导致卡顿不流畅响应速度慢、用户等待时间过长等问题。

基本使用:

  • 点击对应的按钮显示对应的组件
/* ----------------------- 根组件(APP.js) -------------------------- */
import React, { Component, Suspense } from 'react'// 引入Suspense

/* ------------------------ 正常加载(正常引入) --------------------- */
// import Comingsoon from './components/Comingsoon'
// import Nowplaying from './components/Nowplaying'

/* ------------------------ 懒加载(React.lazy) ---------------------- */
const Nowplaying = React.lazy(() => import('./components/Nowplaying'))// 动态加载
const Comingsoon = React.lazy(() => import('./components/Comingsoon'))// 动态加载
export default class App extends Component {
state = {
type: 1
}
render() {
return (
<div>
{/* 点击修改状态 */}
<button onClick={() => {
this.setState({
type: 1
})
}}>正在热映</button>
<button onClick={() => {
this.setState({
type: 2
})
}}>即将上映</button>

{/* 需要使用Suspense来结合使用,不然会报错,且fallback必须要有内容 */}
<Suspense fallback={<div>正在加载中....</div>}>
{
this.state.type === 1 ?
<Nowplaying></Nowplaying>
:
<Comingsoon></Comingsoon>
}
</Suspense>

</div>
)
}
}

结果展示:

image

总结:

单独使用React.lazy时项目是会报错的,因为在React使用了 lazy 之后,会存在一个加载中的空档期,React不知道在这个空档期中该显示什么内容,所以需要我们指定。接下来就要使用到suspense加载指示器,且注意,在Suspense 使用的时候, fallback 一定是存在且有内容的(可以为html片段,或者一段jsx代码), 否则会报错。

3. forwordRef(透传Ref)

  • 引用传递(Refforwading)是一种通过组件向子组件自动传递 引用ref 的技术。对于应用者的大多数组件来说没什么作用。但是对于有些重复使用的组件,可能有用。例如某些input组件,需要控制其focus,本来是可以使用ref来 控制,但是因为该input已被包裹在组件中,这时就需要使用Ref forward来透过组件获得该`input的引用。可以透传多层

案例展示:

  • 在父组件中使用ref获取子组件中的inputDOM节点

1. 未使用forwardRef

/* -------------------- 未使用forwardRef(过于繁琐) ------------------- */

/*
实现原理:
在子组件中使用ref属性获取对应的DOM节点,并在其挂载完成的生命周期中
通过子传父props方式(回调函数),将该节点传递给父组件,父组件再使用
链式变量去接受该DOM节点,最后再实现控制即可
*/
import React, { Component } from 'react'

/* ------------------------------- 父组件 --------------------------------- */
export default class App extends Component {
mytext = null// 设置一个临时变量用于接收子组件传递过来的DOM节点
render() {
return (
<div>
<button onClick={()=>{
// console.log('子组件的DOM节点',this.mytext)
this.mytext.current.focus()//获取子组件中input的焦点
this.mytext.current.value=""//清空它的内容
}}>获取焦点</button>

<Child callback={(el)=>{
// 将子组件传递过来的DOM节点赋值给父组件中的临时变量
this.mytext = el
}}/>
</div>
)
}
}

/* ------------------------- 子组件 ------------------------------ */
class Child extends Component{
mytext= React.createRef()// 创建ref属性

// 子组件创建时将该ref下指定的DOM和节点传递给父组件
componentDidMount() {
this.props.callback(this.mytext)
}

render(){
return <div style={{background:"yellow"}}>
{/* 使用ref属性获取请指定的DOM元素 */}
<input defaultValue="11111111" ref={this.mytext}/>
</div>
}
}

2. 使用forwardRef(透传属性)后

/* -------------------- 使用forwardRef(简单方便) ------------------- */

/*
实现原理:
所谓透传属性就是在父组件中同样能够使用ref属性获取到子组件中的DOM节点,我们
直接在父组件中创建ref,随后直接在子组件中使用ref属性传递我们创建的ref,
子组件在使用高阶组件forwardRef后返回一个全新的组件,此时就能接受到父组件
传递过来的ref属性,随后直接在子组件内部给对应的DOM节点表上ref属性即可,此时,
父组件就能通过对应的ref属性获取到子组件中对应的DOM节点了

ref属性(父组件创建)
--> 通过forwardRef透传到子组件中
--> 标上对应的DOM元素(子组件中)
--> 父组件通过ref属性访问子组件中的DOM元素
*/
import React, { Component, forwardRef } from 'react'// 引入forwardRef(透传属性)

/* --------------------- 父组件 ------------------------ */
export default class App extends Component {
mytext =React.createRef()// 在父组件中创建ref

render() {
return (
<div>
<button onClick={()=>{
console.log('获取到子组件中的DOM元素',this.mytext)
this.mytext.current.value=""// 获取到子组件中input的value属性
this.mytext.current.focus()// 获取到子组件中input的焦点
}}>获取焦点</button>

{/* 直接给子组件传递ref属性获取子组件内部的DOM节点 */}
<Child ref={this.mytext}/>
</div>
)
}
}

/* --------------- 子组件 ---------------- */
/*
使用forwardRef(高阶组件)包裹,将该函数式组件变成一个全新的forwardRef组件,
内部接受两个参数:props和ref,该ref就是父组件透传过来的ref,我们直接在子组件
内部的DOM元素中标上ref属性即可,这样父组件就可以通过ref属性获取到对应的DOM节点了
*/
const Child = forwardRef((props,ref)=>{
return <div style={{background:"yello"}}>
{/* 标上父子组件传递过来的ref属性 */}
<input ref={ref} defaultValue="1111"/>
</div>
})

结果展示:

image

注意:使用forwardRef属性只能透传一个Ref,想要透传多个的话只能使用上面的常规方法了!

4. memo

  • 一般来讲,有父子层级的组件,父组件在重新渲染的时候,里面的子组件也会重新渲染,也就是说:只要父组件的某一个状态改变,父组件下面所有的子组件不论是否使用了该状态,都会进行重新渲染。这就导致了一个问题: 父组件中的状态发生改变,重新渲染是无可避免的,但是如果此时子组件中并没有发生变化,那么此时子组件也重新渲染这个操作就显得多余了,而且还浪费性能!此时就引出了memo高阶组件,与之类似的还有PureComponent,两者同样是用于优化子组件频繁渲染的问题的

  • memo的作用: memoreact的一种缓存技术,这个函数可以检测从父组件接收的props,并且在父组件改变state的时候比对这个state是否是本组件在使用,如果不是,则拒绝重新渲染。

案例展示:

  • 在父组件中设置两个按钮,点击第一个按钮,重新渲染父组件但是保持子组件不重新渲染,第二个按钮,重新渲染子组件
/* ------------------------------- memo的基本使用 --------------------------*/ 
import React, { Component, memo } from 'react'

/*
memo是react的一种缓存技术,这个函数可以检测从父组件接收的props,
并且在父组件改变state的时候比对这个state是否是本组件在使用,
如果不是,则拒绝重新渲染。
*/

/* ------------------------ 父组件 ---------------------------- */
export default class App extends Component {
state ={
name:"lam",// name属性在父组件中展示
title:"aaaaaa"// title属性,在子组件中展示
}
render() {
return (
<div>
{/* 第一个按钮,点击修改name状态(在父组件中展示) */}
<button onClick={()=>{
this.setState({
name:"肥林"
})
}}>点击更新name属性(不更新子组件)</button>

{/* 第二个按钮,点击修改title状态(在子组件中展示) */}
<button onClick={()=>{
this.setState({
title:"bbbbbbbbbb"
})
}}>点击更新title属性(更新子组件)</button><br/>

{/* 展示name属性 */}
{this.state.name}
{/* 使用props将title状态传给子组件 */}
<Child title={this.state.title}/>
</div>
)
}
}

/* -------------------------- 子组件 ---------------------------- */
/*
使用memo函数包裹子组件(函数式组件)
*/
const Child = memo((props)=>{
console.log('子组件重新更新了!')
return <div>子组件-{props.title}</div>
})

结果展示:

image

memoPureComponent区别

  • PureComponent 只能用于class组件,memo 用于functional 组件

memo存在的问题:

  • memo只能进行浅拷贝来校验决定是否触发重新渲染。例如,当我们在父组件给子组件props传递一个数组(对象)时,我们修改数组中的一个数组(如push或者删除),那么此时的子组件中的props改变了,理论上来讲,子组件应该会重新渲染,但实际上不会! 那是因为我们修改数组时,理论上看似数组变了,实际上,变的只是堆中的数据,而我们的props是指向存在栈中数据的地址,修改数组时地址是不会改变的,因此memo检测到的是指向栈的地址没有改变,检测不到栈中的数据有没有改变, 因此要想解决这个问题,就需要向props传递一个新的数组。

  • 由此可见,memo只能进行浅拷贝来校验决定是否触发重新渲染。所以改变数组(对象)props时候记得返回一个全新的数组(对象)

const [list,setList] = useState([1,2,3,4,5]);

setList(list.push(1)); //这样是不会被memo检测到的,是无法触发memo更新的

setList([...list,1]); //这样才可以,创建一个新数组,再在里面解构旧数组,往后面追加 1
//这样,就等于返回了一个新的数组,栈中的地址就会改变,memo就可以检测到并触发更新