flux数据流在rn中的使用

flux介绍

flux是Facebook提出的react开发单向数据流的模式,在实际开发过程做看过很多逻辑混乱,flux可以很大程度解决这一的问题不仅如此,这种设计模式在其他的项目结构中任然很有用。

网上很多文章都是在react中使用flux,放到react-native中各种出错,本文针对react-native。

flux主要分成4个模块

the dispatcher
处理动作分发,维护 Store 之间的依赖关系

the stores
数据和逻辑部分

the views
React 组件,这一层可以看作 controller-views,作为视图同时响应用户交互

the actions
提供给 dispatcher 传递数据给 store

数据的流向 Action -> Dispatcher -> Store -> View

flux的详细概念可以参考这两篇文章

上面的介绍很详细,我就不重复造轮子了,但是有个问题,上面的都是基于react的demo,在react-native中会出现很多问题,主要问题是?

demo

controller层

controller.js 页面, 与view,actions,store 交互,controller有几个作用

  • 视图展示,渲染
  • 发出视图交互事件,但不处理交互事件
  • 订阅store的改变等通知,根据通知处理视图,但不处理数据

//view
var someview = require('./RecipesAction');
//actions
var actions = require('./RecipesAction');
//store
var store = require('./RecipesStore.js');

var Recipes = React.createClass({
  componentDidMount: function() {
     //添加监听作为测试
     store.addTestListener(this.test);
     //初始化数据
     actions.initRecipes();
  }
  render() {
    return (
         <someview></someview>
    );
  ....

);

actions层

actions负责只负责通过dispatcher把事件派发出去,但不处理事件。

var dispatcher = require('../../AppDispatcher.js');

var ButtonActions = {
	  initRecipes: function (text) {
		    dispatcher.dispatch({
		      actionType: 'init_recipes',
		    });
	  },
};
module.exports = ButtonActions;

dispatcher

dispatcher负责处理事件调度,注册和事件委托,派送事件等,在rn中有2个文件就可以完成,

文件dispatcher.js 比较长,大家随意看看,源码解释可以参考:- Flux源码解析(一)

dispatcher.js 文件引用了 invariant ,这个只是一个工具,作为错误消息提示。

这两个文件从react版本中移过来,只是做了简单的修改,可以见文章最后的附录

store

store是数据实体,并保护数据修改的委托实现。

通过dispatcher.register方法注册dispatcher的listener, 类对象使用RCTDeviceEventEmitter对象进行事件转发。

//成功
var dispatcher = require('../../AppDispatcher.js');

import React, {

   Text,
  View
} from 'react-native';


var EventEmitter = require('RCTDeviceEventEmitter');

var TEST_EVENT = 'test';

var RecipesStore = {
  items: [],
  fliter:{},
  init: function() {
    EventEmitter.emit("test");
  },

  addTestListener:function(cb){
    EventEmitter.addListener(TEST_EVENT, cb);
  },

};

dispatcher.register(function(action){
     switch(action.actionType){
      case 'init_recipes' : 
          RecipesStore.init();
      default: //no op
     }

}); 
module.exports = RecipesStore;

参考

最后

感谢收看,如果对大家有帮助,请github上follow和star,本文发布在刘彦玮的技术博客,转载请注明出处

附录

Dispatcher.js


/**
 * Copyright (c) 2014-2015, 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;

invariant.js

/**
 * Copyright 2013-2015, 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.
 */

'use strict';

/**
 * Use invariant() to assert state which your program assumes to be true.
 *
 * Provide sprintf-style format (only %s is supported) and arguments
 * to provide information about what broke and what you were
 * expecting.
 *
 * The invariant message will be stripped in production, but the invariant
 * will remain to ensure logic does not differ in production.
 */

var NODE_ENV = process.env.NODE_ENV;

var invariant = function(condition, format, a, b, c, d, e, f) {
  if (NODE_ENV !== 'production') {
    if (format === undefined) {
      throw new Error('invariant requires an error message argument');
    }
  }

  if (!condition) {
    var error;
    if (format === undefined) {
      error = new Error(
        'Minified exception occurred; use the non-minified dev environment ' +
        'for the full error message and additional helpful warnings.'
      );
    } else {
      var args = [a, b, c, d, e, f];
      var argIndex = 0;
      error = new Error(
        format.replace(/%s/g, function() { return args[argIndex++]; })
      );
      error.name = 'Invariant Violation';
    }

    error.framesToPop = 1; // we don't care about invariant's own frame
    throw error;
  }
};

module.exports = invariant;

Loading Disqus comments...
Table of Contents