從Mixin到HOC再到HOOKS(四)

React系列

React簡單模擬語法(一)
Jsx, 合成事件與Refs(二)
virtualdom diff算法實現(xiàn)分析(三)
從Mixin到HOC再到HOOKS(四)
createElement, ReactElement與Component部分源碼解析(五)

Mixins(已廢棄)

這是React初期提供的一種組合方案,通過引入一個公用組件,然后可以應(yīng)用公用組件的一些生命周期操作或者定義方法,達到抽離公用代碼提供不同模塊使用的目的.

曾經(jīng)的官方文檔demo如下

var SetIntervalMixin = {
    componentWillMount: function() {
        this.intervals = [];
    },
    setInterval: function() {
        this.intervals.push(setInterval.apply(null, arguments));
    },
    componentWillUnmount: function() {
        this.intervals.map(clearInterval);
    }
};

var TickTock = React.createClass({
    mixins: [SetIntervalMixin], // Use the mixin
    getInitialState: function() {
        return {seconds: 0};
    },
    componentDidMount: function() {
        this.setInterval(this.tick, 1000); // Call a method on the mixin
    },
    tick: function() {
        this.setState({seconds: this.state.seconds + 1});
    },
    render: function() {
        return (
            

React has been running for {this.state.seconds} seconds.

); } }); React.render( , document.getElementById('example') );

但是Mixins只能應(yīng)用在createClass的創(chuàng)建方式,在后來的class寫法中已經(jīng)被廢棄了.原因在于:

  1. mixin引入了隱式依賴關(guān)系
  2. 不同mixins之間可能會有先后順序甚至代碼沖突覆蓋的問題
  3. mixin代碼會導(dǎo)致滾雪球式的復(fù)雜性
    詳細介紹mixin危害性文章可直接查閱Mixins Considered Harmful

高階組件(Higher-order component)

HOC是一種React的進階使用方法,大概原理就是接收一個組件然后返回一個新的繼承組件,繼承方式分兩種

屬性代理(Props Proxy)

最基本的實現(xiàn)方式

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      return 
    }
  }
}

從代碼可以看出屬性代理方式其實就是接受一個 WrappedComponent 組件作為參數(shù)傳入,并返回一個繼承了 React.Component 組件的類,且在該類的 render() 方法中返回被傳入的 WrappedComponent 組件

抽離state && 操作props

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        name: 'PropsProxyHOC'
      }
    }

    logName() {
      console.log(this.name)
    }

    render() {
      const newProps = {
        name: this.state.name,
        logName: this.logName
      }
      return 
    }
  }
}

class Main extends Component {
  componentDidMount() {
    this.props.logName()
  }

  render() {
    return (
      PropsProxyHOC
    )
  }
}

export default PropsProxyHOC(Main);

demo代碼可以參考這里
有種常見的情況是用來做雙向綁定

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    constructor(props) {
      super(props)
      this.state = { fields: {} }
    }

    getField(fieldName) {
      const _s = this.state
      if (!_s.fields[fieldName]) {
        _s.fields[fieldName] = {
          value: '',
          onChange: event => {
            this.state.fields[fieldName].value = event.target.value
            // 強行觸發(fā)render
            this.forceUpdate()
            console.log(this.state)
          }
        }
      }

      return {
        value: _s.fields[fieldName].value,
        onChange: _s.fields[fieldName].onChange
      }
    }

    render() {
      const newProps = {
        fields: this.getField.bind(this),
      }
      return 
    }
  }
}

// 被獲取ref實例組件
class Main extends Component {
  render() {
    return 
  }
}

export default PropsProxyHOC(Main);

demo代碼可以參考這里

獲取被繼承refs實例

因為這是一個被HOC包裝過的新組件,所以想要在HOC里面獲取新組件的ref需要用些特殊方式,但是不管哪種,都需要在組件掛載之后才能獲取到.并且不能在無狀態(tài)組件(函數(shù)類型組件)上使用 ref 屬性,因為無狀態(tài)組件沒有實例。愛掏網(wǎng) - it200.com

通過父元素傳遞方法獲取

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      const newProps = {}
      // 監(jiān)聽到有對應(yīng)方法才生成props實例
      typeof this.props.getInstance === "function" && (newProps.ref = this.props.getInstance)
      return 
    }
  }
}

// 被獲取ref實例組件
class Main extends Component {
  render() {
    return (
      Main
    )
  }
}

const HOCComponent = PropsProxyHOC(Main)

class ParentComponent extends Component {
  componentWillMount() {
    console.log('componentWillMount: ', this.wrappedInstance)
  }

  componentDidMount() {
    console.log('componentDidMount: ', this.wrappedInstance)
  }

  // 提供給高階組件調(diào)用生成實例
  getInstance(ref) {
    this.wrappedInstance = ref;
  }

  render() {
    return (
      
    )
  }
}

export default (ParentComponent);

demo代碼可以參考這里

