Vue.jsを使用してリアクティブデータパイプラインを作成する

提供:Dev Guides
移動先:案内検索

Vueは多くのことを行うことができます。 もちろん、そのうちの1つは、アプリのビューレイヤーとして機能します。 実際、Vueの目的はそれだけだと思いますが、実際にはかなりきちんとした他のいくつかの目的を知っています。 今日は、ビューレイヤーにVueを使用することを避け、代わりにそれを使用して…計算されたプロパティの魔法を通してRxJSObservablesに似たリアクティブデータパイプラインを作成します。 うん。

さて、ここにはかなりのコードがあり、できる限り説明しようと思いますが、理解するのは少し難しいかもしれません。

私たちが目指す使用目的は次のようになります。

import { ReactivePipeline } from './reactive-pipeline';

const sourceArray = ['e', 'x', 'a', 'm', 'p', 'l', 'e'];

// Create a new pipeline.
new ReactivePipeline(sourceArray)
// Make all letters uppercase.
.pipe(array => array.map(letter => letter.toUpperCase()))
// Join the array into a string.
.pipe(array => array.join(''))
// Log any errors.
.error(e => {
  console.error(e)
})
// Start the pipeline and listen for changes.
.subscribe(result => {
  // Whenever the output changes, log it.
  console.log(result) // EXAMPLE
});

これで、元の配列が変更されるたびに、 .subscribe のコールバックは、パイプラインを介してその配列を実行した結果を出力します。 なめらかですね。 ああ、ブラウザ環境がなくても、Nodeでも完全に機能することを述べましたか?

クラスの作成

これに必要なのは、依存関係としてインストールされたVueだけです。 通常のrequire()を使用する場合、ノードの下で問題なく実行されます。

コードに関する最初のステップは、いくつかの関数を含む単純なクラスを作成することです。

react-pipeline.js

import Vue from 'vue';

export class ReactivePipeline {
  constructor (sourceData) {
    this._source = sourceData;
    this._tracker = null;
    this._transformers = [];
    this._subscribeHandler = function() {};
    this._errorHandler = function(e) { throw e };
  }

  pipe (transformer) {
    this._transformers.push(transformer);
    return this;
  }

  subscribe (callback) {
    this._subscribeHandler = callback;
    this.setupComponent();
    return this;
  }

  error (callback) {
    this._errorHandler = callback;
    return this;
  }

  setupComponent () {
    // ... We'll flesh this out next.
  }
}

それはかなり自明です。 実際に行っているのは、データを収集してsetupComponent()で使用するためにクラスに格納する一連の関数を作成することだけです。

さて、トリッキーな部分はsetupComponentで何が起こるかです。 実際、ここで達成しようとしていることは少し複雑です(単一のウォッチャーを使用し、計算されたプロパティは使用できません)が、このメソッドを使用すると、Vueの依存関係追跡システムのサポートを追加して、計算されたプロパティをキャッシュし、再実行しないようにすることができます依存関係が変更されたときのすべて。

react-pipeline.js

...
setupComponent () {
  // Get everything in this closure so we can access it from inside the computed handlers.
  const source = this._source;
  const transformers = this._transformers;
  const subscribeHandler = this._subscribeHandler;
  const errorHandler = this._errorHandler;

  const computed = {};

  // Populate computed properties object with transformer function wrappers.
  transformers.forEach((transformer, index) => {
    // Create a named computed property for each transformer.
    // These can't be arrow functions, as they need to be bound to the generated component.
    computed[`transformer_${index}`] = function() {
      try {
        // Run each transformer against the previous value in the chain.
        return transformer(index === 0 ? this.source : this[`transformer_${index - 1}`]);
      } catch (e) {
        // Handle any errors.
        errorHandler(e);
      }
    }
  })

  // Create an "output" computed property that simply serves as the last one in the chain.
  computed['output'] = function() {
    return this[`transformer_${transformers.length - 1}`];
  }

  // Here's where the magic happens.
  // Create a new Vue component with the source data in it's data property.
  // (This makes it observable.)
  const PipelineComponent = Vue.extend({
    data() {
      return {
        source: this._source
      }
    },

    // We need one watcher to "use" the final computed property and cause the chain to update.
    watch: {
      // I do realize we could've just put the entire transformer chain in here, but that would be boring.
      source () {
        subscribeHandler(this.output);
      }
    },

    computed,
  });

  // Now, initialize the component and start the transformation chain going.
  this._tracker = new PipelineComponent();

  return this;
}
...

