どのように1つのコンポーネントの論理を多重化するかについて


前言
本稿では,ReactとVueの2つの主流のビジュアルライブラリの論理的組合せと多重化モードの歴史を簡単に検討した:最初のMixinsからHOC,さらにRender Props,最後に最新のHooksである.
*注意:この文書では、JSスクリプトファイルはすべてグローバルに導入されています.したがって、ESModulesインポートの書き方ではなく、const { createElement: h } = React;のようなオブジェクトの書き方を解くことができます.また、コメントの内容を読むことに注意してください!
全文は22560字で、読み終えるのに約45分かかります.
Mixins
オブジェクト向けmixin
mixinsは従来のオブジェクト向けプログラミングで非常に流行している論理多重モードであり、その本質は属性/方法のコピーであり、例えば以下の例である.
const eventMixin = {
  on(type, handler) {
    this.eventMap[type] = handler;
  },
  emit(type) {
    const evt = this.eventMap[type];
    if (typeof evt === 'function') {
      evt();
    }
  },
};

class Event {
  constructor() {
    this.eventMap = {};
  }
}

//  mixin         Event     
Object.assign(Event.prototype, eventMixin);

const evt = new Event();
evt.on('click', () => { console.warn('a'); });
// 1    click  
setTimeout(() => {
  evt.emit('click');
}, 1000);

Vueのmixin
Vueではmixinには、data、computed、mountedなどのライフサイクルフック関数など、すべてのコンポーネントインスタンスが入力できるオプションが含まれます.同じ名前の競合マージポリシーは、値をオブジェクトとするオプションがコンポーネントデータを優先し、同じ名前のライフサイクルフック関数が呼び出され、mixinのライフサイクルフック関数がコンポーネントの前に呼び出されます.
const mixin = {
  data() {
    return { message: 'a' };
  },
  computed: {
    msg() { return `msg-${this.message}`; }
  },
  mounted() {
    //                   ?
    console.warn(this.message, this.msg);
  },
};

new Vue({
  //         el   ?        el    ,     mounted         ,            ,    mounted  created  
  el: '#app',
  mixins: [mixin],
  data() {
    return { message: 'b' };
  },
  computed: {
    msg() { return `msg_${this.message}`; }
  },
  mounted() {
    // data  message    merge,       b; msg      ,    msg_b
    console.warn(this.message, this.msg);
  },
});

mixinの同名衝突合併戦略からも分かるように、コンポーネントにmixinを追加するには、コンポーネントに特殊な処理が必要であり、多くのmixinsを追加するとパフォーマンス損失が発生することは避けられない.
Reactのmixin
ReactではmixinはcreateClassメソッドに従って16バージョンで削除されましたが、15のバージョンを探してみることもできます.
//             ,React                ,               
const mixin = {
  // getInitialState() {
  //   return { message: 'a' }; 
  // },
  componentWillMount() {
    console.warn(this.state.message);
    this.setData();
  },
  // setData() {
  //   this.setState({ message: 'c' });
  // },
};

const { createElement: h } = React;
const App = React.createClass({
  mixins: [mixin],
  getInitialState() {
    return { message: 'b' }; 
  },
  componentWillMount() {
    //               Vue React    :                , mixin                  。
    console.warn(this.state.message);
    this.setData();
  },
  setData() {
    this.setState({ message: 'd' });
  },
  render() { return null; },
});

ReactDOM.render(h(App), document.getElementById('app'));