通過高階組件當中間層

相比較上一方式,需要在高階組件提供設(shè)置賦值函數(shù),并且需要一個props屬性做標記

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    // 返回ref實例
    getWrappedInstance = () => {
      if (this.props.withRef) {
        return this.wrappedInstance;
      }
    }

    //設(shè)置ref實例
    setWrappedInstance = (ref) => {
      this.wrappedInstance = ref;
    }

    render() {
      const newProps = {}
      // 監(jiān)聽到有對應(yīng)方法才賦值props實例
      this.props.withRef && (newProps.ref = this.setWrappedInstance)
      return 
    }
  }
}

// 被獲取ref實例組件
class Main extends Component {
  render() {
    return (
      Main
    )
  }
}

const HOCComponent = PropsProxyHOC(Main)

class ParentComponent extends Component {

  componentWillMount() {
    console.log('componentWillMount: ', this.refs.child)
  }

  componentDidMount() {
    console.log('componentDidMount: ', this.refs.child.getWrappedInstance())
  }

  render() {
    return (
      
    )
  }
}

export default (ParentComponent);

demo代碼可以參考這里

forwardRef

React.forwardRef 會創(chuàng)建一個React組件,這個組件能夠?qū)⑵浣邮艿?ref 屬性轉(zhuǎn)發(fā)到其組件樹下的另一個組件中。愛掏網(wǎng) - it200.com這種技術(shù)并不常見,但在以下兩種場景中特別有用:

  • 轉(zhuǎn)發(fā) refs 到 DOM 組件
  • 在高階組件中轉(zhuǎn)發(fā) refs
const FancyButton = React.forwardRef((props, ref) => (
  
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
Click me!;

以下是對上述示例發(fā)生情況的逐步解釋:

  1. 我們通過調(diào)用 React.createRef 創(chuàng)建了一個 React ref 并將其賦值給 ref 變量。愛掏網(wǎng) - it200.com
  2. 我們通過指定 ref 為 JSX 屬性,將其向下傳遞給 愛掏網(wǎng) - it200.com
  3. React 傳遞 ref 給 fowardRef 內(nèi)函數(shù) (props, ref) => ...,作為其第二個參數(shù)。愛掏網(wǎng) - it200.com
  4. 我們向下轉(zhuǎn)發(fā)該 ref 參數(shù)到 ,將其指定為 JSX 屬性。愛掏網(wǎng) - it200.com
  5. 當 ref 掛載完成,ref.current 將指向 DOM 節(jié)點。愛掏網(wǎng) - it200.com

劫持渲染

最簡單的例子莫過于loading組件了

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      return this.props.isLoading ? Loading... : 
    }
  }
}

// 被獲取ref實例組件
class Main extends Component {
  render() {
    return (
      Main
    )
  }
}

const HOCComponent = PropsProxyHOC(Main)

class ParentComponent extends Component {
  constructor() {
    super()
    this.state = {
      isLoading: true
    }
  }

  render() {
    setTimeout(() => this.setState({ isLoading: false }), 2000)
    return (
      
    )
  }
}

export default (ParentComponent);

當然也能用于布局上嵌套在其他元素輸出
demo代碼可以參考這里

反向繼承(Inheritance Inversion)

最簡單的demo代碼

