React系列 --- 從Mixin到HOC再到HOOKS(四)

React系列

React系列 --- 簡單模擬語法(一)
React系列 --- Jsx, 合成事件與Refs(二)
React系列 --- virtualdom diff算法實現分析(三)
React系列 --- 從Mixin到HOC再到HOOKS(四)
React系列 --- createElement, ReactElement與Component部分源碼解析(五)
React系列 --- 從使用React瞭解Css的各類使用方案(六)
React系列 --- 從零構建狀態管理及Redux源碼解析(七)
React系列 --- 擴展狀態管理功能及Redux源碼解析(八)html

Mixins(已廢棄)

這是React初期提供的一種組合方案,經過引入一個公用組件,而後能夠應用公用組件的一些生命週期操做或者定義方法,達到抽離公用代碼提供不一樣模塊使用的目的.node

曾經的官方文檔demo以下react

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 <p>React has been running for {this.state.seconds} seconds.</p>;
  },
});

React.render(<TickTock />, document.getElementById('example'));

可是Mixins只能應用在createClass的建立方式,在後來的class寫法中已經被廢棄了.緣由在於:git

  1. mixin引入了隱式依賴關係
  2. 不一樣mixins之間可能會有前後順序甚至代碼衝突覆蓋的問題
  3. mixin代碼會致使滾雪球式的複雜性

詳細介紹mixin危害性文章可直接查閱Mixins Considered Harmfulgithub

高階組件(Higher-order component)

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

屬性代理(Props Proxy)

最基本的實現方式segmentfault

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

從代碼能夠看出屬性代理方式其實就是接受一個 WrappedComponent 組件做爲參數傳入,並返回一個繼承了 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 <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

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

  render() {
    return <div>PropsProxyHOC</div>;
  }
}

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;
            // 強行觸發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 <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

// 被獲取ref實例組件
class Main extends Component {
  render() {
    return <input type="text" {...this.props.fields('name')} />;
  }
}

export default PropsProxyHOC(Main);

demo代碼能夠參考這裏性能優化

獲取被繼承refs實例

由於這是一個被HOC包裝過的新組件,因此想要在HOC裏面獲取新組件的ref須要用些特殊方式,可是無論哪一種,都須要在組件掛載以後才能獲取到.而且不能在無狀態組件(函數類型組件)上使用 ref 屬性,由於無狀態組件沒有實例。

經過父元素傳遞方法獲取

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      const newProps = {};
      // 監聽到有對應方法才生成props實例
      typeof this.props.getInstance === 'function' && (newProps.ref = this.props.getInstance);
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

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

const HOCComponent = PropsProxyHOC(Main);

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

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

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

  render() {
    return <HOCComponent getInstance={this.getInstance.bind(this)} />;
  }
}

export default ParentComponent;

demo代碼能夠參考這裏

經過高階組件當中間層

相比較上一方式,須要在高階組件提供設置賦值函數,而且須要一個props屬性作標記

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

    //設置ref實例
    setWrappedInstance = ref => {
      this.wrappedInstance = ref;
    };

    render() {
      const newProps = {};
      // 監聽到有對應方法才賦值props實例
      this.props.withRef && (newProps.ref = this.setWrappedInstance);
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

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

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 <HOCComponent ref="child" withRef />;
  }
}

export default ParentComponent;

demo代碼能夠參考這裏

forwardRef

React.forwardRef 會建立一個React組件,這個組件可以將其接受的 ref 屬性轉發到其組件樹下的另外一個組件中。這種技術並不常見,但在如下兩種場景中特別有用:

  • 轉發 refs 到 DOM 組件
  • 在高階組件中轉發 refs
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

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

如下是對上述示例發生狀況的逐步解釋:

  1. 咱們經過調用 React.createRef 建立了一個 React ref 並將其賦值給 ref 變量。
  2. 咱們經過指定 ref 爲 JSX 屬性,將其向下傳遞給 <FancyButton ref={ref}>
  3. React 傳遞 ref 給 fowardRef 內函數 (props, ref) => ...,做爲其第二個參數。
  4. 咱們向下轉發該 ref 參數到 <button ref={ref}>,將其指定爲 JSX 屬性。
  5. 當 ref 掛載完成,ref.current 將指向 <button> DOM 節點。

劫持渲染

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

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

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

const HOCComponent = PropsProxyHOC(Main);

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

  render() {
    setTimeout(() => this.setState({ isLoading: false }), 2000);
    return <HOCComponent isLoading={this.state.isLoading} />;
  }
}

export default ParentComponent;

