很多同学用react
开发的时候,真正用到的React
的api
少之又少,基本停留在Component
,React.memo
等层面,实际react
源码中,暴露出来的方法并不少,只是我们平时很少用。但是React
暴露出这么多api
并非没有用,想要玩转react
,就要明白这些API
究竟是干什么的,应用场景是什么,今天就让我们从react
到 react-dom
,一次性把react
生产环境的暴露api
复习个遍(涵盖90%+)。
我们把react
,API
,分为组件类,工具类,hooks
,再加上 react-dom
,一共四大方向,分别加以探讨。
为了能让屏幕前的你,更理解api
,我是绞尽脑汁,本文的每一个api
基本都会出一个demo
演示效果,弥补一下天书般的react
文档😂😂😂,还有就是我对api
基本概念的理解。创作不易,希望屏幕前的你能给笔者赏个赞,以此鼓励我继续创作前端硬文。
老规矩,我们带着疑问开始今天的阅读(自测掌握程度)?
1 react
暴露的api
有哪些,该如何使用?2 react
提供了哪些自测性能的手段?3 ref
既然不能用在函数组件中,那么父组件如何控制函数子组件内的state
和方法?4 createElement
和cloneElement
有什么区别,应用场景是什么?5 react
内置的children
遍历方法,和数组方法,有什么区别?6 react
怎么将子元素渲染到父元素之外的指定容器中?...
我相信读完这篇文章,这些问题全都会迎刃而解?
组件类,详细分的话有三种类,第一类说白了就是我平时用于继承的基类组件Component
,PureComponent
,还有就是react
提供的内置的组件,比如Fragment
,StrictMode
,另一部分就是高阶组件forwardRef
,memo
等。
Component
是class
组件的根基。类组件一切始于Component
。对于React.Component
使用,我们没有什么好讲的。我们这里重点研究一下react
对Component
做了些什么。
react/src/ReactBaseClasses.js
js 体验AI代码助手 代码解读复制代码function Component(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue; }
这就是Component
函数,其中updater
对象上保存着更新组件的方法。
我们声明的类组件是什么时候以何种形式被实例化的呢?
react-reconciler/src/ReactFiberClassComponent.js
constructClassInstance
js 体验AI代码助手 代码解读复制代码function constructClassInstance( workInProgress, ctor, props ){ const instance = new ctor(props, context); instance.updater = { isMounted, enqueueSetState(){ /* setState 触发这里面的逻辑 */ }, enqueueReplaceState(){}, enqueueForceUpdate(){ /* forceUpdate 触发这里的逻辑 */ } } }
对于Component
, react
处理逻辑还是很简单的,实例化我们类组件,然后赋值updater
对象,负责组件的更新。然后在组件各个阶段,执行类组件的render
函数,和对应的生命周期函数就可以了。
PureComponent
和 Component
用法,差不多一样,唯一不同的是,纯组件PureComponent
会浅比较,props
和state
是否相同,来决定是否重新渲染组件。所以一般用于性能调优,减少render次数。
什么叫做浅比较,我这里举个列子:
js 体验AI代码助手 代码解读复制代码class Index extends React.PureComponent{ constructor(props){ super(props) this.state={ data:{ name:'alien', age:28 } } } handerClick= () =>{ const { data } = this.state data.age++ this.setState({ data }) } render(){ const { data } = this.state return你的姓名是: { data.name }年龄: { data.age }age++} }
点击按钮,没有任何反应,因为PureComponent
会比较两次data
对象,都指向同一个data
,没有发生改变,所以不更新视图。
解决这个问题很简单,只需要在handerClick
事件中这么写:
js 体验AI代码助手 代码解读复制代码 this.setState({ data:{...data} })
浅拷贝就能根本解决问题。
React.memo
和PureComponent
作用类似,可以用作性能优化,React.memo
是高阶组件,函数组件和类组件都可以使用, 和区别PureComponent
是 React.memo
只能对props
的情况确定是否渲染,而PureComponent
是针对props
和state
。
React.memo
接受两个参数,第一个参数原始组件本身,第二个参数,可以根据一次更新中props
是否相同决定原始组件是否重新渲染。是一个返回布尔值,true
证明组件无须重新渲染,false
证明组件需要重新渲染,这个和类组件中的shouldComponentUpdate()
正好相反 。
React.memo: 第二个参数 返回 true
组件不渲染 , 返回 false
组件重新渲染。shouldComponentUpdate: 返回 true
组件渲染 , 返回 false
组件不渲染。
接下来我们做一个场景,控制组件在仅此一个props
数字变量,一定范围渲染。
例子🌰:
控制 props
中的 number
:
1 只有 number
更改,组件渲染。
2 只有 number
小于 5 ,组件渲染。
js 体验AI代码助手 代码解读复制代码function TextMemo(props){ console.log('子组件渲染') if(props) returnhello,world} const controlIsRender = (pre,next)=>{ if(pre.number === next.number ){ // number 不改变 ,不渲染组件 return true }else if(pre.number !== next.number && next.number > 5 ) { // number 改变 ,但值大于5 , 不渲染组件 return true }else { // 否则渲染组件 return false } } const NewTexMemo = memo(TextMemo,controlIsRender) class Index extends React.Component{ constructor(props){ super(props) this.state={ number:1, num:1 } } render(){ const { num , number } = this.state return改变num:当前值 { num }this.setState({ num:num + 1 }) } >num++this.setState({ num:num - 1 }) } >num--改变number: 当前值 { number }this.setState({ number:number + 1 }) } > number ++this.setState({ number:number - 1 }) } > number --} }
效果:
完美达到了效果,React.memo
一定程度上,可以等价于组件外部使用shouldComponentUpdate
,用于拦截新老props
,确定组件是否更新。
官网对forwardRef
的概念和用法很笼统,也没有给定一个具体的案例。很多同学不知道 forwardRef
具体怎么用,下面我结合具体例子给大家讲解forwardRef
应用场景。
1 转发引入Ref
这个场景实际很简单,比如父组件想获取孙组件,某一个dom
元素。这种隔代ref
获取引用,就需要forwardRef
来助力。
js 体验AI代码助手 代码解读复制代码function Son (props){ const { grandRef } = props returni am alien这个是想要获取元素} class Father extends React.Component{ constructor(props){ super(props) } render(){ return} } const NewFather = React.forwardRef((props,ref)=>) class GrandFather extends React.Component{ constructor(props){ super(props) } node = null componentDidMount(){ console.log(this.node) } render(){ returnthis.node = node } />} }
效果
react
不允许ref
通过props
传递,因为组件上已经有 ref
这个属性,在组件调和过程中,已经被特殊处理,forwardRef
出现就是解决这个问题,把ref
转发到自定义的forwardRef
定义的属性上,让ref
,可以通过props
传递。
2 高阶组件转发Ref
一文吃透hoc
文章中讲到,由于属性代理的hoc
,被包裹一层,所以如果是类组件,是通过ref
拿不到原始组件的实例的,不过我们可以通过forWardRef
转发ref
。
js 体验AI代码助手 代码解读复制代码function HOC(Component){ class Wrap extends React.Component{ render(){ const { forwardedRef ,...otherprops } = this.props return} } return React.forwardRef((props,ref)=>) } class Index extends React.Component{ componentDidMount(){ console.log(666) } render(){ returnhello,world} } const HocIndex = HOC(Index,true) export default ()=>{ const node = useRef(null) useEffect(()=>{ /* 就可以跨层级,捕获到 Index 组件的实例了 */ console.log(node.current.componentDidMount) },[]) return}
如上,解决了高阶组件引入Ref
的问题。
React.lazy 和 Suspense 技术还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,我们推荐 Loadable Components 这个库
React.lazy
和Suspense
配合一起用,能够有动态加载组件的效果。React.lazy
接受一个函数,这个函数需要动态调用 import()
。它必须返回一个 Promise
,该 Promise
需要 resolve
一个 default export
的 React
组件。
我们模拟一个动态加载的场景。
父组件
js 体验AI代码助手 代码解读复制代码import Test from './comTest' const LazyComponent = React.lazy(()=> new Promise((resolve)=>{ setTimeout(()=>{ resolve({ default: ()=>}) },2000) })) class index extends React.Component{ render(){ return} >} }
我们用setTimeout
来模拟import
异步引入效果。
Test
js 体验AI代码助手 代码解读复制代码class Test extends React.Component{ constructor(props){ super(props) } componentDidMount(){ console.log('--componentDidMount--') } render(){ return} }
效果
何为Suspense
, Suspense
让组件“等待”某个异步操作,直到该异步操作结束即可渲染。
用于数据获取的 Suspense
是一个新特性,你可以使用
以声明的方式来“等待”任何内容,包括数据。本文重点介绍它在数据获取的用例,它也可以用于等待图像、脚本或其他异步的操作。
上面讲到高阶组件lazy
时候,已经用 lazy
+ Suspense
模式,构建了异步渲染组件。我们看一下官网文档中的案例:
js 体验AI代码助手 代码解读复制代码const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懒加载}>
react
不允许一个组件返回多个节点元素,比如说如下情况
js 体验AI代码助手 代码解读复制代码render(){ return🍎🍎🍎🍌🍌🍌🍇🍇🍇}
如果我们想解决这个情况,很简单,只需要在外层套一个容器元素。
js 体验AI代码助手 代码解读复制代码render(){ return🍎🍎🍎🍌🍌🍌🍇🍇🍇}
但是我们不期望,增加额外的dom
节点,所以react
提供Fragment
碎片概念,能够让一个组件返回多个元素。
所以我们可以这么写
js 体验AI代码助手 代码解读复制代码🍎🍎🍎🍌🍌🍌🍇🍇🍇
还可以简写成:
js 体验AI代码助手 代码解读复制代码<>🍎🍎🍎🍌🍌🍌🍇🍇🍇</>
和Fragment
区别是,Fragment
可以支持key
属性。<></>
不支持key
属性。
温馨提示。我们通过map
遍历后的元素,react
底层会处理,默认在外部嵌套一个
。
比如:
js 体验AI代码助手 代码解读复制代码{ [1,2,3].map(item=>{ item.name }) }
react
底层处理之后,等价于:
html 体验AI代码助手 代码解读复制代码
Profiler
这个api
一般用于开发阶段,性能检测,检测一次react
组件渲染用时,性能开销。
Profiler
需要两个参数:
第一个参数:是 id
,用于表识唯一性的Profiler
。
第二个参数:onRender
回调函数,用于渲染完成,接受渲染参数。
实践:
js 体验AI代码助手 代码解读复制代码const index = () => { const callback = (...arg) => console.log(arg) return{ renderRoutes(menusList) }}
结果
onRender
0 -id: root
-> Profiler
树的 id
。1 -phase: mount
-> mount
挂载 , update
渲染了。2 -actualDuration: 6.685000262223184
-> 更新 committed
花费的渲染时间。3 -baseDuration: 4.430000321008265
-> 渲染整颗子树需要的时间4 -startTime : 689.7299999836832
-> 本次更新开始渲染的时间5 -commitTime : 698.5799999674782
-> 本次更新committed 的时间6 -interactions: set{}
-> 本次更新的 interactions
的集合
尽管 Profiler 是一个轻量级组件,我们依然应该在需要时才去使用它。对一个应用来说,每添加一些都会给 CPU 和内存带来一些负担。
StrictMode
见名知意,严格模式,用于检测react
项目中的潜在的问题,。与 Fragment
一样, StrictMode
不会渲染任何可见的 UI
。它为其后代元素触发额外的检查和警告。
严格模式检查仅在开发模式下运行;它们不会影响生产构建。
StrictMode
目前有助于:
①识别不安全的生命周期。②关于使用过时字符串 ref API
的警告③关于使用废弃的 findDOMNode
方法的警告④检测意外的副作用⑤检测过时的 context API
实践:识别不安全的生命周期
对于不安全的生命周期,指的是UNSAFE_componentWillMount
,UNSAFE_componentWillReceiveProps
, UNSAFE_componentWillUpdate
外层开启严格模式:
js 体验AI代码助手 代码解读复制代码{ renderRoutes(menusList) }
我们在内层组件中,使用不安全的生命周期:
js 体验AI代码助手 代码解读复制代码class Index extends React.Component{ UNSAFE_componentWillReceiveProps(){ } render(){ return} }
效果:
接下来我们一起来探究一下react
工具类函数的用法。
一提到createElement
,就不由得和JSX
联系一起。我们写的jsx
,最终会被 babel
,用createElement
编译成react
元素形式。我写一个组件,我们看一下会被编译成什么样子,
如果我们在render
里面这么写:
js 体验AI代码助手 代码解读复制代码render(){ return生命周期Flagment{ /* */ } text文本}
会被编译成这样:
js 体验AI代码助手 代码解读复制代码render() { return React.createElement("div", { className: "box" }, React.createElement("div", { className: "item" }, "\u751F\u547D\u5468\u671F"), React.createElement(Text, { mes: "hello,world" }), React.createElement(React.Fragment, null, " Flagment "), "text\u6587\u672C"); }
当然我们可以不用jsx
模式,而是直接通过createElement
进行开发。
createElement
模型:
js 体验AI代码助手 代码解读复制代码React.createElement( type, [props], [...children] )
createElement
参数:
**第一个参数:**如果是组件类型,会传入组件,如果是dom
元素类型,传入div
或者span
之类的字符串。
第二个参数::第二个参数为一个对象,在dom
类型中为属性,在组件
类型中为props。
其他参数:,依次为children
,根据顺序排列。
createElement做了些什么?
经过createElement
处理,最终会形成 $$typeof = Symbol(react.element)
对象。对象上保存了该react.element
的信息。
可能有的同学还傻傻的分不清楚cloneElement
和createElement
区别和作用。
createElement
把我们写的jsx
,变成element
对象; 而cloneElement
的作用是以 element
元素为样板克隆并返回新的 React
元素。返回元素的 props
是将新的 props
与原始元素的 props
浅层合并后的结果。
那么cloneElement
感觉在我们实际业务组件中,可能没什么用,但是在一些开源项目,或者是公共插槽组件中用处还是蛮大的,比如说,我们可以在组件中,劫持children element
,然后通过cloneElement
克隆element
,混入props
。经典的案例就是 react-router
中的Swtich
组件,通过这种方式,来匹配唯一的 Route
并加以渲染。
我们设置一个场景,在组件中,去劫持children
,然后给children
赋能一些额外的props
:
js 体验AI代码助手 代码解读复制代码function FatherComponent({ children }){ const newChildren = React.cloneElement(children, { age: 18}) return{ newChildren }} function SonComponent(props){ console.log(props) returnhello,world} class Index extends React.Component{ render(){ return} }
打印:
完美达到了效果!
createContext
用于创建一个Context
对象,createContext
对象中,包括用于传递 Context
对象值 value
的Provider
,和接受value
变化订阅的Consumer
。
js 体验AI代码助手 代码解读复制代码const MyContext = React.createContext(defaultValue)
createContext
接受一个参数defaultValue
,如果Consumer
上一级一直没有Provider
,则会应用defaultValue
作为value
。只有当组件所处的树中没有匹配到 Provider
时,其 defaultValue
参数才会生效。
我们来模拟一个 Context.Provider
和Context.Consumer
的例子:
js 体验AI代码助手 代码解读复制代码function ComponentB(){ /* 用 Consumer 订阅, 来自 Provider 中 value 的改变 */ return{ (value) =>}} function ComponentA(props){ const { name , mes } = props return姓名: { name }想对大家说: { mes }} function index(){ const [ value , ] = React.useState({ name:'alien', mes:'let us learn React ' }) return}
打印结果:
Provider
和Consumer
的良好的特性,可以做数据的存和取,Consumer
一方面传递value
,另一方面可以订阅value
的改变。
Provider
还有一个特性可以层层传递value
,这种特性在react-redux
中表现的淋漓尽致。
js 体验AI代码助手 代码解读复制代码React.createFactory(type)
返回用于生成指定类型 React 元素的函数。类型参数既可以是标签名字符串(像是 'div
' 或 'span
'),也可以是 React 组件 类型 ( class
组件或函数组件),或是 React fragment
类型。
使用:
js 体验AI代码助手 代码解读复制代码 const Text = React.createFactory(()=>hello,world) function Index(){ return}
效果
报出警告,这个api
将要被废弃,我们这里就不多讲了,如果想要达到同样的效果,请用React.createElement
createRef
可以创建一个 ref
元素,附加在react
元素上。
用法:
js 体验AI代码助手 代码解读复制代码class Index extends React.Component{ constructor(props){ super(props) this.node = React.createRef() } componentDidMount(){ console.log(this.node) } render(){ returnmy name is alien} }
个人觉得createRef
这个方法,很鸡肋,我们完全可以class
类组件中这么写,来捕获ref
。
js 体验AI代码助手 代码解读复制代码class Index extends React.Component{ node = null componentDidMount(){ console.log(this.node) } render(){ returnthis.node } > my name is alien} }
或者在function
组件中这么写:
js 体验AI代码助手 代码解读复制代码function Index(){ const node = React.useRef(null) useEffect(()=>{ console.log(node.current) },[]) returnmy name is alien}
这个方法可以用来检测是否为react element
元素,接受待验证对象,返回true
或者false
。这个api可能对于业务组件的开发,作用不大,因为对于组件内部状态,都是已知的,我们根本就不需要去验证,是否是react element
元素。
但是,对于一起公共组件或是开源库,isValidElement
就很有作用了。
实践
我们做一个场景,验证容器组件的所有子组件,过滤到非react element
类型。
没有用isValidElement
验证之前:
js 体验AI代码助手 代码解读复制代码const Text = () =>hello,worldclass WarpComponent extends React.Component{ constructor(props){ super(props) } render(){ return this.props.children } } function Index(){ returnmy name is alienLet's learn react together!}
过滤之前的效果
我们用isValidElement
进行react element
验证:
js 体验AI代码助手 代码解读复制代码class WarpComponent extends React.Component{ constructor(props){ super(props) this.newChidren = this.props.children.filter(item => React.isValidElement(item) ) } render(){ return this.newChidren } }
过滤之后效果
过滤掉了非react element
的 Let's learn react together!
。
接下来的五个api
都是和react.Chidren
相关的,我们来分别介绍一下,我们先来看看官网的描述,React.Children
提供了用于处理 this.props.children
不透明数据结构的实用方法。
有的同学会问遍历 children
用数组方法,map
,forEach
不就可以了吗? 请我们注意一下不透明数据结构
,什么叫做不透明结构?
我们先看一下透明的结构:
js 体验AI代码助手 代码解读复制代码class Text extends React.Component{ render(){ returnhello,world} } function WarpComponent(props){ console.log(props.children) return props.children } function Index(){ returnhello,world}
打印
但是我们把Index
结构改变一下:
js 体验AI代码助手 代码解读复制代码function Index(){ return{ new Array(3).fill(0).map(()=>) } hello,world}
打印
这个数据结构,我们不能正常的遍历了,即使遍历也不能遍历,每一个子元素。此时就需要 react.Chidren
来帮忙了。
但是我们把WarpComponent
组件用react.Chidren
处理children
:
js 体验AI代码助手 代码解读复制代码function WarpComponent(props){ const newChildren = React.Children.map(props.children,(item)=>item) console.log(newChildren) return newChildren }
此时就能正常遍历了,达到了预期效果。
注意
如果 children
是一个 Fragment
对象,它将被视为单一子节点的情况处理,而不会被遍历。
Children.forEach
和Children.map
用法类似,Children.map
可以返回新的数组,Children.forEach
仅停留在遍历阶段。
我们将上面的WarpComponent
方法,用Children.forEach
改一下。
js 体验AI代码助手 代码解读复制代码function WarpComponent(props){ React.Children.forEach(props.children,(item)=>console.log(item)) return props.children }
children
中的组件总数量,等同于通过 map
或 forEach
调用回调函数的次数。对于更复杂的结果,Children.count
可以返回同一级别子组件的数量。
我们还是把上述例子进行改造:
js 体验AI代码助手 代码解读复制代码function WarpComponent(props){ const childrenCount = React.Children.count(props.children) console.log(childrenCount,'childrenCount') return props.children } function Index(){ return{ new Array(3).fill(0).map((item,index) => new Array(2).fill(1).map((item,index1)=>)) } hello,world}
效果:
Children.toArray
返回,props.children
扁平化后结果。
js 体验AI代码助手 代码解读复制代码function WarpComponent(props){ const newChidrenArray = React.Children.toArray(props.children) console.log(newChidrenArray,'newChidrenArray') return newChidrenArray } function Index(){ return{ new Array(3).fill(0).map((item,index)=>new Array(2).fill(1).map((item,index1)=>)) } hello,world}
效果:
newChidrenArray ,就是扁平化的数组结构。React.Children.toArray()
在拉平展开子节点列表时,更改 key
值以保留嵌套数组的语义。也就是说, toArray
会为返回数组中的每个 key
添加前缀,以使得每个元素 key
的范围都限定在此函数入参数组的对象内。
验证 children
是否只有一个子节点(一个 React
元素),如果有则返回它,否则此方法会抛出错误。
不唯一
js 体验AI代码助手 代码解读复制代码function WarpComponent(props){ console.log(React.Children.only(props.children)) return props.children } function Index(){ return{ new Array(3).fill(0).map((item,index)=>) } hello,world}
效果
唯一
js 体验AI代码助手 代码解读复制代码function WarpComponent(props){ console.log(React.Children.only(props.children)) return props.children } function Index(){ return}
效果
React.Children.only()
不接受 React.Children.map()
的返回值,因为它是一个数组而并不是 React
元素。
对于react-hooks
,我已经写了三部曲,介绍了react-hooks
使用,自定义hooks
,以及react-hooks
原理,感兴趣的同学可以去看看,文章末尾有链接,对于常用的api
,我这里参考了react-hooks
如何使用那篇文章。并做了相应精简化和一些内容的补充。
useState
可以弥补函数组件没有state
的缺陷。useState
可以接受一个初识值,也可以是一个函数action
,action
返回值作为新的state
。返回一个数组,第一个值为state
读取值,第二个值为改变state
的dispatchAction
函数。
我们看一个例子:
jsx 体验AI代码助手 代码解读复制代码const DemoState = (props) => { /* number为此时state读取值 ,setNumber为派发更新的函数 */ let [number, setNumber] = useState(0) /* 0为初始值 */ return ({ number } { setNumber(number+1) /* 写法一 */ setNumber(number=>number + 1 ) /* 写法二 */ console.log(number) /* 这里的number是不能够即时改变的 */ } } >num++) }
useEffect
可以弥补函数组件没有生命周期的缺点。我们可以在useEffect
第一个参数回调函数中,做一些请求数据,事件监听等操作,第二个参数作为dep
依赖项,当依赖项发生变化,重新执行第一个函数。
useEffect可以用作数据交互。
jsx 体验AI代码助手 代码解读复制代码/* 模拟数据交互 */ function getUserInfo(a){ return new Promise((resolve)=>{ setTimeout(()=>{ resolve({ name:a, age:16, }) },500) }) } const DemoEffect = ({ a }) => { const [ userMessage , setUserMessage ] :any= useState({}) const div= useRef() const [number, setNumber] = useState(0) /* 模拟事件监听处理函数 */ const handleResize =()=>{} /* useEffect使用 ,这里如果不加限制 ,会是函数重复执行,陷入死循环*/ useEffect(()=>{ /* 请求数据 */ getUserInfo(a).then(res=>{ setUserMessage(res) }) /* 操作dom */ console.log(div.current) /* div */ /* 事件监听等 */ window.addEventListener('resize', handleResize) /* 只有当props->a和state->number改变的时候 ,useEffect副作用函数重新执行 ,如果此时数组为空[],证明函数只有在初始化的时候执行一次相当于componentDidMount */ },[ a ,number ]) return ({ userMessage.name } { userMessage.age }setNumber(1) } >{ number }) }
useEffect可以用作事件监听,还有一些基于dom
的操作。,别忘了在useEffect
第一个参数回调函数,返一个函数用于清除事件监听等操作。
jsx 体验AI代码助手 代码解读复制代码const DemoEffect = ({ a }) => { /* 模拟事件监听处理函数 */ const handleResize =()=>{} useEffect(()=>{ /* 定时器 延时器等 */ const timer = setInterval(()=>console.log(666),1000) /* 事件监听 */ window.addEventListener('resize', handleResize) /* 此函数用于清除副作用 */ return function(){ clearInterval(timer) window.removeEventListener('resize', handleResize) } },[ a ]) return () }
useMemo
接受两个参数,第一个参数是一个函数,返回值用于产生保存值。 第二个参数是一个数组,作为dep
依赖项,数组里面的依赖项发生变化,重新执行第一个函数,产生新的值。
应用场景: 1 缓存一些值,避免重新执行上下文
js 体验AI代码助手 代码解读复制代码const number = useMemo(()=>{ /** ....大量的逻辑运算 **/ return number },[ props.number ]) // 只有 props.number 改变的时候,重新计算number的值。
2 减少不必要的dom
循环
js 体验AI代码助手 代码解读复制代码/* 用 useMemo包裹的list可以限定当且仅当list改变的时候才更新此list,这样就可以避免selectList重新循环 */ {useMemo(() => ({ selectList.map((i, v) => ( {i.patentName} ))}), [selectList])}
3 减少子组件渲染
js 体验AI代码助手 代码解读复制代码/* 只有当props中,list列表改变的时候,子组件才渲染 */ const goodListChild = useMemo(()=>,[ props.list ])
useMemo
和 useCallback
接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo
返回的是函数运行的结果, useCallback
返回的是函数。 返回的callback
可以作为props
回调函数传递给子组件。
js 体验AI代码助手 代码解读复制代码/* 用react.memo */ const DemoChildren = React.memo((props)=>{ /* 只有初始化的时候打印了 子组件更新 */ console.log('子组件更新') useEffect(()=>{ props.getInfo('子组件') },[]) return子组件}) const DemoUseCallback=({ id })=>{ const [number, setNumber] = useState(1) /* 此时usecallback的第一参数 (sonName)=>{ console.log(sonName) } 经过处理赋值给 getInfo */ const getInfo = useCallback((sonName)=>{ console.log(sonName) },[id]) return{/* 点击按钮触发父组件更新 ,但是子组件没有更新 */}setNumber(number+1) } >增加}
useRef
的作用:
一 是可以用来获取dom
元素,或者class
组件实例 。二 react-hooks原理
文章中讲过,创建useRef
时候,会创建一个原始对象,只要函数组件不被销毁,原始对象就会一直存在,那么我们可以利用这个特性,来通过useRef
保存一些数据。
jsx 体验AI代码助手 代码解读复制代码const DemoUseRef = ()=>{ const dom= useRef(null) const handerSubmit = ()=>{ /*表单组件dom 节点 */ console.log(dom.current) } return{/* ref 标记当前dom节点 */}表单组件handerSubmit()} >提交}
useEffect
执行顺序: 组件更新挂载完成 -> 浏览器 dom
绘制完成 -> 执行 useEffect
回调。useLayoutEffect
执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect
回调-> 浏览器dom
绘制完成。
所以说 useLayoutEffect
代码可能会阻塞浏览器的绘制 。我们写的 effect
和 useLayoutEffect
,react
在底层会被分别打上PassiveEffect
,HookLayout
,在commit
阶段区分出,在什么时机执行。
jsx 体验AI代码助手 代码解读复制代码const DemoUseLayoutEffect = () => { const target = useRef() useLayoutEffect(() => { /*我们需要在dom绘制之前,移动dom到制定位置*/ const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */ animate(target.current,{ x,y }) }, []); return () }
在react-hooks
原理那篇文章中讲解到,useState
底层就是一个简单版的useReducer
useReducer
接受的第一个参数是一个函数,我们可以认为它就是一个 reducer
, reducer
的参数就是常规 reducer
里面的 state
和 action
,返回改变后的 state
, useReducer
第二个参数为 state
的初始值 返回一个数组,数组的第一项就是更新之后 state
的值 ,第二个参数是派发更新的 dispatch
函数。
我们来看一下useReducer
如何使用:
js 体验AI代码助手 代码解读复制代码const DemoUseReducer = ()=>{ /* number为更新后的state值, dispatchNumbner 为当前的派发函数 */ const [ number , dispatchNumbner ] = useReducer((state,action)=>{ const { payload , name } = action /* return的值为新的state */ switch(name){ case 'add': return state + 1 case 'sub': return state - 1 case 'reset': return payload } return state },0) return当前值:{ number } { /* 派发更新 */ }dispatchNumbner({ name:'add' })} >增加dispatchNumbner({ name:'sub' })} >减少dispatchNumbner({ name:'reset' ,payload:666 })} >赋值{ /* 把dispatch 和 state 传递给子组件 */ }}
我们可以使用 useContext
,来获取父级组件传递过来的 context
值,这个当前值就是最近的父级组件 Provider
设置的 value
值,useContext
参数一般是由 createContext
方式引入 ,也可以父级上下文 context
传递 ( 参数为 context
)。useContext
可以代替 context.Consumer
来获取 Provider
中保存的 value
值
jsx 体验AI代码助手 代码解读复制代码/* 用useContext方式 */ const DemoContext = ()=> { const value:any = useContext(Context) /* my name is alien */ returnmy name is { value.name }} /* 用Context.Consumer 方式 */ const DemoContext1 = ()=>{ return{/* my name is alien */} { (value)=>my name is { value.name }}} export default ()=>{ return}
useImperativeHandle
可以配合 forwardRef
自定义暴露给父组件的实例值。这个很有用,我们知道,对于子组件,如果是class
类组件,我们可以通过ref
获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过ref
的,那么此时useImperativeHandle
和 forwardRef
配合就能达到效果。
useImperativeHandle
接受三个参数:
第一个参数ref: 接受 forWardRef
传递过来的 ref
。
第二个参数 createHandle
:处理函数,返回值作为暴露给父组件的ref
对象。
第三个参数 deps
:依赖项 deps
,依赖项更改形成新的ref
对象。
我们来模拟给场景,用useImperativeHandle
,使得父组件能让子组件中的input
自动赋值并聚焦。
js 体验AI代码助手 代码解读复制代码function Son (props,ref) { console.log(props) const inputRef = useRef(null) const [ inputValue , setInputValue ] = useState('') useImperativeHandle(ref,()=>{ const handleRefs = { /* 声明方法用于聚焦input框 */ onFocus(){ inputRef.current.focus() }, /* 声明方法用于改变input的值 */ onChangeValue(value){ setInputValue(value) } } return handleRefs },[]) return} const ForwarSon = forwardRef(Son) class Index extends React.Component{ inputRef = null handerClick(){ const { onFocus , onChangeValue } =this.cur onFocus() onChangeValue('let us learn React!') } render(){ return(this.inputRef = node)} />操控子组件} }
效果:
useDebugValue
可用于在 React
开发者工具中显示自定义 hook
的标签。这个hooks
目的就是检查自定义hooks
js 体验AI代码助手 代码解读复制代码function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); // ... // 在开发者工具中的这个 Hook 旁边显示标签 // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline'); return isOnline; }
我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
useTransition
允许延时由state
改变而带来的视图渲染。避免不必要的渲染。它还允许组件将速度较慢的数据获取更新推迟到随后渲染,以便能够立即渲染更重要的更新。
js 体验AI代码助手 代码解读复制代码const TIMEOUT_MS = { timeoutMs: 2000 } const [startTransition, isPending] = useTransition(TIMEOUT_MS)
useTransition
接受一个对象, timeoutMs
代码需要延时的时间。
返回一个数组。第一个参数: 是一个接受回调的函数。我们用它来告诉 React
需要推迟的 state
。 第二个参数: 一个布尔值。表示是否正在等待,过度状态的完成(延时state
的更新)。
下面我们引入官网的列子,来了解useTransition
的使用。
js 体验AI代码助手 代码解读复制代码const SUSPENSE_CONFIG = { timeoutMs: 2000 }; function App() { const [resource, setResource] = useState(initialResource); const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG); return ( <>{ startTransition(() => { const nextUserId = getNextId(resource.userId); setResource(fetchProfileData(nextUserId)); }); }} > Next{isPending ? " 加载中..." : null}}></> ); }
在这段代码中,我们使用 startTransition
包装了我们的数据获取。这使我们可以立即开始获取用户资料的数据,同时推迟下一个用户资料页面以及其关联的 Spinner
的渲染 2 秒钟( timeoutMs
中显示的时间)。
这个api
目前处于实验阶段,没有被完全开放出来。
接下来,我们来一起研究react-dom
中比较重要的api
。
render
是我们最常用的react-dom
的 api
,用于渲染一个react
元素,一般react
项目我们都用它,渲染根部容器app
。
js 体验AI代码助手 代码解读复制代码ReactDOM.render(element, container[, callback])
使用
jsx 体验AI代码助手 代码解读复制代码ReactDOM.render( < App / >, document.getElementById('app') )
ReactDOM.render
会控制container
容器节点里的内容,但是不会修改容器节点本身。
服务端渲染用hydrate
。用法与 render()
相同,但它用于在 ReactDOMServer
渲染的容器中对 HTML
的内容进行 hydrate
操作。
js 体验AI代码助手 代码解读复制代码ReactDOM.hydrate(element, container[, callback])
Portal
提供了一种将子节点渲染到存在于父组件以外的 DOM
节点的优秀的方案。createPortal
可以把当前组件或 element
元素的子节点,渲染到组件之外的其他地方。
那么具体应用到什么场景呢?
比如一些全局的弹窗组件model
,
组件一般都写在我们的组件内部,倒是真正挂载的dom
,都是在外层容器,比如body
上。此时就很适合createPortal
API。
createPortal
接受两个参数:
js 体验AI代码助手 代码解读复制代码ReactDOM.createPortal(child, container)
第一个: child
是任何可渲染的 React
子元素
第二个: container
是一个 DOM
元素。
接下来我们实践一下:
js 体验AI代码助手 代码解读复制代码function WrapComponent({ children }){ const domRef = useRef(null) const [ PortalComponent, setPortalComponent ] = useState(null) React.useEffect(()=>{ setPortalComponent( ReactDOM.createPortal(children,domRef.current) ) },[]) return{ PortalComponent }} class Index extends React.Component{ render(){ returnhello,world} }
效果
我们可以看到,我们children
实际在container
之外挂载的,但是已经被createPortal
渲染到container
中。
在react-legacy
模式下,对于事件,react
事件有批量更新来处理功能,但是这一些非常规的事件中,批量更新功能会被打破。所以我们可以用react-dom
中提供的unstable_batchedUpdates
来进行批量更新。
一次点击实现的批量更新
js 体验AI代码助手 代码解读复制代码class Index extends React.Component{ constructor(props){ super(props) this.state={ numer:1, } } handerClick=()=>{ this.setState({ numer : this.state.numer + 1 }) console.log(this.state.numer) this.setState({ numer : this.state.numer + 1 }) console.log(this.state.numer) this.setState({ numer : this.state.numer + 1 }) console.log(this.state.numer) } render(){ returnclick me} }
效果
渲染次数一次。
批量更新条件被打破
js 体验AI代码助手 代码解读复制代码 handerClick=()=>{ Promise.resolve().then(()=>{ this.setState({ numer : this.state.numer + 1 }) console.log(this.state.numer) this.setState({ numer : this.state.numer + 1 }) console.log(this.state.numer) this.setState({ numer : this.state.numer + 1 }) console.log(this.state.numer) }) }
效果
渲染次数三次。
unstable_batchedUpdate助力
js 体验AI代码助手 代码解读复制代码 handerClick=()=>{ Promise.resolve().then(()=>{ ReactDOM.unstable_batchedUpdates(()=>{ this.setState({ numer : this.state.numer + 1 }) console.log(this.state.numer) this.setState({ numer : this.state.numer + 1 }) console.log(this.state.numer) this.setState({ numer : this.state.numer + 1 }) console.log(this.state.numer) }) }) }
渲染次数一次,完美解决批量更新问题。
flushSync
可以将回调函数中的更新任务,放在一个较高的优先级中。我们知道react
设定了很多不同优先级的更新任务。如果一次更新任务在flushSync
回调函数内部,那么将获得一个较高优先级的更新。比如
js 体验AI代码助手 代码解读复制代码ReactDOM.flushSync(()=>{ /* 此次更新将设置一个较高优先级的更新 */ this.setState({ name: 'alien' }) })
为了让大家理解flushSync
,我这里做一个demo
奉上,
js 体验AI代码助手 代码解读复制代码/* flushSync */ import ReactDOM from 'react-dom' class Index extends React.Component{ state={ number:0 } handerClick=()=>{ setTimeout(()=>{ this.setState({ number: 1 }) }) this.setState({ number: 2 }) ReactDOM.flushSync(()=>{ this.setState({ number: 3 }) }) this.setState({ number: 4 }) } render(){ const { number } = this.state console.log(number) // 打印什么?? return{ number }测试flushSync} }
先不看答案,点击一下按钮,打印什么呢?
我们来点击一下看看
打印 0 3 4 1 ,相信不难理解为什么这么打印了。
首先 flushSync
this.setState({ number: 3 })
设定了一个高优先级的更新,所以3 先被打印2 4 被批量更新为 4
相信这个demo
让我们更深入了解了flushSync
。
findDOMNode
用于访问组件DOM
元素节点,react
推荐使用ref
模式,不期望使用findDOMNode
。
js 体验AI代码助手 代码解读复制代码ReactDOM.findDOMNode(component)
注意的是:
1 findDOMNode
只能用在已经挂载的组件上。
2 如果组件渲染内容为 null
或者是 false
,那么 findDOMNode
返回值也是 null
。
3 findDOMNode
不能用于函数组件。
接下来让我们看一下,findDOMNode
具体怎么使用的:
js 体验AI代码助手 代码解读复制代码class Index extends React.Component{ handerFindDom=()=>{ console.log(ReactDOM.findDOMNode(this)) } render(){ returnhello,world获取容器dom} }
效果:
我们完全可以将外层容器用ref
来标记,获取捕获原生的dom
节点。
从 DOM
中卸载组件,会将其事件处理器和 state
一并清除。 如果指定容器上没有对应已挂载的组件,这个函数什么也不会做。如果组件被移除将会返回 true
,如果没有组件可被移除将会返回 false
。
我们来简单举例看看unmountComponentAtNode
如何使用?
js 体验AI代码助手 代码解读复制代码function Text(){ returnhello,world} class Index extends React.Component{ node = null constructor(props){ super(props) this.state={ numer:1, } } componentDidMount(){ /* 组件初始化的时候,创建一个 container 容器 */ ReactDOM.render(, this.node ) } handerClick=()=>{ /* 点击卸载容器 */ const state = ReactDOM.unmountComponentAtNode(this.node) console.log(state) } render(){ returnthis.node = node } >click me} }
效果
本文通过react
组件层面,工具层面,hooks
层面,react-dom
了解了api
的用法,希望看完的同学,能够对着文章中的demo
自己敲一遍,到头来会发现自己成长不少。
最后, 送人玫瑰,手留余香,觉得有收获的朋友可以给笔者点赞,关注一波 ,陆续更新前端超硬核文章。
提前透漏:接下来会出一部揭秘react
事件系统的文章。
有话要说...