function InheritanceInversionHOC(WrappedComponent) {
  return class NewComponent extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

在這里WrappedComponent成了被繼承的那一方,從而可以在高階組件中獲取到傳遞組件的所有相關(guān)實例

獲取繼承組件實例

function InheritanceInversionHOC(WrappedComponent) {
  return class NewComponent extends WrappedComponent {

    componentDidMount() {
      console.log('componentDidMount: ', this)
    }

    render() {
      return super.render()
    }
  }
}

// 被獲取ref實例組件
class Main extends Component {
  constructor() {
    super()
    this.state = {
      name: 'WrappedComponent'
    }
  }

  render() {
    return (
      Main
    )
  }
}

export default (InheritanceInversionHOC(Main));

demo代碼可以參考這里

修改props和劫持渲染

再講解demo之前先科普React的一個方法

React.cloneElement(
  element,
  [props],
  [...children]
)

以 element 元素為樣板克隆并返回新的 React 元素。愛掏網(wǎng) - it200.com返回元素的 props 是將新的 props 與原始元素的 props 淺層合并后的結(jié)果。愛掏網(wǎng) - it200.com新的子元素將取代現(xiàn)有的子元素,而來自原始元素的 key 和 ref 將被保留。愛掏網(wǎng) - it200.com
React.cloneElement() 幾乎等同于:

{children}

但是,這也保留了組件的 ref。愛掏網(wǎng) - it200.com這意味著當通過 ref 獲取子節(jié)點時,你將不會意外地從你祖先節(jié)點上竊取它。愛掏網(wǎng) - it200.com相同的 ref 將添加到克隆后的新元素中。愛掏網(wǎng) - it200.com

相比屬性繼承來說,反向繼承修改props會比較復(fù)雜一點

function InheritanceInversionHOC(WrappedComponent) {
  return class NewComponent extends WrappedComponent {
    constructor() {
      super()
      this.state = {
        'a': 'b'
      }
    }

    componentDidMount() {
      console.log('componentDidMount: ', this)
    }

    render() {
      const wrapperTree = super.render()
      const newProps = {
        name: 'NewComponent'
      }
      const newTree = React.cloneElement(wrapperTree, newProps, wrapperTree.props.children)
      console.log('newTree: ', newTree)
      return newTree
    }
  }
}

// 被獲取ref實例組件
class Main extends Component {
  render() {
    return (
      Main
    )
  }
}

export default (InheritanceInversionHOC(Main));

demo代碼可以參考這里

為什么需要用到cloneElement方法?

因為render函數(shù)內(nèi)實際上是調(diào)用React.creatElement產(chǎn)生的React元素,盡管我們可以拿到這個方法但是無法修改它.可以用getOwnPropertyDescriptors查看它的配置項
所以用cloneElement創(chuàng)建新的元素替代

相比較屬性繼承來說,后者只能條件性選擇是否渲染WrappedComponent,但是前者可以更加細粒度劫持渲染元素,可以獲取到 state,props,組件生命周期(component lifecycle)鉤子,以及渲染方法(render),但是依舊不能保證WrappedComponent里的子組件是否渲染,也無法劫持.

注意

  • 靜態(tài)屬性失效
    因為高階組件返回的已經(jīng)不是原組件了,所以原組件的靜態(tài)屬性方法已經(jīng)無法獲取,除非你主動將它們拷貝到返回組件中
  • 渲染機制
    因為高階組件返回的是新組件,里面的唯一標志也會變化,所以不建議在render里面也調(diào)用高階組件,這會導(dǎo)致其每次都重新卸載再渲染,即使它可能長得一樣.
    所以建議高階組件都是無副作用的純函數(shù),即相同輸入永遠都是相同輸出,不允許任何有可變因素.
  • 嵌套過深
    在原組件中如果包裹層級過多會產(chǎn)生類似回調(diào)地獄的煩惱,難以調(diào)試,可閱讀性糟糕
  • 遵守規(guī)則
    如果沒有規(guī)范情況下,也可能造成代碼沖突覆蓋的局面

HOOKS

Hooks是React v16.7.0-alpha中加入的新特性。愛掏網(wǎng) - it200.com它可以讓你在class以外使用state和其他React特性。愛掏網(wǎng) - it200.com

Hooks是可以讓你與React狀態(tài)以及函數(shù)式組件的生命周期特性“掛鉤”的函數(shù)。愛掏網(wǎng) - it200.com鉤子是為了讓你拋棄類使用React的,所以它不能在類中運行,但是可以用在純函數(shù)中,這就解決了一直以來可能因為需要用到生命周期或者react狀態(tài)的時候,你不得不將原本的純函數(shù)代碼整個替換成Class寫法的煩惱.

Hooks也分兩種

State Hook

能夠讓你在不使用Class的情況下使用state和其他的React功能

useState

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    
      

You clicked {count} times

); }

等價于下面Class寫法

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      
        

You clicked {this.state.count} times

); } }

demo代碼可以參考這里
從上面可以看出useState實際上就是在state里聲明一個變量并且初始化了一個值而且提供一個可以改變對應(yīng)state的函數(shù).因為在純函數(shù)中沒有this.state.count的這種用法,所以直接使用count替代
上面的count就是聲明的變量,setCount就是改變變量的方法.
需要注意的一點是useState和this.state有點不同,它只有在組件第一次render才會創(chuàng)建狀態(tài),之后每次都只會返回當前的值.

賦值初始值的時候如果需要經(jīng)過某些邏輯處理才能得到的話,可以通過函數(shù)傳遞,例如

const [count, setCount] = useState(() => doSomethings())

如果改變需要根據(jù)之前的數(shù)據(jù)變化,可以通過函數(shù)接收舊數(shù)據(jù),例如

setCount(prevCount => prevCount + 1)

如果是想聲明多個state的時候,就需要使用多次useState

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}

或者通過組合對象一次合并多個數(shù)據(jù)

Effect Hook

執(zhí)行有副作用的函數(shù),你可以把 useEffect Hooks 視作 componentDidMountcomponentDidUpdatecomponentWillUnmount 的結(jié)合,useEffect 會在瀏覽器繪制后延遲執(zhí)行,但會保證在任何新的渲染前執(zhí)行愛掏網(wǎng) - it200.comReact 將在組件更新前刷新上一輪渲染的 effect。愛掏網(wǎng) - it200.comReact 組件中的 side effects 大致可以分為兩種

不需要清理