固然也能用於佈局上嵌套在其餘元素輸出
demo代碼能夠參考這裏

反向繼承(Inheritance Inversion)

最簡單的demo代碼

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

在這裏WrappedComponent成了被繼承的那一方,從而能夠在高階組件中獲取到傳遞組件的全部相關實例

獲取繼承組件實例

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 <div ref="child">Main</div>;
  }
}

export default InheritanceInversionHOC(Main);

demo代碼能夠參考這裏

修改props和劫持渲染

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

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

以 element 元素爲樣板克隆並返回新的 React 元素。返回元素的 props 是將新的 props 與原始元素的 props 淺層合併後的結果。新的子元素將取代現有的子元素,而來自原始元素的 key 和 ref 將被保留。
React.cloneElement() 幾乎等同於:

<element.type {...element.props} {...props}>{children}</element.type>

可是,這也保留了組件的 ref。這意味着當經過 ref 獲取子節點時,你將不會意外地從你祖先節點上竊取它。相同的 ref 將添加到克隆後的新元素中。

相比屬性繼承來講,反向繼承修改props會比較複雜一點

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 <div ref="child">Main</div>;
  }
}

export default InheritanceInversionHOC(Main);

demo代碼能夠參考這裏

爲何須要用到cloneElement方法?

由於render函數內其實是調用React.creatElement產生的React元素,儘管咱們能夠拿到這個方法可是沒法修改它.能夠用getOwnPropertyDescriptors查看它的配置項
圖片描述
因此用cloneElement建立新的元素替代

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

注意

靜態屬性失效

由於高階組件返回的已經不是原組件了,因此原組件的靜態屬性方法已經沒法獲取,除非你主動將它們拷貝到返回組件中

渲染機制

由於高階組件返回的是新組件,裏面的惟一標誌也會變化,因此不建議在render裏面也調用高階組件,這會致使其每次都從新卸載再渲染,即便它可能長得同樣.
因此建議高階組件都是無反作用的純函數,即相同輸入永遠都是相同輸出,不容許任何有可變因素.

嵌套過深

在原組件中若是包裹層級過多會產生相似回調地獄的煩惱,難以調試,可閱讀性糟糕

遵照規則

若是沒有規範狀況下,也可能形成代碼衝突覆蓋的局面

HOOKS

Hooks是React v16.7.0-alpha中加入的新特性。它可讓你在class之外使用state和其餘React特性。

Hooks是可讓你與React狀態以及函數式組件的生命週期特性「掛鉤」的函數。鉤子是爲了讓你拋棄類使用React的,因此它不能在類中運行,可是能夠用在純函數中,這就解決了一直以來可能由於須要用到生命週期或者react狀態的時候,你不得不將本來的純函數代碼整個替換成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 (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

等價於下面Class寫法

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

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>Click me</button>
      </div>
    );
  }
}

demo代碼能夠參考這裏
從上面能夠看出useState實際上就是在state裏聲明一個變量而且初始化了一個值並且提供一個能夠改變對應state的函數.由於在純函數中沒有this.state.count的這種用法,因此直接使用count替代
上面的count就是聲明的變量,setCount就是改變變量的方法.
須要注意的一點是useState和this.state有點不一樣,它[color=#ff4753]只有在組件第一次render纔會建立狀態,以後每次都只會返回當前的值[/color].

賦值初始值的時候若是須要通過某些邏輯處理才能獲得的話,能夠經過函數傳遞,例如

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

若是改變須要根據以前的數據變化,能夠經過函數接收舊數據,例如

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' }]);
}

或者經過組合對象一次合併多個數據

Effect Hook

執行有反作用的函數,你能夠把 useEffect Hooks 視做 componentDidMountcomponentDidUpdatecomponentWillUnmount 的結合,[color=#ff4753]useEffect 會在瀏覽器繪製後延遲執行,但會保證在任何新的渲染前執行[/color]。React 將在組件更新前刷新上一輪渲染的 effect。React 組件中的 side effects 大體能夠分爲兩種

不須要清理

有時咱們想要在 React 更新過 DOM 以後執行一些額外的操做。好比網絡請求、手動更新 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 (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>Click me</button>
      </div>
    );
  }
}

如上所示,若是放在render的話在掛載前也會觸發,可是爲了不這個問題咱們不得不在兩個生命週期寫一樣的代碼.可是若是咱們換成HOOKS的寫法

import { useState, useEffect } from 'react';

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

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

demo代碼能夠參考這裏