これが完了すると、記事の冒頭で示した方法で使用できるようになります。

すべて一緒に今:

ReactivePipelineクラス…

react-pipeline.js

import Vue from 'vue';

export class ReactivePipeline {
  constructor (sourceData) {
    this._source = sourceData;
    this._tracker = null;
    this._transformers = [];
    this._subscribeHandler = function() {};
    this._errorHandler = function(e) { throw e };
  }

  pipe (transformer) {
    this._transformers.push(transformer);
    return this;
  }

  subscribe (callback) {
    this._subscribeHandler = callback;
    this.setupComponent();
    return this;
  }

  error (callback) {
    this._errorHandler = callback;
    return this;
  }

  setupComponent () {
    // Get everything in this closure so we can access it from inside the computed handlers.
    const source = this._source;
    const transformers = this._transformers;
    const subscribeHandler = this._subscribeHandler;
    const errorHandler = this._errorHandler;

    const computed = {};

    // Populate computed properties object with transformer function wrappers.
    transformers.forEach((transformer, index) => {
      // Create a named computed property for each transformer.
      // These can't be arrow functions, as they need to be bound to the generated component.
      computed[`transformer_${index}`] = function() {
        try {
          // Run each transformer against the previous value in the chain.
          return transformer(index === 0 ? this.source : this[`transformer_${index - 1}`]);
        } catch (e) {
          // Handle any errors.
          errorHandler(e);
        }
      }
    })

    // Create an "output" computed property that simply serves as the last one in the chain.
    computed['output'] = function() {
      return this[`transformer_${transformers.length - 1}`];
    }

    // Here's where the magic happens.
    // Create a new Vue component with the source data in it's data property.
    // (This makes it observable.)
    const PipelineComponent = Vue.extend({
      data() {
        return {
          source: this._source
        }
      },

      // We need one watcher to "use" the final computed property and cause the chain to update.
      watch: {
        // I do realize we could've just put the entire transformer chain in here, but that would be boring.
        source () {
          subscribeHandler(this.output);
        }
      },

      computed,
    });

    // Now, initialize the component and start the transformation chain going.
    this._tracker = new PipelineComponent();

    return this;
  }
}

…および使用法:

main.js

import { ReactivePipeline } from './reactive-pipeline';

const sourceArray = ['e', 'x', 'a', 'm', 'p', 'l', 'e'];

// Create a new pipeline.
new ReactivePipeline(sourceArray)
// Make all letters uppercase.
.pipe(array => array.map(letter => letter.toUpperCase()))
// Join the array into a string.
.pipe(array => array.join(''))
// Log any errors.
.error(e => {
  console.error(e)
})
// Start the pipeline and listen for changes.
.subscribe(result => {
  // Whenever the output changes, log it.
  console.log(result) // EXAMPLE
});

BOOM! 100 SLoC未満のVue.jsを使用したRxJSスタイルのリアクティブデータパイプライン!

擦り込むだけで、RxJSは〜140 kB 最小化され、Vueは約 60kBになります。 したがって、RxJSの半分以下のサイズでビューフレームワークおよび独自のカスタム監視可能システムを使用できます。 :D

謝辞

Anirudh Sanjeev の仕事に非常に感謝しています。最初に、Vueの計算されたプロパティの可能性に目を向け、想像力をアイデアに夢中にさせました。