有時我們想要在 React 更新過 DOM 之后執(zhí)行一些額外的操作。愛掏網(wǎng) - it200.com比如網(wǎng)絡(luò)請求、手動更新 DOM 、以及打印日志都是常見的不需要清理的 effects

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    console.log(`componentDidMount: You clicked ${this.state.count} times`)
  }

  componentDidUpdate() {
    console.log(`componentDidUpdate: You clicked ${this.state.count} times`)
  }

  render() {
    return (
      
        

You clicked {this.state.count} times

); } }

如上所示,如果放在render的話在掛載前也會觸發(fā),但是為了避免這個問題我們不得不在兩個生命周期寫同樣的代碼.但是如果我們換成HOOKS的寫法

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    
      

You clicked {count} times

); }

demo代碼可以參考這里

useEffect 做了什么? 通過這個 Hook,React 知道你想要這個組件在每次 render 之后做些事情。愛掏網(wǎng) - it200.comReact 會記錄下你傳給 useEffect 的這個方法,然后在進行了 DOM 更新之后調(diào)用這個方法。愛掏網(wǎng) - it200.com但我們同樣也可以進行數(shù)據(jù)獲取或是調(diào)用其它必要的 API。愛掏網(wǎng) - it200.com

為什么 useEffect 在組件內(nèi)部調(diào)用? 將 useEffect 放在一個組件內(nèi)部,可以讓我們在 effect 中,即可獲得對 count state(或其它 props)的訪問,而不是使用一個特殊的 API 去獲取它。愛掏網(wǎng) - it200.com

useEffect 是不是在每次 render 之后都會調(diào)用? 默認情況下,它會在第一次 render 之后的每次 update 后運行。愛掏網(wǎng) - it200.comReact 保證每次運行 effects 之前 DOM 已經(jīng)更新了。愛掏網(wǎng) - it200.com

使用上還有哪些區(qū)別"需要清理的-Effect">需要清理的 Effect

比較常見的就類似掛載的時候監(jiān)聽事件或者開啟定時器,卸載的時候就移除.

class Example extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    document.addEventListener('click', this.clickFunc, false)
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.clickFunc)
  }

  clickFunc(e) {
    //  doSomethings
    console.log(e)
  }

  render() {
    return (
      
    );
  }
}

換成HOOKS寫法類似,只是會返回新的函數(shù)

function Example() {

  useEffect(() => {
    document.addEventListener('click', clickFunc, false)
    return () => {
      document.removeEventListener('click', clickFunc)
    }
  });

  function clickFunc(e) {
    //  doSomethings
    console.log(e)
  }

  return (
    
  );
}

demo代碼可以參考這里

我們?yōu)槭裁丛?effect 中返回一個函數(shù) 這是一種可選的清理機制。愛掏網(wǎng) - it200.com每個 effect 都可以返回一個用來在晚些時候清理它的函數(shù)。愛掏網(wǎng) - it200.com這讓我們讓添加和移除訂閱的邏輯彼此靠近。愛掏網(wǎng) - it200.com它們是同一個 effect 的一部分!

React 究竟在什么時候清理 effect? React 在每次組件 unmount 的時候執(zhí)行清理。愛掏網(wǎng) - it200.com然而,正如我們之前了解的那樣,effect 會在每次 render 時運行,而不是僅僅運行一次。愛掏網(wǎng) - it200.com這也就是為什么 React 會在執(zhí)行下一個 effect 之前,上一個 effect 就已被清除。愛掏網(wǎng) - it200.com

我們可以修改一下代碼看看effect的運行機制

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('addEventListener')
    document.addEventListener('click', clickFunc, false)
    return () => {
      console.log('removeEventListener')
      document.removeEventListener('click', clickFunc)
    }
  });

  function clickFunc(e) {
    //  doSomethings
    console.log(e)
    setCount(count+1)
  }

  return (
    
  );
}

demo代碼可以參考這里
可以看到上面代碼在每次更新都是重新監(jiān)聽,想要避免這種情況可以往下繼續(xù)看.

進階使用

有時候我們可能有多套邏輯寫在不同的生命周期里,如果換成HOOKS寫法的話我們可以按功能劃分使用多個,React將會按照指定的順序應(yīng)用每個effect。愛掏網(wǎng) - it200.com

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`You clicked ${count} times`)
  });

  useEffect(() => {
    document.addEventListener('click', clickFunc, false)
    return () => {
      document.removeEventListener('click', clickFunc)
    }
  });

  function clickFunc(e) {
    //  doSomethings
    console.log(e)
  }

  return (
    
      

You clicked {count} times

); }

demo代碼可以參考這里

為什么Effects會在每次更新后執(zhí)行

如果你們以前使用class的話可能會有疑惑,為什么不是在卸載階段執(zhí)行一次.從官網(wǎng)解釋代碼看