useEffect 作了什麼? 經過這個 Hook,React 知道你想要這個組件在每次 render 以後作些事情。React 會記錄下你傳給 useEffect 的這個方法,而後在進行了 DOM 更新以後調用這個方法。但咱們一樣也能夠進行數據獲取或是調用其它必要的 API。

爲何 useEffect 在組件內部調用?useEffect 放在一個組件內部,可讓咱們在 effect 中,便可得到對 count state(或其它 props)的訪問,而不是使用一個特殊的 API 去獲取它。

useEffect 是否是在每次 render 以後都會調用? 默認狀況下,它會在第一次 render 以後的每次 update 後運行。React 保證每次運行 effects 以前 DOM 已經更新了。

使用上還有哪些區別? 不像 componentDidMount 或者 componentDidUpdateuseEffect 中使用的 effect 並不會阻滯瀏覽器渲染頁面。咱們也提供了一個單獨的 useLayoutEffect來達成這同步調用的效果。它的 API 和 useEffect 是相同的。

須要清理的 Effect

比較常見的就相似掛載的時候監聽事件或者開啓定時器,卸載的時候就移除.

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 <button>click me!</button>;
  }
}

換成HOOKS寫法相似,只是會返回新的函數

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

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

  return <button>click me!</button>;
}

demo代碼能夠參考這裏

咱們爲何在 effect 中返回一個函數 這是一種可選的清理機制。每一個 effect 均可以返回一個用來在晚些時候清理它的函數。這讓咱們讓添加和移除訂閱的邏輯彼此靠近。它們是同一個 effect 的一部分!

React 究竟在何時清理 effect? React 在每次組件 unmount 的時候執行清理。然而,正如咱們以前瞭解的那樣,effect 會在每次 render 時運行,而不是僅僅運行一次。這也就是爲何 React 會在執行下一個 effect 以前,上一個 effect 就已被清除。

咱們能夠修改一下代碼看看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 <button>click me! {count}</button>;
}

demo代碼能夠參考這裏
能夠看到上面代碼在每次更新都是從新監聽,想要避免這種狀況能夠往下繼續看.

進階使用

有時候咱們可能有多套邏輯寫在不一樣的生命週期裏,若是換成HOOKS寫法的話咱們能夠按功能劃分使用多個,React將會按照指定的順序應用每一個effect。

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 (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

demo代碼能夠參考這裏

爲何Effects會在每次更新後執行

若是大家之前使用class的話可能會有疑惑,爲何不是在卸載階段執行一次.從官網解釋代碼看

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

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

它在掛載階段監聽,移除階段移除監聽,每次觸發就根據this.props.friend.id作出對應處理.可是這裏有個隱藏的bug就是[color=#ff4753]當移除階段的時候獲取的this.props.friend.id多是舊的數據,引發的問題就是卸載時候會使用錯誤的id而致使內存泄漏或崩潰[/color],因此在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);
  };
});

