React: 研究Flux设计模式

一、简介

一般来说,State管理在React中是一种最常用的实现机制,使用这种state管理系统基本可以开发各种需求的应用程序。然而,随着应用程序规模的不断扩张,原有的这种State管理系统就会暴露出臃肿的弊端,state中大量的数据放在根组件,而且与UI联系紧密,明显会增加系统的维护成本。此时,最明智的做法就是将State数据和自身的层级进行隔离,独立于UI之外。在React外部管理State,可以大量减少类组件的使用,如果不是特别需要生命周期函数,进而转用无状态函数组件,将类的功能与HOC隔离,从而确保组件只包含无状态的UI。由于无状态函数组件是纯函数,所以构架的函数式应用程序非常容易测试。基于这种理念,Facebook团队开发了一种新的设计模式Flux。Flux设计的目的就是保持数据单向流动,它囊括四个组成部分,分别是Store、Action、Dispatcher、View,单向联系,职责不同。在Flux中,应用程序的State数据就是存放到React之外的Store中进行管理的。具体的就是说,Store保存或者修改数据,是唯一可以更新视图的入口;Action则是封装用户的操作指令和需要更新的目标数据;Dispatcher的用途是将接收的Action排队并逐一分发到相应的Store中;View就是视图层了,根据Store中State数据进行更新。注意Action和State一样,都是不可变的数据,Action来源可以是View,也可以是其他源地址,一般是Web服务器。整个数据单向流程图如下所示:

 

二、View

在Flux中一般用无状态函数式组件表示,Flux会管理应用的State,所以除非特别需要用到生命周期函数,否则不推荐使用类组件。示例如下:

//使用函数式组件创建一个倒计时组件
//倒计时应用的View会将计数作为属性获取。它还会接收一对函数:tick和reset,这对函数定义在下面的Action中。
//当View渲染它之后会显示倒计时,除非值为0,否则会显示点击文案。如果计数值不是0,那么超时函数一秒后执行tick函数。
//当计数值为0时,View不会被任何Action生成器触发,除非用户点击了调起重置reset函数,再次进入倒计时。
const TimeCountDown = ({count, tick, reset}) => {
    if (count){
        setTimeout(() => tick(), 1000);
    }
    return (count) ?
        <h1>{count}</h1> :
        <div onClick={() => reset(10)}>
            <span>(click to start over)</span>
        </div>
};

 

三、Action

Action提供的指令和数据主要是Store用来修改State的。Action生成器就是函数,主要用来构造某个Action的具体细节。Action本身是由若干对象构成,并且至少包含一个类型字段用来区分。Action类型一般通过一个大写字母组成的字符串定义type类型。Action也可能打包了任何Store所需的数据,示例如下:

//当倒计时Action生成器被载入时,dispatcher会作为一个参数传递给它。每次某个TICK或者RESET函数被调用时,
//dispatcher的handleAction方法也会被调用,以便"调度"Action对象。
const timeCountDownActions = dispatcher =>
    ({
        tick() {
            dispatcher.handleAction({type: 'TICK'})
        },
        reset(count){
            dispatcher.handleAction({
                type: 'RESET',
                count
            })
        }
    });

 

四、Dispatcher

Dispatcher在应用程序中一直就只有一个存在。它表示设计模式中的空中管理中心。Dispatcher接收到Action,将与之有关的某些生成源信息一并打包,然后将它发送到相应的Store或者一系列Store中,以便处理这个Action。Dispatcher分派器用于将有效负载广播到已注册的回调。 这与通用的pub-sub系统有两个不同之处:(1)回调未订阅特定事件。 每个有效负载都分派给每个已注册的回调。(2)回调可以全部或部分推迟,直到执行了其他回调为止。 

API基本使用如下:

//例如,考虑以下假设的飞行目的地表格,当选择一个国家时,该表格将选择默认城市:
var flightDispatcher = new Dispatcher();

//跟踪选择哪个国家
var CountryStore = {country: null};

//跟踪选择哪个城市
var CityStore = {city: null};

//跟踪选定城市的基本航班价格
var FlightPriceStore = {price: null}

//注册更改所选城市这个有效负载:
flightDispatcher.register(function(payload) {
      if (payload.actionType === 'city-update') {
        CityStore.city = payload.selectedCity;
     }
});

