「React 進階」 React 全部 api 解讀 - 基礎實踐大全
很多同學用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
Component
是class
組件的根基。類組件一切始於Component
。對於React.Component
使用,我們沒有什麼好講的。我們這裏重點研究一下react
對Component
做了些什麼。
react/src/ReactBaseClasses.js
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
function constructClassInstance(
workInProgress,
ctor,
props
){
const instance = new ctor(props, context);
instance.updater = {
isMounted,
enqueueSetState(){
/* setState 觸發這裏面的邏輯 */
},
enqueueReplaceState(){},
enqueueForceUpdate(){
/* forceUpdate 觸發這裏的邏輯 */
}
}
}
對於Component
, react
處理邏輯還是很簡單的,實例化我們類組件,然後賦值updater
對象,負責組件的更新。然後在組件各個階段,執行類組件的render
函數,和對應的生命週期函數就可以了。
PureComponent
PureComponent
和 Component
用法,差不多一樣,唯一不同的是,純組件PureComponent
會淺比較,props
和state
是否相同,來決定是否重新渲染組件。所以一般用於性能調優,減少 render 次數。
什麼叫做淺比較,我這裏舉個列子:
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 <div class >
<div class >
<div> 你的姓名是: { data.name } </div>
<div> 年齡: { data.age }</div>
<button onClick={ this.handerClick } >age++</button>
</div>
</div>
}
}
PureComponent
會比較兩次data
對象,都指向同一個data
, 沒有發生改變,所以不更新視圖。
解決這個問題很簡單,只需要在handerClick
事件中這麼寫:
this.setState({ data:{...data} })
淺拷貝就能根本解決問題。
memo
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 ,組件渲染。
function TextMemo(props){
console.log('子組件渲染')
if(props)
return <div>hello,world</div>
}
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 <div>
<div>
改變num:當前值 { num }
<button onClick={ ()=>this.setState({ num:num + 1 }) } >num++</button>
<button onClick={ ()=>this.setState({ num:num - 1 }) } >num--</button>
</div>
<div>
改變number: 當前值 { number }
<button onClick={ ()=>this.setState({ number:number + 1 }) } > number ++</button>
<button onClick={ ()=>this.setState({ number:number - 1 }) } > number -- </button>
</div>
<NewTexMemo num={ num } number={number} />
</div>
}
}
完美達到了效果,React.memo
一定程度上,可以等價於組件外部的shouldComponentUpdate
,用於攔截新老props
,確定組件是否更新。
forwardRef
官網對forwardRef
的概念和用法很籠統,也沒有給定一個具體的案例。很多同學不知道 forwardRef
具體怎麼用,下面我結合具體例子給大家講解forwardRef
應用場景。
1 轉發引入 Ref
這個場景實際很簡單,比如父組件想獲取孫組件,某一個dom
元素。這種隔代ref
獲取引用,就需要forwardRef
來助力。
function Son (props){
const { grandRef } = props
return <div>
<div> i am alien </div>
<span ref={grandRef} >這個是想要獲取元素</span>
</div>
}
class Father extends React.Component{
constructor(props){
super(props)
}
render(){
return <div>
<Son grandRef={this.props.grandRef} />
</div>
}
}
const NewFather = React.forwardRef((props,ref)=><Father grandRef={ref} {...props} /> )
class GrandFather extends React.Component{
constructor(props){
super(props)
}
node = null
componentDidMount(){
console.log(this.node)
}
render(){
return <div>
<NewFather ref={(node)=> this.node = node } />
</div>
}
}
效果
forwaedRef.jpg
react
不允許ref
通過props
傳遞,因爲組件上已經有 ref
這個屬性, 在組件調和過程中,已經被特殊處理,forwardRef
出現就是解決這個問題,把ref
轉發到自定義的forwardRef
定義的屬性上,讓ref
,可以通過props
傳遞。
2 高階組件轉發 Ref
一文喫透hoc
文章中講到,由於屬性代理的hoc
,被包裹一層,所以如果是類組件,是通過ref
拿不到原始組件的實例的,不過我們可以通過forWardRef
轉發ref
。
function HOC(Component){
class Wrap extends React.Component{
render(){
const { forwardedRef ,...otherprops } = this.props
return <Component ref={forwardedRef} {...otherprops} />
}
}
return React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> )
}
class Index extends React.Component{
componentDidMount(){
console.log(666)
}
render(){
return <div>hello,world</div>
}
}
const HocIndex = HOC(Index,true)
export default ()=>{
const node = useRef(null)
useEffect(()=>{
/* 就可以跨層級,捕獲到 Index 組件的實例了 */
console.log(node.current.componentDidMount)
},[])
return <div><HocIndex ref={node} /></div>
}
如上,解決了高階組件引入Ref
的問題。
lazy
React.lazy 和 Suspense 技術還不支持服務端渲染。如果你想要在使用服務端渲染的應用中使用,我們推薦 Loadable Components 這個庫
React.lazy
和Suspense
配合一起用,能夠有動態加載組件效果。React.lazy
接受一個函數,這個函數需要動態調用 import()
。它必須返回一個 Promise
,該 Promise
需要 resolve
一個 default export
的 React
組件。
我們模擬一個動態加載的場景。
父組件
import Test from './comTest'
const LazyComponent = React.lazy(()=> new Promise((resolve)=>{
setTimeout(()=>{
resolve({
default: ()=> <Test />
})
},2000)
}))
class index extends React.Component{
render(){
return <div class style={ { marginTop :'50px' } } >
<React.Suspense fallback={ <div class ><SyncOutlined spin /></div> } >
<LazyComponent />
</React.Suspense>
</div>
}
}
我們用setTimeout
來模擬import
異步引入效果。Test
class Test extends React.Component{
constructor(props){
super(props)
}
componentDidMount(){
console.log('--componentDidMount--')
}
render(){
return <div>
<img src={alien} class />
</div>
}
}
效果
Suspense
何爲Suspense
, Suspense
讓組件 “等待” 某個異步操作,直到該異步操作結束即可渲染。
用於數據獲取的 Suspense
是一個新特性,你可以使用 <Suspense>
以聲明的方式來 “等待” 任何內容,包括數據。本文重點介紹它在數據獲取的用例,它也可以用於等待圖像、腳本或其他異步的操作。
上面講到高階組件lazy
時候,已經用 lazy
+ Suspense
模式,構建了異步渲染組件。我們看一下官網文檔中的案例:
const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懶加載
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
Fragment
react
不允許一個組件返回多個節點元素,比如說如下情況
render(){
return <li> 🍎🍎🍎 </li>
<li> 🍌🍌🍌 </li>
<li> 🍇🍇🍇 </li>
}
如果我們想解決這個情況,很簡單,只需要在外層套一個容器元素。
render(){
return <div>
<li> 🍎🍎🍎 </li>
<li> 🍌🍌🍌 </li>
<li> 🍇🍇🍇 </li>
</div>
}
但是我們不期望,增加額外的dom
節點,所以react
提供Fragment
碎片概念,能夠讓一個組件返回多個元素。所以我們可以這麼寫
<React.Fragment>
<li> 🍎🍎🍎 </li>
<li> 🍌🍌🍌 </li>
<li> 🍇🍇🍇 </li>
</React.Fragment>
還可以簡寫成:
<>
<li> 🍎🍎🍎 </li>
<li> 🍌🍌🍌 </li>
<li> 🍇🍇🍇 </li>
</>
和Fragment
區別是,Fragment
可以支持key
屬性。<></>
不支持key
屬性。
Profiler
Profiler
這個api
一般用於開發階段,性能檢測,檢測一次react
組件渲染用時,性能開銷。
Profiler
需要兩個參數:
第一個參數:是 id
,用於表識唯一性的Profiler
。
第二個參數:onRender
回調函數,用於渲染完成,接受渲染參數。
實踐:
const index = () => {
const callback = (...arg) => console.log(arg)
return <div >
<div >
<Profiler id="root" onRender={ callback } >
<Router >
<Meuns/>
<KeepaliveRouterSwitch withoutRoute >
{ renderRoutes(menusList) }
</KeepaliveRouterSwitch>
</Router>
</Profiler>
</div>
</div>
}
結果
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
StrictMode
見名知意,嚴格模式,用於檢測react
項目中的潛在的問題,。與 Fragment
一樣, StrictMode
不會渲染任何可見的 UI
。它爲其後代元素觸發額外的檢查和警告。
嚴格模式檢查僅在開發模式下運行;它們不會影響生產構建。
StrictMode
目前有助於:
-
①識別不安全的生命週期。
-
②關於使用過時字符串
ref API
的警告 -
③關於使用廢棄的
findDOMNode
方法的警告 -
④檢測意外的副作用
-
⑤檢測過時的
context API
實踐: 識別不安全的生命週期
對於不安全的生命週期,指的是UNSAFE_componentWillMount
,UNSAFE_componentWillReceiveProps
, UNSAFE_componentWillUpdate
外層開啓嚴格模式:
<React.StrictMode>
<Router >
<Meuns/>
<KeepaliveRouterSwitch withoutRoute >
{ renderRoutes(menusList) }
</KeepaliveRouterSwitch>
</Router>
</React.StrictMode>
我們在內層組件中,使用不安全的生命週期:
class Index extends React.Component{
UNSAFE_componentWillReceiveProps(){
}
render(){
return <div class />
}
}
效果:
工具類
接下來我們一起來探究一下react
工具類函數的用法。
createElement
一提到createElement
,就不由得和JSX
聯繫一起。我們寫的jsx
,最終會被 babel
,用createElement
編譯成react
元素形式。我寫一個組件,我們看一下會被編譯成什麼樣子,
如果我們在render
裏面這麼寫:
render(){
return <div class >
<div class >生命週期</div>
<Text mes="hello,world" />
<React.Fragment> Flagment </React.Fragment>
{ /* */ }
text文本
</div>
}
會被編譯成這樣:
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
模型:
React.createElement(
type,
[props],
[...children]
)
createElement
參數:
** 第一個參數:** 如果是組件類型,會傳入組件,如果是dom
元素類型,傳入div
或者span
之類的字符串。
第二個參數:: 第二個參數爲一個對象,在dom
類型中爲屬性,在組件
類型中爲 props。
其他參數:,依次爲children
,根據順序排列。
createElement 做了些什麼?
經過createElement
處理,最終會形成 $$typeof = Symbol(react.element)
對象。對象上保存了該react.element
的信息。
cloneElement
可能有的同學還傻傻的分不清楚cloneElement
和createElement
區別和作用。
createElement
把我們寫的jsx
,變成element
對象; 而cloneElement
的作用是以 element
元素爲樣板克隆並返回新的 React
元素。返回元素的 props
是將新的 props
與原始元素的 props
淺層合併後的結果。
那麼cloneElement
感覺在我們實際業務組件中,可能沒什麼用,但是在一些開源項目,或者是公共插槽組件中用處還是蠻大的,比如說,我們可以在組件中,劫持children element
,然後通過cloneElement
克隆element
,混入props
。經典的案例就是 react-router
中的Swtich
組件,通過這種方式,來匹配唯一的 Route
並加以渲染。
我們設置一個場景,在組件中,去劫持children
,然後給children
賦能一些額外的props
:
function FatherComponent({ children }){
const newChildren = React.cloneElement(children, { age: 18})
return <div> { newChildren } </div>
}
function SonComponent(props){
console.log(props)
return <div>hello,world</div>
}
class Index extends React.Component{
render(){
return <div class >
<FatherComponent>
<SonComponent />
</FatherComponent>
</div>
}
}
打印:
完美達到了效果!
createContext
createContext
用於創建一個Context
對象,createContext
對象中,包括用於傳遞 Context
對象值 value
的Provider
,和接受value
變化訂閱的Consumer
。
const MyContext = React.createContext(defaultValue)
createContext
接受一個參數defaultValue
,如果Consumer
上一級一直沒有Provider
, 則會應用defaultValue
作爲value
。只有當組件所處的樹中沒有匹配到 Provider
時,其 defaultValue
參數纔會生效。
我們來模擬一個 Context.Provider
和Context.Consumer
的例子:
function ComponentB(){
/* 用 Consumer 訂閱, 來自 Provider 中 value 的改變 */
return <MyContext.Consumer>
{ (value) => <ComponentA {...value} /> }
</MyContext.Consumer>
}
function ComponentA(props){
const { name , mes } = props
return <div>
<div> 姓名: { name } </div>
<div> 想對大家說: { mes } </div>
</div>
}
function index(){
const [ value , ] = React.useState({
name:'alien',
mes:'let us learn React '
})
return <div style={{ marginTop:'50px' }} >
<MyContext.Provider value={value} >
<ComponentB />
</MyContext.Provider>
</div>
}
打印結果:
Provider
和Consumer
的良好的特性,可以做數據的存和取,Consumer
一方面傳遞value
, 另一方面可以訂閱value
的改變。
Provider
還有一個特性可以層層傳遞value
,這種特性在react-redux
中表現的淋漓盡致。
createFactory
React.createFactory(type)
返回用於生成指定類型 React 元素的函數。類型參數既可以是標籤名字符串(像是 'div
'或'span
'),也可以是 React 組件 類型 ( class
組件或函數組件),或是 React fragment
類型。
使用:
const Text = React.createFactory(()=><div>hello,world</div>)
function Index(){
return <div style={{ marginTop:'50px' }} >
<Text/>
</div>
}
效果
報出警告,這個api
將要被廢棄,我們這裏就不多講了,如果想要達到同樣的效果,請用React.createElement
createRef
createRef
可以創建一個 ref
元素,附加在react
元素上。
用法:
class Index extends React.Component{
constructor(props){
super(props)
this.node = React.createRef()
}
componentDidMount(){
console.log(this.node)
}
render(){
return <div ref={this.node} > my name is alien </div>
}
}
個人覺得createRef
這個方法,很雞肋,我們完全可以class
類組件中這麼寫,來捕獲ref
。
class Index extends React.Component{
node = null
componentDidMount(){
console.log(this.node)
}
render(){
return <div ref={(node)=> this.node } > my name is alien </div>
}
}
或者在function
組件中這麼寫:
function Index(){
const node = React.useRef(null)
useEffect(()=>{
console.log(node.current)
},[])
return <div ref={node} > my name is alien </div>
}
isValidElement
這個方法可以用來檢測是否爲react element
元素, 接受待驗證對象,返回true
或者false
。這個 api 可能對於業務組件的開發,作用不大,因爲對於組件內部狀態,都是已知的,我們根本就不需要去驗證,是否是react element
元素。但是,對於一起公共組件或是開源庫,isValidElement
就很有作用了。
實踐
我們做一個場景,驗證容器組件的所有子組件,過濾到非react element
類型。
沒有用isValidElement
驗證之前:
const Text = () => <div>hello,world</div>
class WarpComponent extends React.Component{
constructor(props){
super(props)
}
render(){
return this.props.children
}
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
<Text/>
<div> my name is alien </div>
Let's learn react together!
</WarpComponent>
</div>
}
過濾之前的效果
我們用isValidElement
進行react element
驗證:
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!
。
Children.map
接下來的五個api
都是和react.Chidren
相關的,我們來分別介紹一下,我們先來看看官網的描述,React.Children
提供了用於處理 this.props.children
不透明數據結構的實用方法。
有的同學會問遍歷 children
用數組方法,map
,forEach
不就可以了嗎?請我們注意一下不透明數據結構
, 什麼叫做不透明結構?
我們先看一下透明的結構:
class Text extends React.Component{
render(){
return <div>hello,world</div>
}
}
function WarpComponent(props){
console.log(props.children)
return props.children
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
<Text/>
<Text/>
<Text/>
<span>hello,world</span>
</WarpComponent>
</div>
}
打印
但是我們把Index
結構改變一下:
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
{ new Array(3).fill(0).map(()=><Text/>) }
<span>hello,world</span>
</WarpComponent>
</div>
}
打印
這個數據結構,我們不能正常的遍歷了,即使遍歷也不能遍歷,每一個子元素。此時就需要 react.Chidren
來幫忙了。
但是我們把WarpComponent
組件用react.Chidren
處理children
:
function WarpComponent(props){
const newChildren = React.Children.map(props.children,(item)=>item)
console.log(newChildren)
return newChildren
}
此時就能正常遍歷了,達到了預期效果。
注意如果 children
是一個 Fragment
對象,它將被視爲單一子節點的情況處理,而不會被遍歷。
Children.forEach
Children.forEach
和Children.map
用法類似,Children.map
可以返回新的數組,Children.forEach
僅停留在遍歷階段。
我們將上面的WarpComponent
方法,用Children.forEach
改一下。
function WarpComponent(props){
React.Children.forEach(props.children,(item)=>console.log(item))
return props.children
}
Children.count
children
中的組件總數量,等同於通過 map
或 forEach
調用回調函數的次數。對於更復雜的結果,Children.count
可以返回同一級別子組件的數量。
我們還是把上述例子進行改造:
function WarpComponent(props){
const childrenCount = React.Children.count(props.children)
console.log(childrenCount,'childrenCount')
return props.children
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
{ new Array(3).fill(0).map((item,index) => new Array(2).fill(1).map((item,index1)=><Text key={index+index1} />)) }
<span>hello,world</span>
</WarpComponent>
</div>
}
效果:
Children.toArray
Children.toArray
返回,props.children
扁平化後結果。
function WarpComponent(props){
const newChidrenArray = React.Children.toArray(props.children)
console.log(newChidrenArray,'newChidrenArray')
return newChidrenArray
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
{ new Array(3).fill(0).map((item,index)=>new Array(2).fill(1).map((item,index1)=><Text key={index+index1} />)) }
<span>hello,world</span>
</WarpComponent>
</div>
}
效果:
newChidrenArray , 就是扁平化的數組結構。React.Children.toArray()
在拉平展開子節點列表時,更改 key
值以保留嵌套數組的語義。也就是說, toArray
會爲返回數組中的每個 key
添加前綴,以使得每個元素 key
的範圍都限定在此函數入參數組的對象內。
Children.only
驗證 children
是否只有一個子節點(一個 React
元素),如果有則返回它,否則此方法會拋出錯誤。
不唯一
function WarpComponent(props){
console.log(React.Children.only(props.children))
return props.children
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
{ new Array(3).fill(0).map((item,index)=><Text key={index} />) }
<span>hello,world</span>
</WarpComponent>
</div>
}
效果
唯一
function WarpComponent(props){
console.log(React.Children.only(props.children))
return props.children
}
function Index(){
return <div style={{ marginTop:'50px' }} >
<WarpComponent>
<Text/>
</WarpComponent>
</div>
}
效果
React.Children.only()
不接受 React.Children.map()
的返回值,因爲它是一個數組而並不是 React
元素。
react-hooks
對於react-hooks
, 我已經寫了三部曲,對於常見的hooks
,我這裏就不多講了,還沒看過的同學建議看一下react-hooks三部曲
。
三部曲
玩轉 react-hooks, 自定義 hooks 設計模式及其實戰
我們今天重點說一下,幾個少用的api
useImperativeHandle
useImperativeHandle
可以配合 forwardRef
自定義暴露給父組件的實例值。這個很有用,我們知道,對於子組件,如果是class
類組件,我們可以通過ref
獲取類組件的實例,但是在子組件是函數組件的情況,如果我們不能直接通過ref
的,那麼此時useImperativeHandle
和 forwardRef
配合就能達到效果。
useImperativeHandle
接受三個參數:
-
第一個參數 ref: 接受
forWardRef
傳遞過來的ref
。 -
第二個參數
createHandle
:處理函數,返回值作爲暴露給父組件的ref
對象。 -
第三個參數
deps
: 依賴項deps
,依賴項更改形成新的ref
對象。
我們來模擬給場景,用useImperativeHandle
,使得父組件能讓子組件中的input
自動賦值並聚焦。
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 <div>
<input
placeholder="請輸入內容"
ref={inputRef}
value={inputValue}
/>
</div>
}
const ForwarSon = forwardRef(Son)
class Index extends React.Component{
cur = null
handerClick(){
const { onFocus , onChangeValue } =this.cur
onFocus()
onChangeValue('let us learn React!')
}
render(){
return <div style={{ marginTop:'50px' }} >
<ForwarSon ref={cur => (this.cur = cur)} />
<button onClick={this.handerClick.bind(this)} >操控子組件</button>
</div>
}
}
效果:
useDebugValue
useDebugValue
可用於在 React
開發者工具中顯示自定義 hook
的標籤。這個hooks
目的就是檢查自定義hooks
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
useTransition
允許延時由state
改變而帶來的視圖渲染。避免不必要的渲染。它還允許組件將速度較慢的數據獲取更新推遲到隨後渲染,以便能夠立即渲染更重要的更新。
const TIMEOUT_MS = { timeoutMs: 2000 }
const [startTransition, isPending] = useTransition(TIMEOUT_MS)
-
useTransition
接受一個對象,timeoutMs
代碼需要延時的時間。 -
返回一個數組。第一個參數: 是一個接受回調的函數。我們用它來告訴
React
需要推遲的state
。第二個參數: 一個布爾值。表示是否正在等待,過度狀態的完成 (延時state
的更新)。
下面我們引入官網的列子,來了解useTransition
的使用。
const SUSPENSE_CONFIG = { timeoutMs: 2000 };
function App() {
const [resource, setResource] = useState(initialResource);
const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG);
return (
<>
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
});
}}
>
Next
</button>
{isPending ? " 加載中..." : null}
<Suspense fallback={<Spinner />}>
<ProfilePage resource={resource} />
</Suspense>
</>
);
}
在這段代碼中,我們使用 startTransition
包裝了我們的數據獲取。這使我們可以立即開始獲取用戶資料的數據,同時推遲下一個用戶資料頁面以及其關聯的 Spinner
的渲染 2 秒鐘( timeoutMs
中顯示的時間)。
這個api
目前處於實驗階段,沒有被完全開放出來。
react-dom
接下來,我們來一起研究react-dom
中比較重要的api
。
render
render
是我們最常用的react-dom
的 api
,用於渲染一個react
元素,一般react
項目我們都用它,渲染根部容器app
。
ReactDOM.render(element, container[, callback])
使用
ReactDOM.render(
< App / >,
document.getElementById('app')
)
ReactDOM.render
會控制container
容器節點裏的內容,但是不會修改容器節點本身。
hydrate
服務端渲染用hydrate
。用法與 render()
相同,但它用於在 ReactDOMServer
渲染的容器中對 HTML
的內容進行 hydrate
操作。
ReactDOM.hydrate(element, container[, callback])
createPortal
Portal
提供了一種將子節點渲染到存在於父組件以外的 DOM
節點的優秀的方案。createPortal
可以把當前組件或 element
元素的子節點,渲染到組件之外的其他地方。
那麼具體應用到什麼場景呢?
比如一些全局的彈窗組件model
,<Model/>
組件一般都寫在我們的組件內部,倒是真正掛載的dom
,都是在外層容器,比如body
上。此時就很適合createPortal
API。
createPortal
接受兩個參數:
ReactDOM.createPortal(child, container)
第一個:child
是任何可渲染的 React
子元素第二個:container
是一個 DOM
元素。
接下來我們實踐一下:
function WrapComponent({ children }){
const domRef = useRef(null)
const [ PortalComponent, setPortalComponent ] = useState(null)
React.useEffect(()=>{
setPortalComponent( ReactDOM.createPortal(children,domRef.current) )
},[])
return <div>
<div class ref={ domRef } ></div>
{ PortalComponent }
</div>
}
class Index extends React.Component{
render(){
return <div style={{ marginTop:'50px' }} >
<WrapComponent>
<div >hello,world</div>
</WrapComponent>
</div>
}
}
效果
我們可以看到,我們children
實際在container
之外掛載的,但是已經被createPortal
渲染到container
中。
unstable_batchedUpdates
在react-legacy
模式下,對於事件,react
事件有批量更新來處理功能, 但是這一些非常規的事件中,批量更新功能會被打破。所以我們可以用react-dom
中提供的unstable_batchedUpdates
來進行批量更新。
一次點擊實現的批量更新
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(){
return <div style={{ marginTop:'50px' }} >
<button onClick={ this.handerClick } >click me</button>
</div>
}
}
效果
渲染次數一次。
批量更新條件被打破
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 助力
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
flushSync
可以將回調函數中的更新任務,放在一個較高的優先級中。我們知道react
設定了很多不同優先級的更新任務。如果一次更新任務在flushSync
回調函數內部,那麼將獲得一個較高優先級的更新。比如
ReactDOM.flushSync(()=>{
/* 此次更新將設置一個較高優先級的更新 */
this.setState({ name: 'alien' })
})
爲了讓大家理解flushSync
,我這裏做一個demo
奉上,
/* 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 <div>
<div>{ number }</div>
<button onClick={this.handerClick} >測試flushSync</button>
</div>
}
}
先不看答案,點擊一下按鈕,打印什麼呢?
我們來點擊一下看看
打印 0 3 4 1 ,相信不難理解爲什麼這麼打印了。
-
首先
flushSync
this.setState({ number: 3 })
設定了一個高優先級的更新,所以 3 先被打印 -
2 4 被批量更新爲 4
相信這個demo
讓我們更深入瞭解了flushSync
。
findDOMNode
findDOMNode
用於訪問組件DOM
元素節點,react
推薦使用ref
模式,不期望使用findDOMNode
。
ReactDOM.findDOMNode(component)
注意的是:
-
1
findDOMNode
只能用在已經掛載的組件上。 -
2 如果組件渲染內容爲
null
或者是false
,那麼findDOMNode
返回值也是null
。 -
3
findDOMNode
不能用於函數組件。
接下來讓我們看一下,findDOMNode
具體怎麼使用的:
class Index extends React.Component{
handerFindDom=()=>{
console.log(ReactDOM.findDOMNode(this))
}
render(){
return <div style={{ marginTop:'100px' }} >
<div>hello,world</div>
<button onClick={ this.handerFindDom } >獲取容器dom</button>
</div>
}
}
效果:
我們完全可以將外層容器用ref
來標記,獲取捕獲原生的dom
節點。
unmountComponentAtNode
從 DOM
中卸載組件,會將其事件處理器和 state
一併清除。如果指定容器上沒有對應已掛載的組件,這個函數什麼也不會做。如果組件被移除將會返回 true
,如果沒有組件可被移除將會返回 false
。
我們來簡單舉例看看unmountComponentAtNode
如何使用?
function Text(){
return <div>hello,world</div>
}
class Index extends React.Component{
node = null
constructor(props){
super(props)
this.state={
numer:1,
}
}
componentDidMount(){
/* 組件初始化的時候,創建一個 container 容器 */
ReactDOM.render(<Text/> , this.node )
}
handerClick=()=>{
/* 點擊卸載容器 */
const state = ReactDOM.unmountComponentAtNode(this.node)
console.log(state)
}
render(){
return <div style={{ marginTop:'50px' }} >
<div ref={ ( node ) => this.node = node } ></div>
<button onClick={ this.handerClick } >click me</button>
</div>
}
}
效果
總結
本文通過react
組件層面,工具層面,hooks
層面,react-dom
瞭解了api
的用法,希望看完的同學,能夠對着文章中的demo
自己敲一遍,到頭來會發現自己成長不少。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wdChmE2-UDNFVyzG7RV0sg