Redux源码里的闭包应用

2017-12-15  本文已影响0人  杨洋的围脖啊

javascript里一个相对难懂且面试官极爱问到的概念就是闭包。

我在早先的面试也遇到过考闭包,只记得我解释的也并不清楚,面试官不甚满意。

今天在看redux的源码,又看到redux用到了闭包。

这个用到闭包的方法就是createStore。贴出它的代码:

function createStore(reducer, initialState, enhancer) {
  if (typeof initialState === 'function' && typeof enhancer === 'undefined') {
    enhancer = initialState;
    initialState = undefined;
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.');
    }

    return enhancer(createStore)(reducer, initialState);
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.');
  }

  var currentReducer = reducer;
  var currentState = initialState;
  var currentListeners = [];
  var nextListeners = currentListeners;
  var isDispatching = false;

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice();
    }
  }

  /**
   * Reads the state tree managed by the store.
   *
   * @returns {any} The current state tree of your application.
   */
  function getState() {
    return currentState;
  }

  /**
   * Adds a change listener. It will be called any time an action is dispatched,
   * and some part of the state tree may potentially have changed. You may then
   * call `getState()` to read the current state tree inside the callback.
   *
   * You may call `dispatch()` from a change listener, with the following
   * caveats:
   *
   * 1. The subscriptions are snapshotted just before every `dispatch()` call.
   * If you subscribe or unsubscribe while the listeners are being invoked, this
   * will not have any effect on the `dispatch()` that is currently in progress.
   * However, the next `dispatch()` call, whether nested or not, will use a more
   * recent snapshot of the subscription list.
   *
   * 2. The listener should not expect to see all states changes, as the state
   * might have been updated multiple times during a nested `dispatch()` before
   * the listener is called. It is, however, guaranteed that all subscribers
   * registered before the `dispatch()` started will be called with the latest
   * state by the time it exits.
   *
   * @param {Function} listener A callback to be invoked on every dispatch.
   * @returns {Function} A function to remove this change listener.
   */
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.');
    }

    var isSubscribed = true;

    ensureCanMutateNextListeners();
    nextListeners.push(listener);

    return function unsubscribe() {
      if (!isSubscribed) {
        return;
      }

      isSubscribed = false;

      ensureCanMutateNextListeners();
      var index = nextListeners.indexOf(listener);
      nextListeners.splice(index, 1);
    };
  }

  /**
   * Dispatches an action. It is the only way to trigger a state change.
   *
   * The `reducer` function, used to create the store, will be called with the
   * current state tree and the given `action`. Its return value will
   * be considered the **next** state of the tree, and the change listeners
   * will be notified.
   *
   * The base implementation only supports plain object actions. If you want to
   * dispatch a Promise, an Observable, a thunk, or something else, you need to
   * wrap your store creating function into the corresponding middleware. For
   * example, see the documentation for the `redux-thunk` package. Even the
   * middleware will eventually dispatch plain object actions using this method.
   *
   * @param {Object} action A plain object representing “what changed”. It is
   * a good idea to keep actions serializable so you can record and replay user
   * sessions, or use the time travelling `redux-devtools`. An action must have
   * a `type` property which may not be `undefined`. It is a good idea to use
   * string constants for action types.
   *
   * @returns {Object} For convenience, the same action object you dispatched.
   *
   * Note that, if you use a custom middleware, it may wrap `dispatch()` to
   * return something else (for example, a Promise you can await).
   */
  function dispatch(action) {
    if (!(0, _isPlainObject2["default"])(action)) {
      throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
    }

    if (typeof action.type === 'undefined') {
      throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.');
    }

    try {
      isDispatching = true;
      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }

    var listeners = currentListeners = nextListeners;
    for (var i = 0; i < listeners.length; i++) {
      listeners[i]();
    }

    return action;
  }

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   *
   * You might need this if your app implements code splitting and you want to
   * load some of the reducers dynamically. You might also need this if you
   * implement a hot reloading mechanism for Redux.
   *
   * @param {Function} nextReducer The reducer for the store to use instead.
   * @returns {void}
   */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.');
    }

    currentReducer = nextReducer;
    dispatch({ type: ActionTypes.INIT });
  }

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  dispatch({ type: ActionTypes.INIT });

  return {
    dispatch: dispatch,
    subscribe: subscribe,
    getState: getState,
    replaceReducer: replaceReducer
  };
}

当我们调用这个函数:

var store = createStore(reducer);

我们先看这个函数返回的是什么。

return {
  dispatch: dispatch,
  subscribe: subscribe,
  getState: getState,
  replaceReducer: replaceReducer
};

它返回了一个对象,该对象有四个属性,四个属性分别存放了一个函数。也就是说,变量store是一个有着4个函数的对象。

想到了什么?store是不是很像C#等高级语言的对象——有着4个实例方法的实例对象?等等,但是这个对象似乎没有成员变量,那么它就是一个无状态的对象啊!

但是我们挑出四个方法的一个方法(函数)来看看:

function getState() {
  return currentState;
}

我们调用这个方法:

var store = createStore(reducer);
store.getState();

它却返回了是一个表示状态的变量啊。但是我们store并没有任何“成员变量”。翻看代码发现currentState是创建storecreateStore函数里的局部变量:

var currentState = initialState;

对于大多数语言特性来说,局部变量在方法结束调用之后就会销毁。但是这里的局部变量并不会销毁,它继续驻存在内存中,所以store对象里的函数可以引用它。

store对象就是一个闭包,它不仅包含了4个函数,还包含了运行这4个函数所需要的所有变量:

var currentReducer = reducer;
var currentState = initialState;
var currentListeners = [];
var nextListeners = currentListeners;
var isDispatching = false;

虽然以上5个变量均不是store的内部变量,而是生成store变量的父函数createStore的局部变量,但它们在createStore结束之后依然在留存在内存中,且可供store访问。这使得你在调用store的时候,无需为它提供任何参数变量,store自成一体,独立运行,顾名思义,所以被称为闭包(closure)。store说我闭着我的大门,也能自己运行。

最后一个极度简化源码便于理解的例子:

function createStore(state) {
  var currentState = state;
  function getState(){
    return currentState;
  }
  return {getState : getState}
}

var v = createStore("hello,world");
alert(v.getState());

如果编程语言本身支持函数(方法)嵌套,闭包是自然而然是一个需要考虑的问题。

子函数引用了父函数的局部变量,当子函数独立之后,子函数引用的父函数的那些变量当然需要继续使用啊。这时候,独立的子函数加上子函数引用的父函数的变量便是一个闭包。

上一篇下一篇

猜你喜欢

热点阅读