componentDidMount() {
  ChatAPI.subscribeToFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

componentWillUnmount() {
  ChatAPI.unsubscribeFromFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

它在掛載階段監(jiān)聽,移除階段移除監(jiān)聽,每次觸發(fā)就根據(jù)this.props.friend.id做出對應(yīng)處理.但是這里有個隱藏的bug就是當移除階段的時候獲取的this.props.friend.id可能是舊的數(shù)據(jù),引起的問題就是卸載時候會使用錯誤的id而導(dǎo)致內(nèi)存泄漏或崩潰,所以在class的時候一般都會在componentDidUpdate 做處理

componentDidUpdate(prevProps) {
  // Unsubscribe from the previous friend.id
  ChatAPI.unsubscribeFromFriendStatus(
    prevProps.friend.id,
    this.handleStatusChange
  );
  // Subscribe to the next friend.id
  ChatAPI.subscribeToFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

但是如果我們換成HOOKS的寫法就不會有這種bug

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
});

這是因為HOOKS會在應(yīng)用下一個effects之前清除前一個effects,此行為默認情況下確保一致性,并防止由于缺少更新邏輯而在類組件中常見的錯誤

通過跳過effects提升性能

就在上面我們知道每次render都會觸發(fā)effects機制可能會有性能方面的問題,在class的寫法里我們可以通過componentDidUpdate做選擇是否更新

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

而在useEffect里我們可以通過傳遞一組數(shù)據(jù)給它作為第二參數(shù),如果在下次執(zhí)行的時候該數(shù)據(jù)沒有發(fā)生變化的話React會跳過當次應(yīng)用

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

所以上面提到的bug案例可以通過這個方式做解決

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes

注意

如果你想使用這種優(yōu)化方式,請確保數(shù)組中包含了所有外部作用域中會發(fā)生變化且在 effect 中使用的變量,否則你的代碼會一直引用上一次render的舊數(shù)據(jù).

如果你想要effects只在掛載和卸載時各清理一次的話,可以傳遞一個空數(shù)組作為第二參數(shù).相當于告訴React你的effects不依賴于任何的props或者state,所以沒必要重復(fù)執(zhí)行.

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

返回一個 memoized 回調(diào)函數(shù)。愛掏網(wǎng) - it200.com
把內(nèi)聯(lián)回調(diào)函數(shù)及依賴項數(shù)組作為參數(shù)傳入 useCallback,它將返回該回調(diào)函數(shù)的 memoized 版本,該回調(diào)函數(shù)僅在某個依賴項改變時才會更新。愛掏網(wǎng) - it200.com當你把回調(diào)函數(shù)傳遞給經(jīng)過優(yōu)化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將非常有用。愛掏網(wǎng) - it200.com
useCallback(fn, deps) 相當于 useMemo(() => fn, deps)。愛掏網(wǎng) - it200.com

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。愛掏網(wǎng) - it200.com它接收一個形如 (state, action) => newState 的 reducer,并返回當前的 state 以及與其配套的 dispatch 方法。愛掏網(wǎng) - it200.com

在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較復(fù)雜且包含多個子值,或者下一個 state 依賴于之前的 state 等。愛掏網(wǎng) - it200.com并且,使用 useReducer 還能給那些會觸發(fā)深更新的組件做性能優(yōu)化,因為你可以向子組件傳遞 dispatch 而不是回調(diào)函數(shù) 。愛掏網(wǎng) - it200.com

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    
      Count: {state.count}
      
      
    
  );
}

demo代碼可以參考這里
從語法上你們會看到還有一個init的入?yún)?是用來做惰性初始化,將 init 函數(shù)作為 useReducer 的第三個參數(shù)傳入,這樣初始 state 將被設(shè)置為 init(initialArg),這么做可以將用于計算 state 的邏輯提取到 reducer 外部,這也為將來對重置 state 的 action 做處理提供了便利

const initialState = 0;
function init(initialCount) {
  return { count: initialCount };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState, init);
  return (
    
      Count: {state.count}
      
      
      
    
  );
}

demo代碼可以參考這里

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一個 memoized 值。愛掏網(wǎng) - it200.com
把“創(chuàng)建”函數(shù)和依賴項數(shù)組作為參數(shù)傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。愛掏網(wǎng) - it200.com這種優(yōu)化有助于避免在每次渲染時都進行高開銷的計算。愛掏網(wǎng) - it200.com
記住,傳入 useMemo 的函數(shù)會在渲染期間執(zhí)行。愛掏網(wǎng) - it200.com請不要在這個函數(shù)內(nèi)部執(zhí)行與渲染無關(guān)的操作,諸如副作用這類的操作屬于 useEffect 的適用范疇,而不是 useMemo。愛掏網(wǎng) - it200.com
如果沒有提供依賴項數(shù)組,useMemo 在每次渲染時都會計算新的值。愛掏網(wǎng) - it200.com

useRef

const refContainer = useRef(initialValue);

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。愛掏網(wǎng) - it200.com返回的 ref 對象在組件的整個生命周期內(nèi)保持不變。愛掏網(wǎng) - it200.com

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已掛載到 DOM 上的文本輸入元素
    inputEl.current.focus();
  };
  return (
    
      
    
  );
}

