「React 進階」 React 全部 api 解讀 - 基礎實踐大全

很多同學用react開發的時候,真正用到的Reactapi少之又少,基本停留在Component,React.memo等層面, 實際react源碼中,暴露出來的方法並不少,只是我們平時很少用。但是React暴露出這麼多api並非沒有用,想要玩轉react, 就要明白這些API究竟是幹什麼的,應用場景是什麼,今天就讓我們從reactreact-dom, 一次性把react生產環境的暴露api複習個遍 (涵蓋 90%+)。

我們把react,API,分爲組件類工具類hooks,再加上 react-dom  ,一共四大方向,分別加以探討。

爲了能讓屏幕前的你,更明白api, 我是絞盡腦汁, 本文的每一個api基本都會出一個demo演示效果, 彌補一下天書般的react文檔😂😂😂,還有就是我對api基本概念的理解。

老規矩,我們帶着疑問開始今天的閱讀 (自測掌握程度)?

我相信讀完這篇文章,這些問題全都會迎刃而解?

組件類

組件類,詳細分的話有三種類,第一類說白了就是我平時用於繼承的基類組件Component,PureComponent, 還有就是react提供的內置的組件,比如Fragment,StrictMode, 另一部分就是高階組件forwardRef,memo等。

Component

Componentclass組件的根基。類組件一切始於Component。對於React.Component使用,我們沒有什麼好講的。我們這裏重點研究一下reactComponent做了些什麼。

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 觸發這裏的邏輯 */
        }
    }
}

對於Componentreact 處理邏輯還是很簡單的,實例化我們類組件,然後賦值updater對象,負責組件的更新。然後在組件各個階段,執行類組件的render函數,和對應的生命週期函數就可以了。

PureComponent

PureComponentComponent用法,差不多一樣,唯一不同的是,純組件PureComponent會淺比較,propsstate是否相同,來決定是否重新渲染組件。所以一般用於性能調優,減少 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.memoPureComponent作用類似,可以用作性能優化,React.memo 是高階組件,函數組件和類組件都可以使用, 和區別PureComponentReact.memo只能對props的情況確定是否渲染,而PureComponent是針對propsstate

React.memo 接受兩個參數,第一個參數原始組件本身,第二個參數,可以根據一次更新中props是否相同決定原始組件是否重新渲染。是一個返回布爾值,true 證明組件無須重新渲染,false證明組件需要重新渲染,這個和類組件中的shouldComponentUpdate()正好相反 。

**React.memo: 第二個參數 返回 true 組件不渲染 , 返回 false 組件重新渲染。**shouldComponentUpdate: 返回 true 組件渲染 , 返回 false 組件不渲染。

解析來我們做一個場景,控制組件在僅此一個props數字變量,一定範圍渲染。

例子🌰:

控制 props 中的 number

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.lazySuspense配合一起用,能夠有動態加載組件效果。React.lazy 接受一個函數,這個函數需要動態調用 import()。它必須返回一個 Promise ,該 Promise 需要 resolve 一個 default exportReact 組件。

我們模擬一個動態加載的場景。

父組件

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目前有助於:

實踐: 識別不安全的生命週期

對於不安全的生命週期,指的是UNSAFE_componentWillMountUNSAFE_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

可能有的同學還傻傻的分不清楚cloneElementcreateElement區別和作用。

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 對象值 valueProvider,和接受value變化訂閱的Consumer

const MyContext = React.createContext(defaultValue)

createContext接受一個參數defaultValue,如果Consumer上一級一直沒有Provider, 則會應用defaultValue作爲value只有當組件所處的樹中沒有匹配到 Provider 時,其 defaultValue 參數纔會生效。

我們來模擬一個 Context.ProviderContext.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>
}

打印結果:

ProviderConsumer的良好的特性,可以做數據的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 elementLet's learn react together!

Children.map

接下來的五個api都是和react.Chidren相關的,我們來分別介紹一下,我們先來看看官網的描述,React.Children 提供了用於處理 this.props.children 不透明數據結構的實用方法。

有的同學會問遍歷 children用數組方法,mapforEach 不就可以了嗎?請我們注意一下不透明數據結構, 什麼叫做不透明結構?

我們先看一下透明的結構:

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.forEachChildren.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 中的組件總數量,等同於通過 mapforEach 調用回調函數的次數。對於更復雜的結果,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 進階」一文喫透 react-hooks 原理

  玩轉 react-hooks, 自定義 hooks 設計模式及其實戰

react-hooks 如何使用?

我們今天重點說一下,幾個少用的api

useImperativeHandle

useImperativeHandle 可以配合 forwardRef自定義暴露給父組件的實例值。這個很有用,我們知道,對於子組件,如果是class類組件,我們可以通過ref獲取類組件的實例,但是在子組件是函數組件的情況,如果我們不能直接通過ref的,那麼此時useImperativeHandleforwardRef配合就能達到效果。

useImperativeHandle接受三個參數:

我們來模擬給場景,用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的使用。

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-domapi,用於渲染一個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上。此時就很適合createPortalAPI。

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 ,相信不難理解爲什麼這麼打印了。

相信這個demo讓我們更深入瞭解了flushSync

findDOMNode

findDOMNode用於訪問組件DOM元素節點,react推薦使用ref模式,不期望使用findDOMNode

ReactDOM.findDOMNode(component)

注意的是:

接下來讓我們看一下,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