React高阶组件
所谓的组件的进阶用法其实就是通过函数来返回组件,这种用法主要起到的就是一个组件复用的作用,这种组件我们也可以叫做高阶组件(HOC)
举例:
这里我们通过类组件来说明下情况,我们有两个类组件,其内部有部分内容是相似或者是完全一样的,这个时候,我们根据以前学习过的的封装思维,讲公共的部分提取做出来进行封装来提高代码的复用率
现在有如下代码:
现在有Son1和Son2两个子组件在Hoc父组件中渲染,两个子组件有相同的state,相同的需要渲染的标签结构,还有相同的changeData方法,但是修改的state值不一样
import React, { Component } from 'react'class Son1 extends Component {state = {userName:"张三"}changeData(){this.setState({userName:"李四"})}render(){return (子组件1:{this.state.userName}
)}}class Son2 extends Component {state = {userName:"张三"}changeData(){this.setState({userName:"王五"})}render(){return (子组件2:{this.state.userName}
)}}export default class Hoc extends Component {render() {return ()}}
在上面的代码中,有很多相同的部分,那我们是不是可以把公共的部分提取出来进行二次封装从而提升代码的复用率
对以上代码做如下修改:
import React, { Component } from 'react'class Son1 extends Component {render(){return (子组件1:{this.props.userName}
)}}class Son2 extends Component {render(){return (子组件2:{this.props.userName}
)}}const HOCfunc = (newName,Comp) => {return class extends Component{state = {userName:"张三"}changeData(){this.setState({userName:newName})}render(){return ()}} }let Child1 = HOCfunc("李四",Son1)let Child2 = HOCfunc("王五",Son2)export default class Hoc extends Component {render() {return ()}}
代码分析:
把Son1和Son2中的state和changData这两个部分提取做出,单独封装在了另外一个组件中,这个组件写在HOCfunc这个函数的返回值上并且是一个匿名组件,当我们调用HOCfunc这个函数的时候,就会把这个匿名组件返回出来,并根据函数调用时传入的参数来决定返回的组件中的之前在没提取出来封装之前不同的地方
其中比较有特点的就是Comp这个参数,这个参数我们这里设定成了一个接收组件作为实参的形参,这里每次调用的时候可以传入不同的组件作为HOCfunc返回组件的子组件使用,其实也就相当于是决定自己本身的标签结构
这里的代码执行逻辑,其实与我们最早学习过的构造函数如出一辙,通过new调用一个构造函数来实例化一个基于该构造函数为模板的对象出来的
练习:
我们在上面的例子中可以看到,其实Son1和Son2的标签结构也都是一样的,就是里面有点文字内容不太一样,把Son1和Son2的render部分也提取出来
import React, { Component } from 'react'const HOCfunc = (newName,textVal) => {return class extends Component{state = {userName:"张三",text:textVal}changeData(){this.setState({userName:newName})}render(){return ({this.state.text}:{this.state.userName}
)}} }let Child1 = HOCfunc("李四","组件1")let Child2 = HOCfunc("王五","组件2")export default class Hoc extends Component {render() {return ()}}
组件懒加载
React实现懒加载需要使用以下两个东西:
React.lazy()
在lazy方法内传入一个回调函数,在回调函数内通过import(‘…/xxxxx’) 导入组件,之lazy方法会把你通过import导入的组件包装成一个懒加载组件返回出来在调用懒加载组件的时候需要给它嵌套一个Suspense组件,然后通过其属性fallback设置一个在懒加载过程中需要显示的内容
举例:
新建一个components文件夹,在这里随便制作一个组件child.js,作为懒加载组件使用
import React, { Component } from 'react'export default class Child extends Component {render() {return (我是一个懒加载组件)}}
要把一个组件做成一个懒加载组件是在把它导入到另外一个组件的时候通过Reac.lazy方法来实现的
新建一个lazyLoad组件,把Child组件导入
import React, { Component, Suspense } from 'react'//通过lazy方法导入child组件把其包装成一个具备懒加载效果的组件const Child = React.lazy(() => import('../components/Child'))export default class Lazyload extends Component {render() {return ({/* 在Child组件外层嵌套Suspense组件,通过fallBack来设置懒加载期间显示的内容 */})}}
最后把这个lazyLoad组件导入到App顶层组件中进行渲染,我们可以通过调整浏览器的网络情况来看到结果
备注:
我们可以把fallback的值直接改成一个专门的loading动画组件也是ok的
React hooks的闭包陷阱
先来看一个例子
import React, { useEffect, useState } from 'react'const Test = () => {const [count,setCount] = useState(0)useEffect(() => {setInterval(() => {console.log(count)},1000)},[])const changeCount = () => {setCount(count + 1)}return (count的值是:{count})}export default Test
代码分析:
现在我们会发现,我们点击按钮执行count+1的操作,页面上渲染的结果没有问题,但是在在控制台打印的却一致都是0,这里其实就是闭包带来的问题,我们先来分析两个问题
问题1:当我们点击按钮执行了setCount(count + 1)之后会发生些什么?
之前我们在讲hook函数的时候就说过,函数组件内的状态发生改变的时候是会将整个组件重新渲染一遍,也就相当于是重新把整个作为组件的函数重新执行一遍,而函数组件本质就是一个函数,而在函数内创建的状态其实本质就是一个局部变量,那么随着函数的执行完毕,局部变量就自然会被释放掉,所以,我们认为每次的重新渲染创建的状态其实都是一个全新的count,与上一次执行的count没有多大关系
问题2:闭包会带来什么影响?
闭包其实简单解释就是上级作用域内的变量,因为被下级作用域引用,而导致无法被释放,必须要等到下级作用域执行完毕才能被正常释放掉,但是这里,我们使用了setInterval在其内部调用了test组件的状态(test函数的局部变量),而setInterval如要执行完毕必须要使用clearInterval才行,所以就导致在setInterval内部调用的状态一致无法被释放掉,从而反复调用的都是第一次执行时创建的count值
解决方案:使用useRef
import React, { useEffect, useRef, useState } from 'react'const Test = () => {const countRef = useRef(0)useEffect(() => {setInterval(() => {console.log(countRef.current)},1000)},[])const changeCount = () => {countRef.current++}return (count的值是:{countRef.current})}export default Test
代码分析:
这里我们借鉴了类组件中的this的特点,因为this的指向在组件的生命周期中是不变的,所以它永远都会指向当此创建出来的类组件,而useRef我之前只用在获取DOM对象上,而对象也是一种数据结构,我们平时也会用useState创建对象结构的状态来使用,我们通过将useRef创建出来的数据赋值给countRef完全了Ref数据的引用,也就是说现在的countRef只会指向useRef创建出来的数据,让countRef与useRef创建出来的数据形成了映射关系,达到了你变我也变的效果
但是组件的内容不会变化,因为ref.current
的改变并不会引起函数组件的重渲染,所以需要配合useState使用
import React, { useEffect, useRef, useState } from 'react'const Test = () => {const [count,setCount] = useState(0)const countRef = useRef(0)useEffect(() => {setInterval(() => {console.log(countRef.current)},1000)},[])const changeCount = () => {setCount(++countRef.current)//这里要换成前++,不然计算会落后一位}return (count的值是:{count})}export default Test
代码分析:
通过将每次递增计算的结果通过setCount再赋值给内部状态count,现在我们就是可以看到组件的内容与控制台打印的结果是一致的了,同时我们可以吧changeCount通过useCallback进行缓存,避免每次修改count重新执行test组件时都还要再创建一遍changeCount
const changeCount = useCallback(() => { setCount(++countRef.current)},[])
React18新特性Transition
React 18中,引入了一个新概念——transition
由此带来了一个新的API——startTransition
两个新的hooks函数useTransition
和usedeferredValue
在react中一旦状态state发生改变就会触发重新渲染,之前我们学习过useMemo和useCallback可以对组件中的state和方法进行缓存,并设置依赖项来做更新的前后对比,如果更新前后的结果一致就不重新渲染组件,但是只要前后有变化还是会立即更新组件,而这个更新过程在react中被划分成两类:
- 紧急更新任务(urgent update):用户期望马上响应的更新操作,例如鼠标单击或键盘输入。
- 过渡更新任务(transition update):一些延迟可以接受的更新操作,如查询时,搜索推荐、搜索结果的展示等。
而之前我们所讲过的内容中执行的都是紧急更新,如果想要实现过渡更新我们可以使用 startTransition 方法
举例:
import React, { startTransition, useEffect, useState } from 'react'const Transition = () => {const [first,setFirst] = useState('a');const [second,setSecond] = useState('b');const changeVal = () => {setFirst("zhangsan");startTransition(() => {//传入一个回调函数,执行second的修改setSecond("lisi")})}useEffect(() => {console.log(first,second);},[first])return ()}export default Transition
代码分析:
在上面的代码中,当我们点击触发changeVal的时候,执行了setFirst和setSecond修改了first和second的状态值,然后,我们监听first的变化执行控制台打印first和second,但是在控制台中的打印结果我们可以看到只有first的值变了,而second的值并没有变,照理说,虽然我们只监听了first,但是first和second的修改是在同一个方法下执行的,那么,打印的结果都应该是修改之后的状态
原因:
因为我们把setSecond标记成了过渡更新,而当某一个状态被标记成了过渡更新之后,在重新渲染的过程中就会有了渲染优先级这样一个情况,被transition标记的成了低优先级任务,而没有的成了高优先级,所以,当我们执行changeVal的时候,先执行了setFirst先修改了first状态值,而监听到first状态值的变化就立即触发了
console.log(first,second)
这个时候second还并没有修改到位注意:
react之前是没有渲染优先级这一说的,但是在react18中正式引入了并发渲染机制,从而实现了渲染优先级
什么是并发渲染机制?
前提场景:
当出现在数据量大,DOM节点元素多的场景下,一次的更新可能带来的处理量将会是非常巨大的,而所有的更新任务都是紧急更新的,在这种情况下浏览器会同时执行大量的渲染工作,而这种情况下带来的最直观的用户体验就是卡顿,特别是在硬件配置越低的设备越明显
并发渲染机制的目的:
根据用户的设备性能和网速对渲染过程进行适当的调整, 保证 React 应用在长时间的渲染过程中依旧保持可交互性,避免页面出现卡顿或无响应的情况,从而提升用户体验。而startTransition就是基于React V18新特性提供的API(方法)
注意:如果想使用v18版本提供的新特性需要使用createRoot来创建项目的根节点
//v18的创建方式import React from 'react';import ReactDOM from 'react-dom/client';import App from './App.js'const root = ReactDOM.createRoot(document.getElementById('root'));root.render( );
v18之前创建根节点的方式
import * as ReactDOM from 'react-dom'import App from './App'const root = document.getElementById('app')// v18 之前的创建方法ReactDOM.render(,root)
Transition
Transition 本质上是用于一些不是很急迫的更新上,用于解决并发渲染的问题,在 React 18 之前,所有的更新任务都被视为急迫的任务,在 React 18 诞生了 concurrent Mode
(并发渲染)模式,在这个模式下,渲染是可以中断,而被startTransiton标记的状态修改会被标记成低优先级任务被暂时中断,可以让高优先级的任务先更新渲染,从而实现在并发渲染模式下大量数据渲染造成的卡顿问题
举例:
import React, { startTransition, useState, memo } from 'react'const fakeDataArr = new Array(10000).fill(1); //模拟一万条数据,填充1const DataList = ({query}) => {console.log("开始渲染")return ({fakeDataArr.map((item,index) => {query})})}const MemoDataList = memo(DataList)const TransitionDemo = () => {const [isTransition,setTransition] = useState(false);//控制transition的开启关闭const [query,setQuery] = useState('');//获取输入框的数据(传递给子组件)const changeTransition = (e) => {if(isTransition){startTransition(() => {setQuery(e.target.value);})}else{setQuery(e.target.value)}}return ()}export default TransitionDemo
代码分析:
以上代码,我们通过isTransition的布尔值切换,来开启关闭transition状态
isTransition为false的关闭情况下:
我们的setQuery是紧急更新,在获取到输入框的value值之后就会立刻传递给MemoDataList,并被渲染一万条出来,而这每次的query值的修改都会造成子组件MemoDataList重新被渲染一万次,并且每次都还是紧急更新,结果就是一旦我们输入的速度快一点,输入框内的输入操作就会出现明显的卡顿情况
isTransition为true的开启情况下:
我们的setQuery被标记成了过渡更新,那么,每次在修改输入框的value值的时候,每修改一个字符所触发的更新,都会比上一次触发的优先级要低,这样就形成了一个类似于排队执行的状态,这样做,我们会发现,虽然渲染结果有一定的滞后性,但是输入框的卡顿情况已经消失了
扩展内容
看到这种情况你会发现,这个和我们之前讲过的防抖高度类似,但是两者在执行过程上差异还是比较大的
我们实现防抖依靠的是setTimeout进行一个延迟执行,控制了执行的频率,但是这种频率设置是固定的,如果控制不好延迟时长的话,延迟太长带来的操作滞后的感觉会非常强烈,延迟太短那么基本等于没有效果,而transition这时就体现出了优势,特别是在硬件性能较差的设备上,因为transition一定是会在上一个任务完成之后才执行下一个,而上一个任务要执行多久完全就看硬件的计算性能了
useTransition
上面我们已经介绍了startTransition,同时也讲到了过渡更新,而过渡更新是有一个过渡期的,在这个期间更新任务会被中断,useTransition可以为中断期间添加业务逻辑,而不是单纯的等待
useTransition返回一个数组,该数组内包含两个元素
- isPending过渡状态值,表示当前任务是否处于任务中断的状态
- 一个方法,这个方法就是上面讲过的startTransition
举例:将上面的demo替换成使用useTransition完成
import React, { useState, memo, useTransition } from 'react'const fakeDataArr = new Array(10000).fill(1); //模拟一万条数据,填充1const DataList = ({query}) => {console.log("开始渲染")return ({fakeDataArr.map((item,index) => {query})})}const MemoDataList = memo(DataList)const TransitionDemo = () => {const [query,setQuery] = useState('');const [isPending,startTransition] = useTransition();//解构获取的isPending等待状态值是一个布尔值const changeTransition = (e) => {startTransition(() => {setQuery(e.target.value);})}return (//根据isPending的值,来决定是否渲染文字{isPending && transition状态中
})}export default TransitionDemo
useDeferredValue
useDeferredValue可以把一个已经修改完成的状态值做延迟响应的处理,传入两个参数,带有一个返回值
参数1:需要延迟响应的状态值
参数2:延迟响应时长
返回值:返回一个新的带有延迟响应的状态值,当真实响应时长超过参数2的设置时长,强制更新
举例:对上面的例子做一个修改
import React, { useState, memo, useTransition, useDeferredValue } from 'react'const fakeDataArr = new Array(10000).fill(1); //模拟一万条数据,填充1const DataList = ({query}) => {console.log("开始渲染")return ({fakeDataArr.map((item,index) => {query})})}const MemoDataList = memo(DataList)const TransitionDemo = () => {const [query,setQuery] = useState('');const deferredQuery = useDeferredValue(query,{timeoutMs:1000}) //把修改好的状态延迟1秒更新const changeTransition = (e) => {setQuery(e.target.value);}return ()}export default TransitionDemo
useTransition与useDeferredValue的区别
useTransition与useDeferredValue两者都可以实现状态的过渡更新,只不过,他们所标记成transition状态的东西不一样,理解两者不同的关键点在于不要把状态值的修改和更新当成一步来看,要分成两步来理解
- useTransition:是把状态修改的过程进行了中断进入pending状态,并且可以获得一个pending状态值,当pending状态结束之后才执行state值的修改并更新
- useDeferredValue:先让状态执行了修改,然后把修改好的状态在即将更新的时候中断更新操作,并设置一个响应时长,如果上一级优先级的更新任务已经完全就算响应时长没到也立即更新,如果超过响应时长强制更新
前端东软老师的课件和课堂记录就到这里了,你们的点赞和关注让我确定自己分享的这些笔记是对别人有帮助的,个人真心觉得这些笔记很赞很通俗易懂,只是我最近一年不会看前端的知识了,打算考网络安全相关的研究生,欢迎大家在评论区留言讨论学习。把前端内容放在博客里,有需要的人可以各取所需,快过年了,给大家拜个早年吧
愿祝君如此山水, 滔滔岌岌风云起。
云程发轫 万里可期!