這是由於[color=#ff4753]HOOKS會在應用下一個effects以前清除前一個effects[/color],此行爲默認狀況下確保一致性,並防止因爲缺乏更新邏輯而在類組件中常見的錯誤

經過跳過effects提高性能

就在上面咱們知道每次render都會觸發effects機制可能會有性能方面的問題,在class的寫法裏咱們能夠經過componentDidUpdate作選擇是否更新

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

而在useEffect裏咱們能夠經過傳遞一組數據給它做爲第二參數,若是在下次執行的時候該數據沒有發生變化的話React會跳過當次應用

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

注意

若是你想使用這種優化方式,請確保數組中包含了全部外部做用域中會發生變化且在 effect 中使用的變量,不然你的代碼會一直引用上一次render的舊數據.

若是你想要effects只在掛載和卸載時各清理一次的話,能夠傳遞一個空數組做爲第二參數.至關於告訴React你的effects不依賴於任何的props或者state,因此不必重複執行.

useCallback

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

返回一個 memoized 回調函數。
把內聯回調函數及依賴項數組做爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新。當你把回調函數傳遞給通過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將很是有用。
useCallback(fn, deps) 至關於 useMemo(() => fn, deps)。

useReducer

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

useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,並返回當前的 state 以及與其配套的 dispatch 方法。

在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於以前的 state 等。而且,使用 useReducer 還能給那些會觸發深更新的組件作性能優化,由於你能夠向子組件傳遞 dispatch 而不是回調函數 。

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 (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

demo代碼能夠參考這裏
從語法上大家會看到還有一個init的入參,是用來作惰性初始化,將 init 函數做爲 useReducer 的第三個參數傳入,這樣初始 state 將被設置爲 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 (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>Reset</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

demo代碼能夠參考這裏

useMemo

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

返回一個 memoized 值。
把「建立」函數和依賴項數組做爲參數傳入 useMemo,它僅會在某個依賴項改變時才從新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算。
記住,傳入 useMemo 的函數會在渲染期間執行。請不要在這個函數內部執行與渲染無關的操做,諸如反作用這類的操做屬於 useEffect 的適用範疇,而不是 useMemo。
若是沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值。

useRef

const refContainer = useRef(initialValue);

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變。

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已掛載到 DOM 上的文本輸入元素
    inputEl.current.focus();
  };
  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
}

demo代碼能夠參考這裏
本質上,useRef 就像是能夠在其 .current 屬性中保存一個可變值的「盒子」。
你應該熟悉 ref 這一種訪問 DOM 的主要方式。若是你將 ref 對象以 <div ref={myRef} /> 形式傳入組件,則不管該節點如何改變,React 都會將 ref 對象的 .current 屬性設置爲相應的 DOM 節點。
然而,useRef() 比 ref 屬性更有用。它能夠很方便地保存任何可變值,其相似於在 class 中使用實例字段的方式。
這是由於它建立的是一個普通 Javascript 對象。而 useRef() 和自建一個 {current: ...} 對象的惟一區別是,useRef 會在每次渲染時返回同一個 ref 對象。
請記住,當 ref 對象內容發生變化時,useRef 並不會通知你。變動 .current 屬性不會引起組件從新渲染。若是想要在 React 綁定或解綁 DOM 節點的 ref 時運行某些代碼,則須要使用回調 ref 來實現。

HOOKS規範

在頂層調用HOOKS

不要在循環,條件,或者內嵌函數中調用.這都是爲了保證你的代碼在每次組件render的時候會按照相同的順序執行HOOKS,而這也是可以讓React在多個useState和useEffect執行中正確保存數據的緣由

只在React函數調用HOOKS

  • React函數組件調用
  • 從自定義HOOKS中調用

能夠確保你源碼中組件的全部有狀態邏輯都是清晰可見的.

自定義HOOKS

咱們能夠將相關邏輯抽取出來

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」開頭爲自定義鉤子命名嗎? 這項公約很是重要。若是沒有它,咱們就不能自動檢查鉤子是否違反了規則,由於咱們沒法判斷某個函數是否包含對鉤子的調用。

使用相同鉤子的兩個組件是否共享狀態? 不。自定義鉤子是一種重用有狀態邏輯的機制(例如設置訂閱並記住當前值),可是每次使用自定義鉤子時,其中的全部狀態和效果都是徹底隔離的。

自定義鉤子如何得到隔離狀態? 對鉤子的每一個調用都處於隔離狀態。從React的角度來看,咱們的組件只調用useStateuseEffect

問題

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

一般,render props 和高階組件只渲染一個子節點。咱們認爲讓 Hook 來服務這個使用場景更加簡單。這兩種模式仍有用武之地,(例如,一個虛擬滾動條組件或許會有一個 renderItem 屬性,或是一個可見的容器組件或許會有它本身的 DOM 結構)。但在大部分場景下,Hook 足夠了,而且可以幫助減小嵌套。

生命週期方法要如何對應到 Hook?

  • constructor:函數組件不須要構造函數。你能夠經過調用 useState 來初始化 state。若是計算的代價比較昂貴,你能夠傳一個函數給 useState。
  • getDerivedStateFromProps:改成在渲染時安排一次更新。
  • shouldComponentUpdate:詳見 React.memo.
  • render:這是函數組件體自己。
  • componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 能夠表達全部這些的組合。
  • componentDidCatch and getDerivedStateFromError:目前尚未這些方法的 Hook 等價寫法,但很快會加上。

我能夠只在更新時運行 effect 嗎?

這是個比較罕見的使用場景。若是你須要的話,你能夠 使用一個可變的 ref 手動存儲一個布爾值來表示是首次渲染仍是後續渲染,而後在你的 effect 中檢查這個標識。

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

目前,你能夠經過ref來手動實現:

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return (
    <h1>
      Now: {count}, before: {prevCount}
    </h1>
  );
}

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

有相似 forceUpdate 的東西嗎?

若是先後兩次的值相同,useState 和 useReducer Hook 都會放棄更新。原地修改 state 並調用 setState 不會引發從新渲染。
一般,你不該該在 React 中修改本地 state。然而,做爲一條出路,你能夠用一個增加的計數器來在 state 沒變的時候依然強制一次從新渲染:

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