//当用户更改所选城市时,我们将分派有效载荷:
flightDispatcher.dispatch({
      actionType: 'city-update',
      selectedCity: 'paris'
}); 

详细API,类文件如下:

/**
 * Copyright (c) 2014-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule Dispatcher
 * @flow
 * @preventMunge
 */

'use strict';

var invariant = require('invariant');

export type DispatchToken = string;

var _prefix = 'ID_';

/**
 * Dispatcher is used to broadcast payloads to registered callbacks. This is
 * different from generic pub-sub systems in two ways:
 *
 *   1) Callbacks are not subscribed to particular events. Every payload is
 *      dispatched to every registered callback.
 *   2) Callbacks can be deferred in whole or part until other callbacks have
 *      been executed.
 *
 * For example, consider this hypothetical flight destination form, which
 * selects a default city when a country is selected:
 *
 *   var flightDispatcher = new Dispatcher();
 *
 *   // Keeps track of which country is selected
 *   var CountryStore = {country: null};
 *
 *   // Keeps track of which city is selected
 *   var CityStore = {city: null};
 *
 *   // Keeps track of the base flight price of the selected city
 *   var FlightPriceStore = {price: null}
 *
 * When a user changes the selected city, we dispatch the payload:
 *
 *   flightDispatcher.dispatch({
 *     actionType: 'city-update',
 *     selectedCity: 'paris'
 *   });
 *
 * This payload is digested by `CityStore`:
 *
 *   flightDispatcher.register(function(payload) {
 *     if (payload.actionType === 'city-update') {
 *       CityStore.city = payload.selectedCity;
 *     }
 *   });
 *
 * When the user selects a country, we dispatch the payload:
 *
 *   flightDispatcher.dispatch({
 *     actionType: 'country-update',
 *     selectedCountry: 'australia'
 *   });
 *
 * This payload is digested by both stores:
 *
 *   CountryStore.dispatchToken = flightDispatcher.register(function(payload) {
 *     if (payload.actionType === 'country-update') {
 *       CountryStore.country = payload.selectedCountry;
 *     }
 *   });
 *
 * When the callback to update `CountryStore` is registered, we save a reference
 * to the returned token. Using this token with `waitFor()`, we can guarantee
 * that `CountryStore` is updated before the callback that updates `CityStore`
 * needs to query its data.
 *
 *   CityStore.dispatchToken = flightDispatcher.register(function(payload) {
 *     if (payload.actionType === 'country-update') {
 *       // `CountryStore.country` may not be updated.
 *       flightDispatcher.waitFor([CountryStore.dispatchToken]);
 *       // `CountryStore.country` is now guaranteed to be updated.
 *
 *       // Select the default city for the new country
 *       CityStore.city = getDefaultCityForCountry(CountryStore.country);
 *     }
 *   });
 *
 * The usage of `waitFor()` can be chained, for example:
 *
 *   FlightPriceStore.dispatchToken =
 *     flightDispatcher.register(function(payload) {
 *       switch (payload.actionType) {
 *         case 'country-update':
 *         case 'city-update':
 *           flightDispatcher.waitFor([CityStore.dispatchToken]);
 *           FlightPriceStore.price =
 *             getFlightPriceStore(CountryStore.country, CityStore.city);
 *           break;
 *     }
 *   });
 *
 * The `country-update` payload will be guaranteed to invoke the stores'
 * registered callbacks in order: `CountryStore`, `CityStore`, then
 * `FlightPriceStore`.
 */
class Dispatcher<TPayload> {
  _callbacks: {[key: DispatchToken]: (payload: TPayload) => void};
  _isDispatching: boolean;
  _isHandled: {[key: DispatchToken]: boolean};
  _isPending: {[key: DispatchToken]: boolean};
  _lastID: number;
  _pendingPayload: TPayload;

  constructor() {
    this._callbacks = {};
    this._isDispatching = false;
    this._isHandled = {};
    this._isPending = {};
    this._lastID = 1;
  }

  /**
   * Registers a callback to be invoked with every dispatched payload. Returns
   * a token that can be used with `waitFor()`.
   */
  register(callback: (payload: TPayload) => void): DispatchToken {
    var id = _prefix + this._lastID++;
    this._callbacks[id] = callback;
    return id;
  }