demo代碼可以參考這里
本質(zhì)上,useRef 就像是可以在其 .current 屬性中保存一個可變值的“盒子”。愛掏網(wǎng) - it200.com
你應(yīng)該熟悉 ref 這一種訪問 DOM 的主要方式。愛掏網(wǎng) - it200.com如果你將 ref 對象以

形式傳入組件,則無論該節(jié)點如何改變,React 都會將 ref 對象的 .current 屬性設(shè)置為相應(yīng)的 DOM 節(jié)點。愛掏網(wǎng) - it200.com
然而,useRef() 比 ref 屬性更有用。愛掏網(wǎng) - it200.com它可以很方便地保存任何可變值,其類似于在 class 中使用實例字段的方式。愛掏網(wǎng) - it200.com
這是因為它創(chuàng)建的是一個普通 Javascript 對象。愛掏網(wǎng) - it200.com而 useRef() 和自建一個 {current: ...} 對象的唯一區(qū)別是,useRef 會在每次渲染時返回同一個 ref 對象。愛掏網(wǎng) - it200.com
請記住,當 ref 對象內(nèi)容發(fā)生變化時,useRef 并不會通知你。愛掏網(wǎng) - it200.com變更 .current 屬性不會引發(fā)組件重新渲染。愛掏網(wǎng) - it200.com如果想要在 React 綁定或解綁 DOM 節(jié)點的 ref 時運行某些代碼,則需要使用回調(diào) ref 來實現(xiàn)。愛掏網(wǎng) - it200.com

HOOKS規(guī)范

在頂層調(diào)用HOOKS

不要在循環(huán),條件,或者內(nèi)嵌函數(shù)中調(diào)用.這都是為了保證你的代碼在每次組件render的時候會按照相同的順序執(zhí)行HOOKS,而這也是能夠讓React在多個useState和useEffect執(zhí)行中正確保存數(shù)據(jù)的原因

只在React函數(shù)調(diào)用HOOKS

  • React函數(shù)組件調(diào)用
  • 從自定義HOOKS中調(diào)用

可以確保你源碼中組件的所有有狀態(tài)邏輯都是清晰可見的.

自定義HOOKS

我們可以將相關(guān)邏輯抽取出來

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

我必須以“use”開頭為自定義鉤子命名嗎? 這項公約非常重要。愛掏網(wǎng) - it200.com如果沒有它,我們就不能自動檢查鉤子是否違反了規(guī)則,因為我們無法判斷某個函數(shù)是否包含對鉤子的調(diào)用。愛掏網(wǎng) - it200.com

使用相同鉤子的兩個組件是否共享狀態(tài)? 不。愛掏網(wǎng) - it200.com自定義鉤子是一種重用有狀態(tài)邏輯的機制(例如設(shè)置訂閱并記住當前值),但是每次使用自定義鉤子時,其中的所有狀態(tài)和效果都是完全隔離的。愛掏網(wǎng) - it200.com

自定義鉤子如何獲得隔離狀態(tài)? 對鉤子的每個調(diào)用都處于隔離狀態(tài)。愛掏網(wǎng) - it200.com從React的角度來看,我們的組件只調(diào)用useStateuseEffect愛掏網(wǎng) - it200.com

問題

Hook 會替代 render props 和高階組件嗎?

通常,render props 和高階組件只渲染一個子節(jié)點。愛掏網(wǎng) - it200.com我們認為讓 Hook 來服務(wù)這個使用場景更加簡單。愛掏網(wǎng) - it200.com這兩種模式仍有用武之地,(例如,一個虛擬滾動條組件或許會有一個 renderItem 屬性,或是一個可見的容器組件或許會有它自己的 DOM 結(jié)構(gòu))。愛掏網(wǎng) - it200.com但在大部分場景下,Hook 足夠了,并且能夠幫助減少嵌套。愛掏網(wǎng) - it200.com

生命周期方法要如何對應(yīng)到 Hook?

  • constructor:函數(shù)組件不需要構(gòu)造函數(shù)。愛掏網(wǎng) - it200.com你可以通過調(diào)用 useState 來初始化 state。愛掏網(wǎng) - it200.com如果計算的代價比較昂貴,你可以傳一個函數(shù)給 useState。愛掏網(wǎng) - it200.com
  • getDerivedStateFromProps:改為在渲染時安排一次更新。愛掏網(wǎng) - it200.com
  • shouldComponentUpdate:詳見 React.memo.
  • render:這是函數(shù)組件體本身。愛掏網(wǎng) - it200.com
  • componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表達所有這些的組合。愛掏網(wǎng) - it200.com
  • componentDidCatch and getDerivedStateFromError:目前還沒有這些方法的 Hook 等價寫法,但很快會加上。愛掏網(wǎng) - it200.com

我可以只在更新時運行 effect 嗎?

