你還沒有聽過React不完全手冊???

xpromise發表於2019-04-01

完全是不可能滴, 這輩子都不可能完全!        -- 來自某非著名碼農

本文總結 React 實用的特性,部分實驗性和不實用的功能將不會納入進來,或許未來可期~

1、setState

  • 面試題
    class App extends Component {
      state = {
        val: 0
      }
      // 震驚!隔壁老王也有失手的時候~
      componentDidMount() {
        this.setState({val: this.state.val + 1});
        console.log(this.state.val); // ?
        
        this.setState((prevState) => ({val: prevState.val + 1}));
        console.log(this.state.val); // ?
          
        setTimeout(() => {
          this.setState({val: this.state.val + 1});
          console.log(this.state.val); // ?
          
          this.setState({val: this.state.val + 1});
          console.log(this.state.val); // ?
        }, 1000)
      }
      
      render() {
        return <h2>App元件</h2>;
      }
    }
    複製程式碼
  • 總結
    • setState 只在 React 合成事件和鉤子函式中是“非同步”的,在原生DOM事件和定時器中都是同步的。
    • 如果需要獲取“非同步”場景的 setState 的值 --> this.setState(partial, callback) 在 callback 中拿到最新的值
    • 如果要在“非同步”場景保證同步更新多次 setState --> this.setState((prevState, props) => {return newState})
      • 能保證同步更新, 但是外面獲取的值還是之前的值

2、Fragment

  • before

    • 程式碼
      export default class App extends Component {
        render() {
          return (
            <div>
              <h2>App元件</h2>
              <p>這是App元件的內容</p>
            </div>
          );
        }
      }
      複製程式碼
    • 效果
      img
  • after

    • 程式碼
      export default class App extends Component {
        render() {
          return (
            <Fragment>
              <h2>App元件</h2>
              <p>這是App元件的內容</p>
            </Fragment>
          );
        }
      }
      複製程式碼
    • 效果
      img
  • 總結:使用 Fragment ,可以不用新增額外的DOM節點

3、React效能優化

  • shouldComponentUpdate

    // 舉個例子:
    shouldComponentUpdate(nextProps, nextState) {
      if (nextProps !== this.props) {
        return true;  // 允許更新
      }
      if (nextState !== this.state) {
        return true;
      }
      return false;  // 不允許更新
    }
    複製程式碼
  • PureComponent 元件

    • 使用
      // 實現了對 state 和 props 的淺比較
      // 相等就不更新,不相等才更新
      class App extends PureComponent {}
      複製程式碼
    • 淺比較原始碼
      // 實現 Object.is() 方法, 判斷x y是否完全相等
      function is(x, y) {
        // (x !== 0 || 1 / x === 1 / y) 用於判斷 0 和 -0 不相等
        // x !== x && y !== y	 用於判斷 NaN 等於 NaN
        return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y
        ;
      }
      
      // 提取了hasOwnProperty方法,快取
      var hasOwnProperty$1 = Object.prototype.hasOwnProperty;
      
      // 返回false為更新,true為不更新
      function shallowEqual(objA, objB) {
        // 如果A和B完全相等,返回true
        if (is(objA, objB)) {
          return true;
        }
        // 如果A和B不相等,並且不是物件,說明就是普通值,返回false
        if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
          return false;
        }
        // 提取A和B的所有屬性
        var keysA = Object.keys(objA);
        var keysB = Object.keys(objB);
        // 如果長度不相等,返回false
        if (keysA.length !== keysB.length) {
          return false;
        }
        
        // 檢測 A 的屬性 和 B 的屬性是否一樣
        for (var i = 0; i < keysA.length; i++) {
          if (!hasOwnProperty$1.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
            return false;
          }
        }
        
        return true;
      }
      複製程式碼
  • 問題:如果使用 pureComponent 只能進行淺比較,如果修改了原資料再更新,就會導致地址值一樣從而不會更新。但實際需要更新。

  • 解決:

    • 保證每次都是新的值
    • 使用 immutable-js 庫,這個庫保證生成的值都是唯一的
      var map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
      // 設定值
      var map2 = map1.set('a', 66);
      // 讀取值
      map1.get('a'); // 1
      map2.get('a'); // 66
      複製程式碼
  • 總結:使用以上方式,可以減少不必要的重複渲染。