function handleClick() {
  forceUpdate();
}

我該如何測量 DOM 節點?

要想測量一個 DOM 節點的位置或是尺寸,你可使用 callback ref。每當 ref 被附加到另外一個節點,React 就會調用 callback。

function MeasureExample() {
  const [rect, ref] = useClientRect();
  return (
    <div>
      <h1 ref={ref}>Hello, world</h1>
      {rect !== null && <h2>The above header is {Math.round(rect.height)}px tall</h2>}
    </div>
  );
}

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

demo代碼能夠參考這裏
使用 callback ref 能夠確保 即使子組件延遲顯示被測量的節點 (好比爲了響應一次點擊),咱們依然可以在父組件接收到相關的信息,以便更新測量結果。

注意到咱們傳遞了 [] 做爲 useCallback 的依賴列表。這確保了 ref callback 不會在再次渲染時改變,所以 React 不會在非必要的時候調用它。

我該如何實現 shouldComponentUpdate?

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

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

React.memo 等效於 PureComponent,但它只比較 props。(你也能夠經過第二個參數指定一個自定義的比較函數來比較新舊 props。若是函數返回 true,就會跳過更新。)

React.memo 不比較 state,由於沒有單一的 state 對象可供比較。但你也可讓子節點變爲純組件,或者 用useMemo優化每個具體的子節點。

如何惰性建立昂貴的對象?

第一個常見的使用場景是當建立初始 state 很昂貴時,爲避免從新建立被忽略的初始 state,咱們能夠傳一個函數給 useState,React 只會在首次渲染時調用這個函數

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

你或許也會偶爾想要避免從新建立 useRef() 的初始值。useRef 不會像 useState 那樣接受一個特殊的函數重載。相反,你能夠編寫你本身的函數來建立並將其設爲惰性的:

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

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

  // 當你須要時,調用 getObserver()
  // ...
}

Hook 會由於在渲染時建立函數而變慢嗎?

不會。在現代瀏覽器中,閉包和類的原始性能只有在極端場景下才會有明顯的差異。
除此以外,能夠認爲 Hook 的設計在某些方面更加高效:

  • Hook 避免了 class 須要的額外開支,像是建立類實例和在構造函數中綁定事件處理器的成本。
  • 符合語言習慣的代碼在使用 Hook 時不須要很深的組件樹嵌套。這個現象在使用高階組件、render props、和 context 的代碼庫中很是廣泛。組件樹小了,React 的工做量也隨之減小。

傳統上認爲,在 React 中使用內聯函數對性能的影響,與每次渲染都傳遞新的回調會如何破壞子組件的 shouldComponentUpdate 優化有關。Hook 從三個方面解決了這個問題。

  • useCallback Hook 容許你在從新渲染之間保持對相同的回調引用以使得 shouldComponentUpdate 繼續工做:
  • useMemo Hook 使控制具體子節點什麼時候更新變得更容易,減小了對純組件的須要。
  • 最後,useReducer Hook 減小了對深層傳遞迴調的須要,就以下面解釋的那樣。

如何避免向下傳遞迴調?

在大型的組件樹中,咱們推薦的替代方案是經過 contextuseReducer 往下傳一個 dispatch 函數:

const TodosDispatch = React.createContext(null);

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

  return (
    <TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}

TodosApp 內部組件樹裏的任何子節點均可以使用 dispatch 函數來向上傳遞 actions

function DeepChild(props) {
  // 若是咱們想要執行一個 action,咱們能夠從 context 中獲取 dispatch。
  const dispatch = useContext(TodosDispatch);

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

  return <button onClick={handleClick}>Add todo</button>;
}

總而言之,從維護的角度來這樣看更加方便(不用不斷轉發回調),同時也避免了回調的問題。像這樣向下傳遞 dispatch 是處理深度更新的推薦模式。

React 是如何把對 Hook 的調用和組件聯繫起來的?

React 保持對當先渲染中的組件的追蹤。多虧了 Hook 規範,咱們得知 Hook 只會在 React 組件中被調用(或自定義 Hook —— 一樣只會在 React 組件中被調用)。每一個組件內部都有一個「記憶單元格」列表。它們只不過是咱們用來存儲一些數據的 JavaScript 對象。當你用 useState() 調用一個 Hook 的時候,它會讀取當前的單元格(或在首次渲染時將其初始化),而後把指針移動到下一個。這就是多個 useState() 調用會獲得各自獨立的本地 state 的緣由。

相關文章
相關標籤/搜索