重構與模式(二)——釋出訂閱模式與中介者模式實際使用

網易考拉前端團隊發表於2017-09-22

釋出訂閱模式

    釋出訂閱模式我們都用過,也都嘗試去實現過這樣的一個EventHandler,但是這裡想再重點強調的是如何實現 先發布後訂閱的功能? 為什麼我們需要支援先發布後訂閱的功能呢?
     實際開發過程中我們遇到過的先發布後訂閱的場景為:我們有一個展示使用者資訊的頭部導航模組,這個模組依賴於獲取使用者資訊的模組來顯示使用者頭像,但是獲取使用者資訊是非同步的,在頭部資訊trigger之後,我們的頭部模組才載入完並listen(我們的模組用了惰性載入)。
    為了滿足這個需求,我們需要建立一個存放離線事件的堆疊,當事件釋出的時候,如果此時還沒有訂閱者來訂閱這個事件,我們可以暫時把釋出事件的動作與引數包裹在一個函式裡面,這些包裝函式將被存入堆疊中,等有物件來訂閱此事件的時候,我們再遍歷堆疊且依次執行這些包裝函式。

(function(root, factory) {
    root.Event = factory();
})(window, function() {
    var Event;
    Event = (function() {
        var namespaceCache = {},
            _shift = Array.prototype.shift,
            _unshift = Array.prototype.unshift,
            _slice = Array.prototype.slice,
            _default = 'default',
            self = this;

        var each = function(ary, fn) {
            if(!ary) return;
            var ret;
            for(var i = 0, l = ary.length; i < l; i++) {
                var n = ary[i];
                ret = fn.call(n, i, n);
            }
            return ret;
        } 

        var _remove = function(key, cache, fn) {
            if(cache[key]) {
                if(fn) {
                   cache[key].forEach(function(val, index){
                    console.log(val);
                    console.log(fn);
                    console.log(val === fn);
                        if(val === fn) {
                            cache[key].splice(index, 1);
                        }                        
                   });
                }else{
                    cache[key] = [];
                }
            }
        }

        var _trigger = function() {
            var cache = _shift.call(arguments),
                key = _shift.call(arguments),
                args = arguments,
                _self = this,
                ret,
                stack = cache[key];

                if(!stack || !stack.length) {
                    return;
                }

                stack.forEach(function(fn, index) {
                    fn.apply(_self, args);
                });
        };


        var _create = function(namespace) {
            namespace = namespace || _default;
            var cache = {}, // 快取回撥事件
                offlineStack = [], // 離線事件
                ret = {
                    listen: function(key, fn, last) {
                        cache[key] = cache[key] || [];
                        cache[key].push(fn);  
                        offlineStack.forEach(function(fn, index) {
                            return fn();
                        });

                        offlineStack  =  null;                        
                    },
                    trigger: function() {
                        var fn,
                            args,
                            _self = this;

                        //  要把cache 加到引數中,後面閉包裡面的函式要用        
                        _unshift.call(arguments, cache); 
                        args = arguments;
                        // 這裡是核心,利用閉包存下回撥與當時傳入的引數
                        fn = function() {
                            return _trigger.apply(_self, args);
                        }   

                        if(offlineStack) {
                            return offlineStack.push(fn);
                        }           
                        return fn();
                    },
                    remove: function(key, fn){
                        _remove(key, cache, fn);
                    }
                };

                // 主要就是為了能夠使用對應namespace下的cache    
                return namespace ? (namespaceCache[ namespace ] ? namespaceCache[ namespace ] : namespaceCache[ namespace ] = ret) : ret;
        };


        return {
            create: _create,  
            listen: function(key, fn, last) {
                var event = this.create();
                event.listen(key, fn, last);
            },
            trigger: function() {
                var event = this.create();
                event.trigger.apply( this, arguments );
            },
            remove: function(key, fn) {
                var event = this.create();
                event.remove(key, fn);
            }

        };
    })();
    return Event;
})複製程式碼

中介者模式

中介者模式就是用來幫助我們把複雜的層級關係鋪平為一對多的簡單關係

使用中介者模式以後,可以幫助我們讓原本按照樹型結構傳遞的資料可以轉換為平級的結構(本來資料就不像元件需要有樹型關係)

模仿redux

我們可能在regular 會這麼簡易的去寫

    var store = new Regular();
    // 釋出
    store.dispatch = function(action) {
        switch(action.type){
            case 'CHANGE_TITLE': {
                this.data.title = action.data;
                break;
            },
            case ...
        }

        store.$emit('change', this.data);
    };    
    // 訂閱    
    store.subscribe = function(listener) {
        this.$on('change', listener);
    }    

    // j.js
    var J = Regular.extend({
        name: 'j',
        config: function() {
            var self = this;
            store.subscribe(function() {
                self.mapState(store.data);
            });
        },
        // 全域性的資料轉換到元件的資料
        mapState: function(storeData) {
            this.data.title = storeData.title;
            this.$update();
        },
        changeTitle: function(title) {
            store.dispatch({
                type: 'CHANGE_TITLE',
                data: title
            })
        }
    }}複製程式碼

看看 redux 的 createStore 方法大致結構

  • 我們在使用createStore方法的時候要傳遞一個reducer 方法得到一個store。
  • 通過store.getState()我們可以拿到當前的state
  • store.dispatch(action) 是唯一的一個可以改變內部state的方法,我們通過給reducer傳遞當前state與action,呼叫reducer方法獲得當前的new state。並且通知所有的listener
  • 通過 store.subscribe(listener) 訂閱方法,那麼createStore 的結構應該如下所示
const createStore = (reducer) => {
  let state;
  let listeners = [];

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  };

  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(l => l !== listener);
    }
  };

// By the time the store is returned, we want the initial state to be populated. We're going to dispatch a dummy action just to get the reducer to return the initial value.
dispatch({}); // dummy dispatch


  return { getState, dispatch, subscribe };
};複製程式碼

最後讓我們看一個完整的使用Redux的計數器的小例子:

  //  reducer
  const counter = (state = 0, action) {
    switch(action.type) {
      case 'INCREMENT': 
        return state + 1;
      case 'DECREMENT': 
        return state - 1;
      default:
        return state;
    }
  }

  // Counter 元件
  const Counter = ({
    value,
    onIncrement,
    onDecrement
  }) => (
    <div>
      <h1>{value}</h1>
      <button onClick={onIncrement}>+</button>
      <button onClick={onDecrement}>-</button>
    </div>
  )

  const {createStore} = Redux;
  const store = createStore(counter);

  const render = ()=> {
    ReactDom.render(
      <Counter
       value = store.getState()
       onIncrement={()=>{
        store.dispatch({
          type: 'INCREMENT'
        });
       }}
       onDecrement={()=>{
        store.dispatch({
          type: 'DRECEMENT'
        })
       }}
       />,
       document.getElementById('root')
    );
  };

  store.subscribe(render);
  render();複製程式碼

相關文章