4、React 高階元件

  • 基本使用
    // WrappedComponent 就是傳入的包裝元件
    function withHoc(WrappedComponent) {
      return class extends Component {
        render () {
          return <WrappedComponent />;
        }
      }
    }
    
    // 使用
    withHoc(App)
    複製程式碼
  • 向其中傳參
    function withHoc(params) {
      return (WrappedComponent) => {
        return class extends Component {
          render () {
            return <WrappedComponent />;
          }
        }
      }
    }
    
    // 使用
    withHoc('hello hoc')(App)
    複製程式碼
  • 接受props
    function withHoc(params) {
      return (WrappedComponent) => {
        return class extends Component {
          render () {
            // 將接受的 props 傳遞給包裝元件使用
            return <WrappedComponent {...this.props}/>;
          }
        }
      }
    }
    複製程式碼
  • 定義元件名稱
    function withHoc(params) {
      return (WrappedComponent) => {
        return class extends Component {
          // 定義靜態方法,修改元件在除錯工具中顯示的名稱
          static displayName = `Form(${getDisplayName(WrappedComponent)})`
          render () {
            return <WrappedComponent {...this.props}/>;
          }
        }
      }
    }
    // 封裝獲取包裝元件的 displayName 的方法
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    複製程式碼

原文連結

5、render props

原文太長,直接上 連結
官網真香, 建議大家將 React 官網過一遍~

6、React 懶載入

  • react-loadable
    import Loadable from 'react-loadable';
    import Loading from './components/loading'
    
    const LoadableComponent = Loadable({
      loader: () => import('./components/home'),
      loading: Loading,
    });
    
    export default class App extends Component {
      render() {
        return (
          <div>
            <h2>App元件</h2>
            <LoadableComponent />
          </div>
        );
      }
    }
    複製程式碼
  • Suspenselazy
    import React, {Component, Suspense, lazy} from 'react';
    import Loading from './components/loading';
    
    const LazyComponent = lazy(() => import('./components/home'));
    
    export default class App extends Component {
      render() {
        return (
          <div>
            <h2>App元件</h2>
            <Suspense fallback={<Loading />}>
              <LazyComponent />
            </Suspense>
          </div>
        );
      }
    }
    複製程式碼
  • 區別
    • react-loadable 是民間 --> 需要額外下載引入
    • Suspenselazy 是官方 --> 只需引入
    • react-loadable 支援伺服器渲染
    • Suspenselazy 不支援伺服器渲染
  • 總結:使用 create-react-app 會將其單獨提取成一個bundle輸出,從而資源可以懶載入和重複利用。

7、虛擬DOM diff演算法

  • 虛擬DOM diff演算法主要就是對以下三種場景進行優化:

  • tree diff

    • 對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。(因為 DOM 節點跨層級的移動操作少到可以忽略不計)
    • 如果父節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用於進一步的比較。
    • 注意:
      • React 官方建議不要進行 DOM 節點跨層級的操作,非常影響 React 效能。
      • 在開發元件時,保持穩定的 DOM 結構會有助於效能的提升。例如,可以通過 CSS 隱藏或顯示節點,而不是真的移除或新增 DOM 節點。
  • component diff

    • 如果是同一型別的元件,按照原策略繼續比較 virtual DOM tree(tree diff)。
      • 對於同一型別的元件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切的知道這點那可以節省大量的 diff 運算時間,因此 React 允許使用者通過 shouldComponentUpdate() 來判斷該元件是否需要進行 diff。
    • 如果不是,直接替換整個元件下的所有子節點。
  • element diff

    • 對處於同一層級的節點進行對比。
    • 這時 React 建議:新增唯一 key 進行區分。雖然只是小小的改動,效能上卻發生了翻天覆地的變化!
      • 如: A B C D --> B A D C
      • 新增 key 之前: 發現 B != A,則建立並插入 B 至新集合,刪除老集合 A;以此類推,建立並插入 A、D 和 C,刪除 B、C 和 D。
      • 新增 key 之後: B、D 不做任何操作,A、C 進行移動操作,即可。
    • 建議:在開發過程中,儘量減少類似將最後一個節點移動到列表首部的操作,當節點數量過大或更新操作過於頻繁時,在一定程度上會影響 React 的渲染效能。
  • 總結

    • React 通過制定大膽的 diff 策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
    • React 通過分層求異的策略,對 tree diff 進行演算法優化;
    • React 通過相同類生成相似樹形結構,不同類生成不同樹形結構的策略,對 component diff 進行演算法優化;
    • React 通過設定唯一 key的策略,對 element diff 進行演算法優化;
    • 建議,在開發元件時,保持穩定的 DOM 結構會有助於效能的提升;
    • 建議,在開發過程中,儘量減少類似將最後一個節點移動到列表首部的操作,當節點數量過大或更新操作過於頻繁時,在一定程度上會影響 React 的渲染效能。

原文連結