  /**
   * Removes a callback based on its token.
   */
  unregister(id: DispatchToken): void {
    invariant(
      this._callbacks[id],
      'Dispatcher.unregister(...): `%s` does not map to a registered callback.',
      id
    );
    delete this._callbacks[id];
  }

  /**
   * Waits for the callbacks specified to be invoked before continuing execution
   * of the current callback. This method should only be used by a callback in
   * response to a dispatched payload.
   */
  waitFor(ids: Array<DispatchToken>): void {
    invariant(
      this._isDispatching,
      'Dispatcher.waitFor(...): Must be invoked while dispatching.'
    );
    for (var ii = 0; ii < ids.length; ii++) {
      var id = ids[ii];
      if (this._isPending[id]) {
        invariant(
          this._isHandled[id],
          'Dispatcher.waitFor(...): Circular dependency detected while ' +
          'waiting for `%s`.',
          id
        );
        continue;
      }
      invariant(
        this._callbacks[id],
        'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
        id
      );
      this._invokeCallback(id);
    }
  }

  /**
   * Dispatches a payload to all registered callbacks.
   */
  dispatch(payload: TPayload): void {
    invariant(
      !this._isDispatching,
      'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'
    );
    this._startDispatching(payload);
    try {
      for (var id in this._callbacks) {
        if (this._isPending[id]) {
          continue;
        }
        this._invokeCallback(id);
      }
    } finally {
      this._stopDispatching();
    }
  }

  /**
   * Is this Dispatcher currently dispatching.
   */
  isDispatching(): boolean {
    return this._isDispatching;
  }

  /**
   * Call the callback stored with the given id. Also do some internal
   * bookkeeping.
   *
   * @internal
   */
  _invokeCallback(id: DispatchToken): void {
    this._isPending[id] = true;
    this._callbacks[id](this._pendingPayload);
    this._isHandled[id] = true;
  }

  /**
   * Set up bookkeeping needed when dispatching.
   *
   * @internal
   */
  _startDispatching(payload: TPayload): void {
    for (var id in this._callbacks) {
      this._isPending[id] = false;
      this._isHandled[id] = false;
    }
    this._pendingPayload = payload;
    this._isDispatching = true;
  }

  /**
   * Clear bookkeeping used for dispatching.
   *
   * @internal
   */
  _stopDispatching(): void {
    delete this._pendingPayload;
    this._isDispatching = false;
  }
}

module.exports = Dispatcher;
View Code

可以继承,示例如下:

import { Dispatcher } from 'flux';

//当handleViewAction被某一个action触发时,它会和该Action起始位置的某些数据一起被分发。
//当某一个Store被创建后,她就会被Dispatcher登记注册并开始监听相关的Action。
//当某个Action被分发后,它会按照一定的次序被处理接收,然后发送到相应的Store中
class TimeCountDownDispatcher extends Dispatcher{
    handleAction(action){
        console.log("dispatching actions:",action);
        this.dispatch({
            source: 'VIEW_ACTION',
            action
        })
    }
}

 

五、Store

Store主要用来存放应用程序逻辑和State数据的若干对象。当前的State数据可以通过访问Store的属性获取。某个Store需要修改State数据的所有操作指令都是由Action提供的。Store将会按照类别处理Action,并修改相关的数据。一旦数据发生了修改,该Store将会发出一个事件通知任何订阅了该Store的View,它们的数据发生了变化。首先介绍EventEmitter的API如下:

//首先安装events包
npm install events --save

EventEmitter是Facebook开发的一个开源的类event.js,完整代码为:

// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';

var R = typeof Reflect === 'object' ? Reflect : null
var ReflectApply = R && typeof R.apply === 'function'
  ? R.apply
  : function ReflectApply(target, receiver, args) {
    return Function.prototype.apply.call(target, receiver, args);
  }

var ReflectOwnKeys
if (R && typeof R.ownKeys === 'function') {
  ReflectOwnKeys = R.ownKeys
} else if (Object.getOwnPropertySymbols) {
  ReflectOwnKeys = function ReflectOwnKeys(target) {
    return Object.getOwnPropertyNames(target)
      .concat(Object.getOwnPropertySymbols(target));
  };
} else {
  ReflectOwnKeys = function ReflectOwnKeys(target) {
    return Object.getOwnPropertyNames(target);
  };
}

