解码:redux的奥秘

redux

redux 是一个函数式的数据流管理库,但是我一直都不了解其实现原理,之前一直用人家封装好的。

根据官网大致理解其运行方式是下图这样的。通过 store dispatch 传入 action 然后经由 reducer 进行数据处理返回新的 state。

redux运行方式

我觉得这像观察者模式,然后在内部绑定了一个数据。

验证

为了验证我的想法我翻看了 redux 的源码,精简之后他的代码大概是这样的.

function createState(reducer, initState,)
  let currentState = initState;
  let currentReducer = reducer;
  let currentListeners = [];

  let isDispatching = false;
  function dispatch(action) {
    try {
      isDispatching = true;

      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }
    const listeners = currentListeners;
    listeners.forEach((listener) => {
      listener();
    });
  }
  function getCurrentState() {
    return currentState;
  }
  function subscribe(listener) {
    if (isDispatching) {
      console.log("正在触发事件不能添加新的监听器");
      return;
    }
    currentListeners.push(listener);
    let isSubscribed = true;
    return function unSubscribe() {
      if (!isSubscribed) {
        console.log("已经取消过了");
        return;
      }
      const index = currentListeners.indexOf(listener);
      currentListeners.splice(index, 1);
    };
  }

  dispatch({ type: "init" });

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

自己动手搞一个

跟我想的差不太多

首先创造一个观察者

function createObserver() {
  let listeners = [];
  function dispatch(eventType, data) {
    listeners.forEach((listener) => {
      listener(eventType, data);
    });
  }
  function addEventListener(eventType, listener) {
    listeners.push(listener);
  }
  return {
    dispatch: dispatch,
    addEventListener,
  };
}

const observer = createObserver();

observer.addEventListener("add", (type, data) => {
  console.log(data);
});

observer.dispatch("add", 1);

通过创建一个可观察的对象,然后向对象发布事件,调用所有监听事件的方法。

这就是观察者模式,进一步改造,我们需要一个全局变量作为我们的 state;

那么在使用的时候需要做这件事。

let state = 0;

const observer = createObserver();

observer.addEventListener("add", (type) => {
  state = state + 1;
});

observer.dispatch("add");

这样就完成了对 state 的操作,通常情况下,对数据的操作和使用数据通常不在一起。

为了代码更好的复用,需要提取数据操作的部分作为一个单独的内容,只操作数据。

并且我们把 state 放到到 createObserver 中,作为 observer 的成员。

function createObserver() {
  // 全局唯一
  let state = 0;
  let operateDataFunc = [];

  // 发送事件
  function dispatch(eventType, data) {
    // 操作数据的方法
    state = operateDataFunc.reduce((data, operateFunc) => {
      return operateFunc(data, eventType);
    }, state);
    console.log(state);
  }

  // 添加操作数据的方法
  function addOperateFunc(eventType, listener) {
    operateDataFunc.push(listener);
  }

  function getState() {
    return state;
  }

  return {
    getState: getState,
    dispatch: dispatch,
    addOperateFunc: addOperateFunc,
  };
}

const observer = createObserver();

observer.addOperateFunc("add", (prevState) => {
  return prevState + 1;
});

observer.dispatch("add");

现在数据操作搞定了,接下来就是数据使用方了,需要对数据进行操作后通知所有的数据使用方,state 数据发生变动。

继续改造 dispatch 方法

function createObserver() {
  let state = 0;
  let operateDataFunc = [];
  let users = [];

  // 发送事件
  function dispatch(eventType, data) {
    // 操作数据的方法
    state = operateDataFunc.reduce((data, operateFunc) => {
      return operateFunc(data, eventType);
    }, state);
    notifyUser();
  }

  // 添加操作数据的方法
  function addOperateFunc(eventType, listener) {
    operateDataFunc.push(listener);
  }

  // 通知使用数据的用户,数据改变了
  function notifyUser() {
    users.forEach((user) => user());
  }

  // 添加数据使用者
  function addUser(user) {
    users.push(user);
  }

  function getState() {
    return state;
  }

  return {
    getState: getState,
    dispatch: dispatch,
    addOperateFunc: addOperateFunc,
    addUser: addUser,
  };
}

const observer = createObserver();

observer.addUser(() => {
  console.log("数据发生变化", observer.getState());
});

observer.addOperateFunc("add", (prevState) => {
  return prevState + 1;
});

observer.dispatch("add");

ok 到这里 mini-redux 完成一大半了,检验一下

function createObserver() {
  let state = 0;
  let operateDataFunc = {};
  let users = [];

  // 发送事件
  function dispatch(eventType, data) {
    // 操作数据的方法
    state = operateDataFunc[eventType].reduce((data, operateFunc) => {
      return operateFunc(data, eventType);
    }, state);
    notifyUser();
  }

  // 添加操作数据的方法
  function addOperateFunc(eventType, listener) {
    if (!operateDataFunc[eventType]) {
      operateDataFunc[eventType] = [];
    }
    operateDataFunc[eventType].push(listener);
  }

  // 通知使用数据的用户,数据改变了
  function notifyUser() {
    users.forEach((user) => user());
  }

  function addUser(user) {
    users.push(user);
  }

  function getState() {
    return state;
  }

  return {
    getState: getState,
    dispatch: dispatch,
    addOperateFunc: addOperateFunc,
    addUser: addUser,
  };
}