8、Fiber

  • Fiber 是為了解決 React 專案的效能問題和之前的一些痛點而誕生的。
  • Fiber 的核心流程可以分為兩個部分:
    • 可中斷的 render/reconciliation 通過構造 workInProgress tree 得出 change。
    • 不可中斷的 commit 應用這些 DOM change。
  • 非同步實現不同優先順序任務的協調執行:
    • requestIdleCallback: 線上程空閒時期排程執行低優先順序函式;
    • requestAnimationFrame: 在下一個動畫幀排程執行高優先順序函式;
  • 總結
    • 可切分,可中斷任務。
    • 可重用各分階段任務,且可以設定優先順序。
    • 可以在父子元件任務間前進/後退切換任務。
    • render方法可以返回多元素(即可以返回陣列)。
    • 支援異常邊界處理異常。

原文連結:
mp.weixin.qq.com/s/uDIknJ-We… juejin.im/post/5a2276…

9、Redux

  • 作用: 集中管理多個元件共享的狀態
  • 特點: 單一資料來源、純函式、只讀state
  • redux 核心模組定義:
    • store.js
      import { createStore, applyMiddleware } from 'redux';
      // 非同步actions使用的中介軟體
      import thunk from 'redux-thunk';
      // redux開發chrome除錯外掛
      import { composeWithDevTools } from 'redux-devtools-extension';
      
      import reducers from './reducers';
      
      export default createStore(reducers, composeWithDevTools(applyMiddleware(thunk)));
      複製程式碼
    • reducers.js
      import { combineReducers } from 'redux';
      import { TEST1, TEST2 } from './action-types';
      
      function a(prevState = 0, action) {
        switch (action.type) {
          case TEST1 :
            return action.data + 1;
          default :
            return prevState;
        }
      }
      
      function b(prevState = 0, action) {
        switch (action.type) {
          case TEST2 :
            return action.data + 1;
          default :
            return prevState;
        }
      }
      // 組合兩個reducer函式並暴露出去
      export default combineReducers({a, b});   
      複製程式碼
    • actions.js
      import { TEST1, TEST2 } from './action-types';
      // 同步action creator,返回值為action物件
      export const test1 = (data) => ({type: TEST1, data});
      export const test2 = (data) => ({type: TEST2, data});
      // 非同步action creator,返回值為函式
      export const test2Async = (data) => {
        return (dispatch) => {
          setTimeout(() => {
            dispatch(test2(data));
          }, 1000)
        }
      };
      複製程式碼
    • action-types.js
      export const TEST1 = 'test1';
      export const TEST2 = 'test2';
      複製程式碼
  • 元件內使用:
    • App.jsx
      import React, { Component } from 'react';
      import PropTypes from 'prop-types';
      import { connect } from 'react-redux';
      import {test1, test2Async} from './redux/actions';
      
      class App extends Component {
        static propTypes = {
          a: PropTypes.number.isRequired,
          b: PropTypes.number.isRequired,
          test1: PropTypes.func.isRequired,
          test2Async: PropTypes.func.isRequired,
        }
        
        componentDidMount() {
          const { a, b, test1, test2Async } = this.props;
          // 測試
          test1(a + 1);
          test2Async(b + 1);
        }
        
        render() {
          return (
            <div>App元件</div>
          );
        }
      }
      
      /* =============== redux相關程式碼 ================== */
      // 將狀態資料對映為屬性以props方式傳入元件
      const mapStateToProps = (state) => ({a: state.a, b: state.b});
      // 將操作狀態資料的方法對映為屬性以props方式傳入元件
      const mapDispatchToProps = (dispatch) => {
        return {
          test1(data) {
            dispatch(test1(data));
          },
          test2Async(data) {
            dispatch(test2Async(data));
          }
        }
      }
      // connect就是一個典型的HOC
      export default connect(mapStateToProps, mapDispatchToProps)(App);
      
      /*
      // 上面寫的太複雜了,但是好理解。而以下就是上面的簡寫方式
      export default connect(
        (state) => ({...state}),
        { test1, test2Async }
      )(App);
      */
      複製程式碼
    • index.js
      // 入口檔案的配置
      import React from 'react';
      import ReactDOM from 'react-dom';
      import { Provider } from 'react-redux';
      import App from './App';
      import store from './redux/store';
      
      ReactDOM.render(
        <Provider store={store}>
          <App />
        </Provider>, document.getElementById('root'));
      複製程式碼
  • 總結:
    • 我們會發現使用 Redux 會變得更加複雜,以及多了很多模板程式碼(例如: action creators)
    • 但是,這是 Redux 能幫助我們更好操作狀態,追蹤和除錯錯誤等。
    • 並且 Redux 有著一整套豐富的生態圈,這些你都能在 官方文件 找到答案
    • 總之,目前比起世面上 mobx 等庫,更適用於大型專案開發~

10、未來可期

其實還有很多技術沒有說,像 contextReact Hooks 等,但受限於筆者的眼界,目前沒有發現大規模使用的場景(如果有,請小夥伴們指正),所以就不談了~有興趣的小夥伴去找找看吧~

相關文章