Mixinsの欠陥
  • 最初にMixinsは暗黙的な依存関係を導入し、特に複数のmixinが導入され、さらにはネストされたmixinが導入された場合、コンポーネント内の属性/メソッドのソースは非常に不明確である.
  • 次にMixinsは名前空間の衝突を引き起こす可能性があり、導入されたmixinはすべて同じ名前空間にあり、前のmixinが導入した属性/方法は後のmixinの同名属性/方法で上書きされ、これはサードパーティ製パッケージを参照した項目に特に友好的ではない
  • ネストMixinsは互いに依存して結合し、雪だるま式の複雑さをもたらし、コードメンテナンス
  • に不利である.
    はい、以上はmixinのすべての内容についてです.もしあなたが少し疲れたら、先に休んでみてください.後にはたくさんの内容があります.
    HOC
    高次関数
    まず、高次関数を理解し、ウィキペディアの概念を見てみましょう.
    数学とコンピュータ科学では、高次関数は少なくとも以下の条件を満たす関数である:1つ以上の関数を入力として受け入れ、1つの関数を出力する
    多くの関数式プログラミング言語で見つけられるmap関数は高次関数の一例である.パラメータとして関数fを受け入れ、リストを受け入れ、各要素にfを適用する関数を返します.関数式プログラミングでは,別の関数を返す高次関数をCurry化関数と呼ぶ.
    例を挙げます(私がタイプチェックをしていないことを無視してください):
    function sum(...args) {
      return args.reduce((a, c) => a + c);
    }
    
    const withAbs = fn => (...args) => fn.apply(null, args.map(Math.abs));
    //                           ,            ,           
    // function withAbs(fn) {
    //   return (...args) => {
    //     return fn.apply(null, args.map(Math.abs));
    //   };
    // }
    
    const sumAbs = withAbs(sum);
    console.warn(sumAbs(1, 2, 3));
    console.warn(sumAbs(1, -2));

    ReactのHOC
    上記の概念によると、高次コンポーネントはコンポーネント関数を受け入れ、コンポーネント関数のCurry化関数を出力するものであり、HOCの最も古典的な例は、コンポーネントにロード状態を包むことである.例えば、
    一部のロードが遅いリソースでは、コンポーネントは最初は標準的なLoading効果を示しますが、一定時間(例えば2秒)後には、「リソースが大きく、積極的にロードしています.少々お待ちください」という友好的なヒントになり、リソースのロードが完了してから具体的な内容を示します.
    const { createElement: h, Component: C } = React;
    
    // HOC            
    function Display({ loading, delayed, data }) {
      if (delayed) {
        return h('div', null, '    ,      ,   ');
      }
      if (loading) {
        return h('div', null, '    ');
      }
    
      return h('div', null, data);
    }
    //               ,         Curry    
    const A = withDelay()(Display);
    const B = withDelay()(Display);
    
    class App extends C {
      constructor(props) {
        super(props);
        this.state = {
          aLoading: true,
          bLoading: true,
          aData: null,
          bData: null,
        };
        this.handleFetchA = this.handleFetchA.bind(this);
        this.handleFetchB = this.handleFetchB.bind(this);
      }
      
      componentDidMount() {
        this.handleFetchA();
        this.handleFetchB();
      }
    
      handleFetchA() {
        this.setState({ aLoading: true });
        //   1     ,            
        setTimeout(() => {
          this.setState({ aLoading: false, aData: 'a' });
        }, 1000);
      }
    
      handleFetchB() {
        this.setState({ bLoading: true });
        //     7     ,    5          
        setTimeout(() => {
          this.setState({ bLoading: false, bData: 'b' });
        }, 7000);
      }
      
      render() {
        const {
          aLoading, bLoading, aData, bData,
        } = this.state;
        
        return h('article', null, [
          h(A, { loading: aLoading, data: aData }),
          h(B, { loading: bLoading, data: bData }),
          //      ,             
          h('button', { onClick: this.handleFetchB, disabled: bLoading }, 'click me'),
        ]);
      }
    }
    
    //   5          
    function withDelay(delay = 5000) {
      //               ?           
    }
    
    ReactDOM.render(h(App), document.getElementById('app'));

    大体こう書いてあります.
    function withDelay(delay = 5000) {
      return (ComponentIn) => {
        class ComponentOut extends C {
          constructor(props) {
            super(props);
            this.state = {
              timeoutId: null,
              delayed: false,
            };
            this.setDelayTimeout = this.setDelayTimeout.bind(this);
          }
    
          componentDidMount() {
            this.setDelayTimeout();
          }
    
          componentDidUpdate(prevProps) {
            //     /     ,       ,       
            if (this.props.loading !== prevProps.loading) {
              clearTimeout(this.state.timeoutId);
              this.setDelayTimeout();
            }
          }
    
          componentWillUnmount() {
            clearTimeout(this.state.timeoutId);
          }
    
          setDelayTimeout() {
            //      /        delayed
            if (this.state.delayed) {
              this.setState({ delayed: false });
            }
            //             
            if (this.props.loading) {
              const timeoutId = setTimeout(() => {
                this.setState({ delayed: true });
              }, delay);
              this.setState({ timeoutId });
            }
          }
          
          render() {
            const { delayed } = this.state;
            //   props
            return h(ComponentIn, { ...this.props, delayed });
          }
        }
        
        return ComponentOut;
      };
    }

    VueのHOC
    VueでHOCを実装する考え方も同じですが、Vueの入出力コンポーネントは関数やクラスではなく、template/renderオプションを含むJavaScriptオブジェクトです.
    const A = {
      template: '
    a
    ', }; const B = { render(h) { return h('div', null, 'b'); }, }; new Vue({ el: '#app', render(h) { // , template/render JavaScript return h('article', null, [h(A), h(B)]); }, // , // components: { A, B }, // template: ` //
    // // // // `, });

    このため、VueにおけるHOCの入力は、
    const Display = {
      //        ,                
      props: ['loading', 'data', 'delayed'],
      render(h) {
        if (this.delayed) {
          return h('div', null, '    ,      ');
        }
        if (this.loading) {
          return h('div', null, '    ');
        }
    
        return h('div', null, this.data);
      },
    };
    //            
    const A = withDelay()(Display);
    const B = withDelay()(Display);
    
    new Vue({
      el: '#app',
      data() {
        return {
          aLoading: true,
          bLoading: true,
          aData: null,
          bData: null,
        };
      },
      mounted() {
        this.handleFetchA();
        this.handleFetchB();
      },
      methods: {
        handleFetchA() {
          this.aLoading = true;
          //   1     ,            
          setTimeout(() => {
            this.aLoading = false;
            this.aData = 'a';
          }, 1000);
        },
    
        handleFetchB() {
          this.bLoading = true;
          //     7     ,    5          
          setTimeout(() => {
            this.bLoading = false;
            this.bData = 'b';
          }, 7000);
        },
      },
      render(h) {
        return h('article', null, [
          h(A, { props: { loading: this.aLoading, data: this.aData } }),
          h(B, { props: { loading: this.bLoading, data: this.bData } }),
          //      ,             
          h('button', {
            attrs: {
              disabled: this.bLoading,
            },
            on: {
              click: this.handleFetchB,
            },
          }, 'click me'),
        ]);
      },
    });
    withDelay関数も簡単に書けます.
    function withDelay(delay = 5000) {
      return (ComponentIn) => {
        return {
          //   ComponentIn ComponentOut props         `props: ComponentIn.props`   
          props: ['loading', 'data'],
          data() {
            return {
              delayed: false,
              timeoutId: null,
            };
          },
          watch: {
            //  watch  componentDidUpdate
            loading(val, oldVal) {
              //     /     ,       ,       
              if (oldVal !== undefined) {
                clearTimeout(this.timeoutId);
                this.setDelayTimeout();
              }
            },
          },
          mounted() {
            this.setDelayTimeout();
          },
          beforeDestroy() {
            clearTimeout(this.timeoutId);
          },
          methods: {
            setDelayTimeout() {
              //      /        delayed
              if (this.delayed) {
                this.delayed = false;
              }
              //             
              if (this.loading) {
                this.timeoutId = setTimeout(() => {
                  this.delayed = true;
                }, delay);
              }
            },
          },
          render(h) {
            const { delayed } = this;
            //   props
            return h(ComponentIn, {
              props: { ...this.$props, delayed },
            });
          },
        };
      };
    }

    ネストされたHOC
    ここではReactの書き方を例に挙げます.
    const { createElement: h, Component: C } = React;
    
    const withA = (ComponentIn) => {
      class ComponentOut extends C {
        renderA() {
          return h('p', { key: 'a' }, 'a');
        }
        render() {
          const { renderA } = this;
          return h(ComponentIn, { ...this.props, renderA });
        }
      }
    
      return ComponentOut;
    };
    
    const withB = (ComponentIn) => {
      class ComponentOut extends C {
        renderB() {
          return h('p', { key: 'b' }, 'b');
        }
        //  HOC      
        renderA() {
          return h('p', { key: 'c' }, 'c');
        }
        render() {
          const { renderB, renderA } = this;
          return h(ComponentIn, { ...this.props, renderB, renderA });
        }
      }
    
      return ComponentOut;
    };
    
    class App extends C {
      render() {
        const { renderA, renderB } = this.props;
        return h('article', null, [
          typeof renderA === 'function' && renderA(),
          'app',
          typeof renderB === 'function' && renderB(),
        ]);
      }
    }
    
    //    renderA      ? withA(withB(App)) ?
    const container = withB(withA(App));
    
    ReactDOM.render(h(container), document.getElementById('app'));

    したがって,HOCにとってpropsにもネーミング競合の問題があることは明らかである.同じように複数のHOCを導入してHOCをネストした場合でも,コンポーネント内のpropの属性/メソッドのソースは非常に不明確である.
    HOCの優位性と欠陥
    まず欠陥を言います.
  • まずMixinsと同様にHOCのpropsも暗黙的な依存関係を導入し,複数のHOCを導入し,さらにはネストされたHOCを導入した場合,コンポーネント内のpropの属性/方法源は非常に不明確である
  • .
  • 次HOCのpropsは、名前空間の競合を引き起こす可能性があり、propの同名のプロパティ/メソッドは、その後に実行されるHOCによって上書きされます.
  • HOCは、論理をカプセル化するために追加のコンポーネントインスタンスネストを必要とし、無駄なパフォーマンスオーバーヘッド
  • をもたらす.
    さらにメリット:
  • HOCは副作用のない純粋な関数であり,ネストされたHOCは相互に依存する相互結合しない
  • である.
  • 出力コンポーネントは入力コンポーネントと共有されていない状態であり、自身のsetStateを用いて出力コンポーネントの状態を直接修正することもできず、状態修正ソースが単一であることを保証している.

  • HOCがMixinsがもたらす問題をあまり解決していないことを知りたいかもしれませんが、なぜMixinsを使い続けないのですか?
    クラス/関数構文に基づいて定義されたコンポーネントは、mixinsのプロパティ/メソッドをコンポーネントにコピーするにはインスタンス化が必要であり、開発者はコンストラクション関数で自分でコピーできますが、クラスライブラリではこのようなmixinsオプションを提供するのは難しいためです.
    さて、以上がHOCについての本文のすべてです.HOCを使った注意事項/compose関数などの知識点は紹介されておらず、不慣れな読者はReactの公式文書を読むことができます.
    Render Props
    ReactのRender Props
    実際には、上記のネストされたHOCのセクションでRender Propsの使い方を見たことがあります.その本質は、レンダリング関数をサブコンポーネントに渡すことです.
    const { createElement: h, Component: C } = React;
    
    class Child extends C {
      render() {
        const { render } = this.props;
        return h('article', null, [
          h('header', null, 'header'),
          typeof render === 'function' && render(),
          h('footer', null, 'footer'),
        ]);
      }
    }
    
    class App extends C {
      constructor(props) {
        super(props);
        this.state = { loading: false };
      }
      
      componentDidMount() {
        this.setState({ loading: true });
        setTimeout(() => {
          this.setState({ loading: false });
        }, 1000);
      }
      renderA() { return h('p', null, 'a'); }
      renderB() { return h('p', null, 'b'); }
    
      render() {
        const render = this.state.loading ? this.renderA : this.renderB;
        //         render,                     
        return h(Child, { render });
      }
    }
    
    ReactDOM.render(h(App), document.getElementById('app'));

    Vueのslot
    VueにおけるRender Propsに対応する概念は、スロット(slots)またはRenderless Componentsと大まかに呼ばれる.
    const child = {
      template: `
        
    header
    footer
    `, // , : // render(h) { // return h('article', null, [ // h('header', null, 'header'), // // slot, default Vnode // this.$slots.default, // h('footer', null, 'footer'), // ]); // }, }; new Vue({ el: '#app', components: { child }, data() { return { loading: false }; }, mounted() { this.loading = true; setTimeout(() => { this.loading = false; }, 1000); }, template: `

    a

    b

    `, });

    Vueでは、$slotsを介してライブラリが自動的に渡されるレンダー関数を明示的に渡す必要はありません.
    紙面に限る、Vue 2.6版以前の書き方:slotslot-scopeここでは紹介しません.読者はVueの公式ドキュメントを読むことができます.ここではv-slotの書き方を紹介します.
    const child = {
      data() {
        return {
          obj: { name: 'obj' },
        };
      },
      // slot              ,  `v-slot:[name]="slotProps"`  ,  slotProps         ,                 
      template: `
        
    header
    footer
    `, }; new Vue({ el: '#app', components: { child }, data() { return { loading: false }; }, mounted() { this.loading = true; setTimeout(() => { this.loading = false; }, 1000); }, // #content v-slot:content template: ` `, });
    slotとは異なり、v-slot