const observer = createObserver();

observer.addOperateFunc("add", (prevState) => {
  return prevState + 1;
});
observer.addOperateFunc("subtraction", (prevState) => {
  return prevState - 1;
});

let result = document.getElementById("result");
result.textContent = "result: " + observer.getState();

observer.addUser(() => {
  result.textContent = "result: " + observer.getState();
});

document.getElementById("add").onclick = () => {
  observer.dispatch("add");
};

document.getElementById("subtraction").onclick = () => {
  observer.dispatch("subtraction");
};

See the Pen observer 模式 by bxer (@angelname) on CodePen. 如果你看不到这个演示,说明当前网站不支持 codepen ,可以到 苏鑫的博客

现在完成了无副作用的 redux,函数相同的参数返回值一定是相同的,但是前段经常通过请求来获取数据。

假设我们对 result 的操作,加 1 或者减 1,来至于服务器。

// mock server
function getOperate() {
  return new Promise((r) => {
    setTimeout(() => {
      if (Math.random() < 0.5) {
        r("add");
      } else {
        r("subtraction");
      }
    }, 500);
  });
}

和同步代码一样,需要添加一个专门存放副作用函数的对象。
只需要发送给副作用事件的时候,把 dispatch 传给副作用的处理数据的函数就可以了。

最终结果

function createObserver() {
  let state = 0;
  let operateDataFunc = {};
  let users = [];
  let effectOperateDataFunc = {};

  // 发送事件
  function dispatch(eventType, data) {
    // 操作数据的方法
    state = operateDataFunc[eventType].reduce((data, operateFunc) => {
      return operateFunc(data, eventType);
    }, state);
    notifyUser();
  }

  // 添加操作数据的方法
  function addOperateFunc(eventType, listener) {
    if (!operateDataFunc[eventType]) {
      operateDataFunc[eventType] = [];
    }
    operateDataFunc[eventType].push(listener);
  }

  // 添加存在副作用的函数
  function addEffectOperateFunc(eventType, listener) {
    if (!effectOperateDataFunc[eventType]) {
      effectOperateDataFunc[eventType] = [];
    }
    effectOperateDataFunc[eventType].push(listener);
  }

  // 发送给副作用函数的事件
  async function dispatchEffect(eventType, data) {
    for (const operateFunc of effectOperateDataFunc[eventType]) {
      await operateFunc(dispatch, state, data, eventType);
    }
  }

  // 通知使用数据的用户,数据改变了
  function notifyUser() {
    users.forEach((user) => user());
  }

  function addUser(user) {
    users.push(user);
  }

  function getState() {
    return state;
  }

  return {
    getState: getState,
    dispatch: dispatch,
    addOperateFunc: addOperateFunc,
    addUser: addUser,
    addEffectOperateFunc: addEffectOperateFunc,
    dispatchEffect: dispatchEffect,
  };
}

const observer = createObserver();

observer.addOperateFunc("add", (prevState) => {
  return prevState + 1;
});
observer.addOperateFunc("subtraction", (prevState) => {
  return prevState - 1;
});

observer.addEffectOperateFunc("getOpereate", (dispatch, prevState) => {
  return getOperate().then((operateType) => {
    dispatch(operateType);
  });
});

let result = document.getElementById("result");
result.textContent = "result: " + observer.getState();

observer.addUser(() => {
  result.textContent = "result: " + observer.getState();
});

document.getElementById("add").onclick = () => {
  observer.dispatchEffect("getOpereate");
};

function getOperate() {
  return new Promise((r) => {
    setTimeout(() => {
      if (Math.random() < 0.5) {
        r("add");
      } else {
        r("subtraction");
      }
    }, 500);
  });
}

See the Pen effect 模式 by bxer (@angelname) on CodePen. 如果你看不到这个演示,说明当前网站不支持 codepen ,可以到 苏鑫的博客

这样就完成了一个玩具 redux 喽。

redux 源码中还有一个方式是和 Observables 的 ECMAScript 提案相关的。

可以有序的处理这种副作用函数的执行,有兴趣可以看看。

observables-coming-to-ecmascript

顺便一提,关于函数式编程,函数式编程是一种思维方式,也许作为前端的你可能没研究过,但是你只要用上了 map,find,reduce,forEach,filter 其实你就已经用上了函数式编程,因为在语言方面就提供了这种 api,还有就是为什么函数式编程现在才流行,因为它慢。如果感兴趣可以留言,我看看要不要写一篇函数式编程的文章。

我是苏鑫,关注我,带你搞各种各样的小玩具哦。