這是個比較罕見的使用場景。愛掏網(wǎng) - it200.com如果你需要的話,你可以 使用一個可變的 ref 手動存儲一個布爾值來表示是首次渲染還是后續(xù)渲染,然后在你的 effect 中檢查這個標識。愛掏網(wǎng) - it200.com

如何獲取上一輪的 props 或 state?

目前,你可以通過ref來手動實現(xiàn):

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return 

Now: {count}, before: {prevCount}

; } function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }

有類似 forceUpdate 的東西嗎?

如果前后兩次的值相同,useState 和 useReducer Hook 都會放棄更新。愛掏網(wǎng) - it200.com原地修改 state 并調(diào)用 setState 不會引起重新渲染。愛掏網(wǎng) - it200.com
通常,你不應(yīng)該在 React 中修改本地 state。愛掏網(wǎng) - it200.com然而,作為一條出路,你可以用一個增長的計數(shù)器來在 state 沒變的時候依然強制一次重新渲染:

const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
  forceUpdate();
}

我該如何測量 DOM 節(jié)點?

要想測量一個 DOM 節(jié)點的位置或是尺寸,你可以使用 callback ref。愛掏網(wǎng) - it200.com每當 ref 被附加到另一個節(jié)點,React 就會調(diào)用 callback。愛掏網(wǎng) - it200.com