function ProcessEmitWarning(warning) {
  if (console && console.warn) console.warn(warning);
}

var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
  return value !== value;
}

function EventEmitter() {
  EventEmitter.init.call(this);
}
module.exports = EventEmitter;

// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter;

EventEmitter.prototype._events = undefined;
EventEmitter.prototype._eventsCount = 0;
EventEmitter.prototype._maxListeners = undefined;

// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
var defaultMaxListeners = 10;

Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
  enumerable: true,
  get: function() {
    return defaultMaxListeners;
  },
  set: function(arg) {
    if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
      throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
    }
    defaultMaxListeners = arg;
  }
});

EventEmitter.init = function() {

  if (this._events === undefined ||
      this._events === Object.getPrototypeOf(this)._events) {
    this._events = Object.create(null);
    this._eventsCount = 0;
  }

  this._maxListeners = this._maxListeners || undefined;
};

// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
  if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
    throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
  }
  this._maxListeners = n;
  return this;
};

function $getMaxListeners(that) {
  if (that._maxListeners === undefined)
    return EventEmitter.defaultMaxListeners;
  return that._maxListeners;
}

EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
  return $getMaxListeners(this);
};

EventEmitter.prototype.emit = function emit(type) {
  var args = [];
  for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
  var doError = (type === 'error');

  var events = this._events;
  if (events !== undefined)
    doError = (doError && events.error === undefined);
  else if (!doError)
    return false;

  // If there is no 'error' event listener then throw.
  if (doError) {
    var er;
    if (args.length > 0)
      er = args[0];
    if (er instanceof Error) {
      // Note: The comments on the `throw` lines are intentional, they show
      // up in Node's output if this results in an unhandled exception.
      throw er; // Unhandled 'error' event
    }
    // At least give some kind of context to the user
    var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
    err.context = er;
    throw err; // Unhandled 'error' event
  }

  var handler = events[type];

  if (handler === undefined)
    return false;

  if (typeof handler === 'function') {
    ReflectApply(handler, this, args);
  } else {
    var len = handler.length;
    var listeners = arrayClone(handler, len);
    for (var i = 0; i < len; ++i)
      ReflectApply(listeners[i], this, args);
  }

  return true;
};

function _addListener(target, type, listener, prepend) {
  var m;
  var events;
  var existing;

  if (typeof listener !== 'function') {
    throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
  }

  events = target._events;
  if (events === undefined) {
    events = target._events = Object.create(null);
    target._eventsCount = 0;
  } else {
    // To avoid recursion in the case that type === "newListener"! Before
    // adding it to the listeners, first emit "newListener".
    if (events.newListener !== undefined) {
      target.emit('newListener', type,
                  listener.listener ? listener.listener : listener);

      // Re-assign `events` because a newListener handler could have caused the
      // this._events to be assigned to a new object
      events = target._events;
    }
    existing = events[type];
  }

  if (existing === undefined) {
    // Optimize the case of one listener. Don't need the extra array object.
    existing = events[type] = listener;
    ++target._eventsCount;
  } else {
    if (typeof existing === 'function') {
      // Adding the second element, need to change to array.
      existing = events[type] =
        prepend ? [listener, existing] : [existing, listener];
      // If we've already got an array, just append.
    } else if (prepend) {
      existing.unshift(listener);
    } else {
      existing.push(listener);
    }

    // Check for listener leak
    m = $getMaxListeners(target);
    if (m > 0 && existing.length > m && !existing.warned) {
      existing.warned = true;
      // No error code for this since it is a Warning
      // eslint-disable-next-line no-restricted-syntax
      var w = new Error('Possible EventEmitter memory leak detected. ' +
                          existing.length + ' ' + String(type) + ' listeners ' +
                          'added. Use emitter.setMaxListeners() to ' +
                          'increase limit');
      w.name = 'MaxListenersExceededWarning';
      w.emitter = target;
      w.type = type;
      w.count = existing.length;
      ProcessEmitWarning(w);
    }
  }

  return target;
}

EventEmitter.prototype.addListener = function addListener(type, listener) {
  return _addListener(this, type, listener, false);
};

EventEmitter.prototype.on = EventEmitter.prototype.addListener;

EventEmitter.prototype.prependListener =
    function prependListener(type, listener) {
      return _addListener(this, type, listener, true);
    };

function onceWrapper() {
  var args = [];
  for (var i = 0; i < arguments.length; i++) args.push(arguments[i]);
  if (!this.fired) {
    this.target.removeListener(this.type, this.wrapFn);
    this.fired = true;
    ReflectApply(this.listener, this.target, args);
  }
}

function _onceWrap(target, type, listener) {
  var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
  var wrapped = onceWrapper.bind(state);
  wrapped.listener = listener;
  state.wrapFn = wrapped;
  return wrapped;
}

EventEmitter.prototype.once = function once(type, listener) {
  if (typeof listener !== 'function') {
    throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
  }
  this.on(type, _onceWrap(this, type, listener));
  return this;
};

EventEmitter.prototype.prependOnceListener =
    function prependOnceListener(type, listener) {
      if (typeof listener !== 'function') {
        throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
      }
      this.prependListener(type, _onceWrap(this, type, listener));
      return this;
    };

// Emits a 'removeListener' event if and only if the listener was removed.
EventEmitter.prototype.removeListener =
    function removeListener(type, listener) {
      var list, events, position, i, originalListener;

      if (typeof listener !== 'function') {
        throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
      }

      events = this._events;
      if (events === undefined)
        return this;

      list = events[type];
      if (list === undefined)
        return this;

      if (list === listener || list.listener === listener) {
        if (--this._eventsCount === 0)
          this._events = Object.create(null);
        else {
          delete events[type];
          if (events.removeListener)
            this.emit('removeListener', type, list.listener || listener);
        }
      } else if (typeof list !== 'function') {
        position = -1;

        for (i = list.length - 1; i >= 0; i--) {
          if (list[i] === listener || list[i].listener === listener) {
            originalListener = list[i].listener;
            position = i;
            break;
          }
        }

        if (position < 0)
          return this;

        if (position === 0)
          list.shift();
        else {
          spliceOne(list, position);
        }

        if (list.length === 1)
          events[type] = list[0];

        if (events.removeListener !== undefined)
          this.emit('removeListener', type, originalListener || listener);
      }

      return this;
    };

EventEmitter.prototype.off = EventEmitter.prototype.removeListener;

EventEmitter.prototype.removeAllListeners =
    function removeAllListeners(type) {
      var listeners, events, i;

      events = this._events;
      if (events === undefined)
        return this;

      // not listening for removeListener, no need to emit
      if (events.removeListener === undefined) {
        if (arguments.length === 0) {
          this._events = Object.create(null);
          this._eventsCount = 0;
        } else if (events[type] !== undefined) {
          if (--this._eventsCount === 0)
            this._events = Object.create(null);
          else
            delete events[type];
        }
        return this;
      }

      // emit removeListener for all listeners on all events
      if (arguments.length === 0) {
        var keys = Object.keys(events);
        var key;
        for (i = 0; i < keys.length; ++i) {
          key = keys[i];
          if (key === 'removeListener') continue;
          this.removeAllListeners(key);
        }
        this.removeAllListeners('removeListener');
        this._events = Object.create(null);
        this._eventsCount = 0;
        return this;
      }

      listeners = events[type];

      if (typeof listeners === 'function') {
        this.removeListener(type, listeners);
      } else if (listeners !== undefined) {
        // LIFO order
        for (i = listeners.length - 1; i >= 0; i--) {
          this.removeListener(type, listeners[i]);
        }
      }

      return this;
    };

function _listeners(target, type, unwrap) {
  var events = target._events;

  if (events === undefined)
    return [];

  var evlistener = events[type];
  if (evlistener === undefined)
    return [];

  if (typeof evlistener === 'function')
    return unwrap ? [evlistener.listener || evlistener] : [evlistener];

  return unwrap ?
    unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
}

EventEmitter.prototype.listeners = function listeners(type) {
  return _listeners(this, type, true);
};

EventEmitter.prototype.rawListeners = function rawListeners(type) {
  return _listeners(this, type, false);
};

EventEmitter.listenerCount = function(emitter, type) {
  if (typeof emitter.listenerCount === 'function') {
    return emitter.listenerCount(type);
  } else {
    return listenerCount.call(emitter, type);
  }
};