function MeasureExample() {
  const [rect, ref] = useClientRect();
  return (
    
      

Hello, world

{rect !== null && } ); } function useClientRect() { const [rect, setRect] = useState(null); const ref = useCallback(node => { if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); return [rect, ref]; }

demo代碼可以參考這里
使用 callback ref 可以確保 即便子組件延遲顯示被測量的節(jié)點 (比如為了響應(yīng)一次點擊),我們依然能夠在父組件接收到相關(guān)的信息,以便更新測量結(jié)果。愛掏網(wǎng) - it200.com

注意到我們傳遞了 [] 作為 useCallback 的依賴列表。愛掏網(wǎng) - it200.com這確保了 ref callback 不會在再次渲染時改變,因此 React 不會在非必要的時候調(diào)用它。愛掏網(wǎng) - it200.com

我該如何實現(xiàn) shouldComponentUpdate?

你可以用 React.memo 包裹一個組件來對它的 props 進行淺比較:

const Button = React.memo((props) => {
  // 你的組件
});

React.memo 等效于 PureComponent,但它只比較 props。愛掏網(wǎng) - it200.com(你也可以通過第二個參數(shù)指定一個自定義的比較函數(shù)來比較新舊 props。愛掏網(wǎng) - it200.com如果函數(shù)返回 true,就會跳過更新。愛掏網(wǎng) - it200.com

React.memo 不比較 state,因為沒有單一的 state 對象可供比較。愛掏網(wǎng) - it200.com但你也可以讓子節(jié)點變?yōu)榧兘M件,或者 用useMemo優(yōu)化每一個具體的子節(jié)點。愛掏網(wǎng) - it200.com

如何惰性創(chuàng)建昂貴的對象?

第一個常見的使用場景是當創(chuàng)建初始 state 很昂貴時,為避免重新創(chuàng)建被忽略的初始 state,我們可以傳一個函數(shù)給 useState,React 只會在首次渲染時調(diào)用這個函數(shù)

function Table(props) {
  // createRows() 只會被調(diào)用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}

你或許也會偶爾想要避免重新創(chuàng)建 useRef() 的初始值。愛掏網(wǎng) - it200.comuseRef 不會像 useState 那樣接受一個特殊的函數(shù)重載。愛掏網(wǎng) - it200.com相反,你可以編寫你自己的函數(shù)來創(chuàng)建并將其設(shè)為惰性的:

function Image(props) {
  const ref = useRef(null);

  //  IntersectionObserver 只會被惰性創(chuàng)建一次
  function getObserver() {
    let observer = ref.current;
    if (observer !== null) {
      return observer;
    }
    let newObserver = new IntersectionObserver(onIntersect);
    ref.current = newObserver;
    return newObserver;
  }

  // 當你需要時,調(diào)用 getObserver()
  // ...
}

Hook 會因為在渲染時創(chuàng)建函數(shù)而變慢嗎?

不會。愛掏網(wǎng) - it200.com在現(xiàn)代瀏覽器中,閉包和類的原始性能只有在極端場景下才會有明顯的差別。愛掏網(wǎng) - it200.com
除此之外,可以認為 Hook 的設(shè)計在某些方面更加高效:

  • Hook 避免了 class 需要的額外開支,像是創(chuàng)建類實例和在構(gòu)造函數(shù)中綁定事件處理器的成本。愛掏網(wǎng) - it200.com
  • 符合語言習(xí)慣的代碼在使用 Hook 時不需要很深的組件樹嵌套。愛掏網(wǎng) - it200.com這個現(xiàn)象在使用高階組件、render props、和 context 的代碼庫中非常普遍。愛掏網(wǎng) - it200.com組件樹小了,React 的工作量也隨之減少。愛掏網(wǎng) - it200.com

傳統(tǒng)上認為,在 React 中使用內(nèi)聯(lián)函數(shù)對性能的影響,與每次渲染都傳遞新的回調(diào)會如何破壞子組件的 shouldComponentUpdate 優(yōu)化有關(guān)。愛掏網(wǎng) - it200.comHook 從三個方面解決了這個問題。愛掏網(wǎng) - it200.com

  • useCallback Hook 允許你在重新渲染之間保持對相同的回調(diào)引用以使得 shouldComponentUpdate 繼續(xù)工作:
  • useMemo Hook 使控制具體子節(jié)點何時更新變得更容易,減少了對純組件的需要。愛掏網(wǎng) - it200.com
  • 最后,useReducer Hook 減少了對深層傳遞回調(diào)的需要,就如下面解釋的那樣。愛掏網(wǎng) - it200.com

如何避免向下傳遞回調(diào)?

在大型的組件樹中,我們推薦的替代方案是通過 contextuseReducer 往下傳一個 dispatch 函數(shù):

const TodosDispatch = React.createContext(null);

function TodosApp() {
  // 提示:`dispatch` 不會在重新渲染之間變化
  const [todos, dispatch] = useReducer(todosReducer);

  return (
    
  );
}

TodosApp 內(nèi)部組件樹里的任何子節(jié)點都可以使用 dispatch 函數(shù)來向上傳遞 actions

function DeepChild(props) {
  // 如果我們想要執(zhí)行一個 action,我們可以從 context 中獲取 dispatch。愛掏網(wǎng) - it200.com
  const dispatch = useContext(TodosDispatch);

  function handleClick() {
    dispatch({ type: 'add', text: 'hello' });
  }

  return (
    
  );
}

總而言之,從維護的角度來這樣看更加方便(不用不斷轉(zhuǎn)發(fā)回調(diào)),同時也避免了回調(diào)的問題。愛掏網(wǎng) - it200.com像這樣向下傳遞 dispatch 是處理深度更新的推薦模式。愛掏網(wǎng) - it200.com

React 是如何把對 Hook 的調(diào)用和組件聯(lián)系起來的?

React 保持對當先渲染中的組件的追蹤。愛掏網(wǎng) - it200.com多虧了 Hook 規(guī)范,我們得知 Hook 只會在 React 組件中被調(diào)用(或自定義 Hook —— 同樣只會在 React 組件中被調(diào)用)。愛掏網(wǎng) - it200.com
每個組件內(nèi)部都有一個「記憶單元格」列表。愛掏網(wǎng) - it200.com它們只不過是我們用來存儲一些數(shù)據(jù)的 JavaScript 對象。愛掏網(wǎng) - it200.com當你用 useState() 調(diào)用一個 Hook 的時候,它會讀取當前的單元格(或在首次渲染時將其初始化),然后把指針移動到下一個。愛掏網(wǎng) - it200.com這就是多個 useState() 調(diào)用會得到各自獨立的本地 state 的原因。愛掏網(wǎng) - it200.com

聲明:所有內(nèi)容來自互聯(lián)網(wǎng)搜索結(jié)果,不保證100%準確性,僅供參考。如若本站內(nèi)容侵犯了原著者的合法權(quán)益,可聯(lián)系我們進行處理。
發(fā)表評論
更多 網(wǎng)友評論0 條評論)
暫無評論

返回頂部

主站蜘蛛池模板: 男人和女人做免费做爽爽视频| 一二三四社区在线中文视频| 黑人巨大两根69gv| 最新版天堂中文在线官网| 国产特级淫片免费看| 亚洲国产成人久久精品影视| 6080手机理论三级理论| 欧美性bbbwbbbw| 国产精品一区视频| 亚洲av成人综合网| 黄网站免费在线| 日本激情一区二区三区| 国产一起色一起爱| 中文字幕av无码专区第一页| 精品国偷自产在线视频| 好男人观看免费视频播放全集 | 日本理论片理论免费| 国产乱码一区二区三区| 中文字幕日韩wm二在线看| 精品少妇一区二区三区视频| 天天躁夜夜躁狠狠躁2021| 亚洲精品亚洲人成在线观看| 6一13小幻女| 旧里番yy4480在线高清影院| 国产亚洲精品资源在线26U| 中文在线最新版天堂| 精品免费国产一区二区| 夜夜爽夜夜叫夜夜高潮漏水| 亚洲图片欧美日韩| 黄色网址免费观看| 日本乱子伦xxxx| 午夜人妻久久久久久久久| a毛片免费观看完整| 欧美日韩在线不卡| 国产成人精品无码片区在线观看| 久久久91精品国产一区二区三区| 精品欧美一区二区精品久久| 在线毛片免费观看| 亚洲av成人一区二区三区| 超清首页国产亚洲丝袜| 妞干网在线免费视频|