EventEmitter.prototype.listenerCount = listenerCount;
function listenerCount(type) {
  var events = this._events;

  if (events !== undefined) {
    var evlistener = events[type];

    if (typeof evlistener === 'function') {
      return 1;
    } else if (evlistener !== undefined) {
      return evlistener.length;
    }
  }

  return 0;
}

EventEmitter.prototype.eventNames = function eventNames() {
  return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
};

function arrayClone(arr, n) {
  var copy = new Array(n);
  for (var i = 0; i < n; ++i)
    copy[i] = arr[i];
  return copy;
}

function spliceOne(list, index) {
  for (; index + 1 < list.length; index++)
    list[index] = list[index + 1];
  list.pop();
}

function unwrapListeners(arr) {
  var ret = new Array(arr.length);
  for (var i = 0; i < ret.length; ++i) {
    ret[i] = arr[i].listener || arr[i];
  }
  return ret;
}
View Code
//列举几个基本API如下:

//1、添加事件监听,一直监听
//type:事件名称
//listener:回调函数
EventEmitter.prototype.addListener = function addListener(type, listener) {};
EventEmitter.prototype.on = EventEmitter.prototype.addListener; //等同上面代码

//2、添加事件监听,只监听一次
EventEmitter.prototype.once = function once(type, listener){}

//3、移除事件监听
EventEmitter.prototype.removeListener = function removeListener(type, listener){}
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;//等同上面代码

//4、移除所有监听
EventEmitter.prototype.removeAllListeners = function removeAllListeners(type){}

//5、触发监听
EventEmitter.prototype.emit = function emit(type){}

可以继承,示例如下: 

import { EventEmitter } from 'events';

//
Store会保存倒计时应用程序的State,也即计数值。计数值可以通过一个只读属性访问。当Action被分发后,Store会使用它们来修改计数值。 //一个TICK Action会减少计数值。一个RESET Action会重置整个计数值,以及该Action引用的所有数据。 //一旦State发生改变,Store就会发起一个事件到任何正在监听的View export class TimeCountDownStore extends EventEmitter{ constructor(count=5, dispatcher){ super(); this._count = count; dispatcher.register( this.dispatch.bind(this) ) } get count(){ return this._count; } dispatch(payload){ const {type,count} = payload.action; switch (type) { case 'TICK': this._count = this._count - 1; this.emit("TICK"); //触发TICK事件 return true; case 'RESET': this._count = count; this.emit("RESET"); //触发RESET事件 return true; default: return true; } } }

 

六、整合

首先,创建了appDispatcher,然后使用appDispatcher生成Action生成器。最后,appDispatcher被注册到了Store中,并且Store将初始化的计数值设置为10。render函数用于渲染包含计数值的View,该计数值是通过参数进行传递的。同时还有Action生成器也作为属性被传递给了该View。最后,某些监听器被添加到了Store中,从而完成整个循环流程。当Store发起了一个TICK或者RESET,它会产生一个新的计数,因此需要马上在View中渲染。然后,初始View会根据Store中的计数值进行渲染。每次View发起一个TICK或者RESET时,该Action将会沿着循环节点发送,最终作为准备重现渲染的数据返回该View。

//将这些部分综合连接起来
const appDispatcher = new TimeCountDownDispatcher();
const actions = timeCountDownActions(appDispatcher);
const store = new TimeCountDownStore(10, appDispatcher);

const render = count => ReactDOM.render(
    <TimeCountDown count={count} {...actions} />,
    document.getElementById('root')
);

store.on('TICK', ()=>render(store.count));   //监听TICK事件
store.on('RESET', ()=>render(store.count));  //监听RESET事件
render(store.count);

 

七、演示

myTest.js

import React from 'react';
import {Dispatcher} from 'flux';
import {EventEmitter} from 'events';

//使用函数式组件创建一个倒计时组件
//倒计时应用的View会将计数作为属性获取。它还会接收一对函数:tick和reset。
//当View渲染它之后会显示倒计时,除非值为0,否则会显示点击文案。如果计数值不是0,那么超时函数一秒后执行tick函数。
//当计数值为0时,View不会被任何Action生成器触发,除非用户点击了调起重置reset函数,再次进入倒计时。
export const TimeCountDown = ({count, tick, reset}) => {

    if (count){
        setTimeout(() => tick(), 1000);
    }

    const divStyle = {
        width: 200,
        textAlign: "center",
        backgroundColor: "red",
        padding: 50,
        fontFamily: "sans-serif",
    };

    const textStyle = {
        color: "white",
        fontSize: 30,
        fontFamily: "sans-serif",
    };

    return (
        (count) ?
            <div style={divStyle}><h1 style={textStyle}>{count}</h1></div> :
            <div style={divStyle} onClick={() => reset(10)}>
                <h1 style={textStyle}>Click Restart</h1>
            </div>
    )
};


//当倒计时Action生成器被载入时,dispatcher会作为一个参数传递给它。每次某个TICK或者RESET函数被调用时,
//dispatcher的handleViewAction方法也会被调用,以便"调度"Action对象。
export const timeCountDownActions = dispatcher =>
    ({
        tick() {
            dispatcher.handleAction({type: 'TICK'})
        },
        reset(count){
            dispatcher.handleAction({
                type: 'RESET',
                count
            })
        }
    });

//当handleViewAction被某一个action触发时,它会和该Action起始位置的某些数据一起被分发。
//当某一个Store被创建后,她就会被Dispatcher登记注册并开始监听相关的Action。
//当某个Action被分发后,它会按照一定的次序被处理接收,然后发送到相应的Store中
export class TimeCountDownDispatcher extends Dispatcher{
    handleAction(action) {
        console.log("dispatching actions:", action);
        this.dispatch({
            source: 'VIEW_ACTION',
            action
        })
    }
}

//export default class App extends Component

//Store会保存倒计时应用程序的State,也即计数值。计数值可以通过一个只读属性访问。当Action被分发后,Store会使用它们来修改计数值。
//一个TICK Action会减少计数值。一个RESET Action会重置整个计数值,以及该Action引用的所有数据。
//一旦State发生改变,Store就会发起一个事件到任何正在监听的View
export class TimeCountDownStore extends EventEmitter{

    constructor(count=5, dispatcher){
        super();
        this._count = count;
        dispatcher.register(
            this.dispatch.bind(this)
        )
    }

    get count(){
        return this._count;
    }

    dispatch(payload){
        const {type,count} = payload.action;
        switch (type) {
            case 'TICK':
                this._count = this._count - 1;
                this.emit("TICK");  //触发TICK事件
                return true;
            case 'RESET':
                this._count = count;
                this.emit("RESET"); //触发RESET事件
                return true;
            default:
                return true;
        }
    }
}
View Code

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

import {TimeCountDown, timeCountDownActions, TimeCountDownDispatcher, TimeCountDownStore} from './myTest'

//将这些部分综合连接起来
const appDispatcher = new TimeCountDownDispatcher();
const actions = timeCountDownActions(appDispatcher);
const store = new TimeCountDownStore(10, appDispatcher);

const render = count => ReactDOM.render(
    <TimeCountDown count={count} {...actions} />,
    document.getElementById('root')
);

store.on("TICK", ()=>render(store.count));  //监听TICK事件
store.on("RESET", ()=>render(store.count)); //监听RESET事件

render(store.count);
View Code

结果如下:可以看到每一个action被分派执行

 

八、引用

实现Flux模式的方法有很多种。一些库基于这种设计模式的特定实现已经开源了。如下所示:

Flux(https://facebook.github.io/flux/)。该库包含一个Dispatcher的实现。

Reflux(https://github.com/reflux/reflux.js)。单向数据流的简化版实现,主要聚焦于Action、Store和View。

Flummox(http://acdlite.github.io/flummox)。一个Flux模式的具体实现,允许用户通过扩展JavaScript类来构建Flux模块。

Fluxible(http://fluxble.io)。一个由Yahoo创建的Flux框架,用于同构Flux应用。

Redux(http://redux.js.org)。一个类Flux库,用于函数取代对象来实现模块化。目前最受欢迎的Flux框架之一。

MobX(https://mobx.js.org/getting-started.html)。一个State管理库,使用观察检测来响应State中的变化。

 

posted @ 2019-12-19 14:53  XYQ全哥  阅读(621)  评论(0编辑  收藏  举报