
前端开发者或多或少都听说过,或者使用过 状态管理器:
通常我们会选择 redux, vuex, mobx 这些著名的工具。
那么为什么要使用状态管理器呢?这篇文章,我来谈一谈这个形而上学的问题。
有一个输入框,输入文本后,在输入框下方展示出所有包含已输入文本的名字。

简单粗暴的做法:
const names = ["张零", "李一", "王二", "张三", "李四", "王五"]; const input = document.getElementById("input"); const list = document.getElementById("list"); function handleInputChange(e) { const { value } = e.target; list.innerHTML = ""; if (value === "") { return; } names.filter(name => name.includes(value)).forEach(name => { const p = document.createElement("p"); p.innerText = name; list.append(p); }); } input.addEventListener("keyup", handleInputChange); 这段代码完美地实现了上述场景的需求。
现在我们这个项目要更加工程化一些,input 和 list 是两个程序员开发的,像下面这样:
// data.js const names = ["张零", "李一", "王二", "张三", "李四", "王五"]; // input.js const input = document.createElement("input"); // list.js const list = document.createElement("list"); function handleInputChange(e) { const { value } = e.target; list.innerHTML = ""; if (value === "") { return; } names.filter(name => name.includes(value)).forEach(name => { const p = document.createElement("p"); p.innerText = name; list.append(p); }); } input.addEventListener("keyup", handleInputChange); 我们看list.js的代码,有这些隐患:
input.js 的引用依赖,这会导致 list 必须晚于 input 加载,但其实这两个组件从意义上讲,并没有这层限制,只是因为程序中有引用才导致这种情况list 直接关注了 input 的具体业务逻辑(按键),这是一个问题,每当增加一种修改 input 值的方式,list 就需要多注册一个回调来追踪这种改变。但细想起来,list 真正应该关注的是 input 的数据,而不是 input 有哪些事件可能会修改其数据input.js 的迭代非常危险,需要时刻关注有哪些模块在依赖他的事件,对自身事件的增删改操作都会对整个应用伤筋动骨因此,如果在这种开发模式下去开发一个前端应用,过不了很久,甚至在你的应用变得庞大之前,你已经做不下去了。
为了避免这种困境,我们来对这个小应用进行改造:
// data.js const names = ["张零", "李一", "王二", "张三", "李四", "王五"]; // 共同的数据源 source.js const source = (() => { let data = ""; const listeners = []; return { subscribe(listener) { listeners.push(listener); }, setData(newData) { data = newData; listeners.forEach(listener => listener()); }, getData() { return data; } }; })(); // input.js const input = document.createElement("input"); function handleInputChange(e) { source.setData(e.target.value); } input.addEventListener("keyup", handleInputChange); // list.js const list = document.createElement("list"); source.subscribe(() => { list.innerHTML = ""; const value = source.getData(); if (value === "") { return; } names.filter(name => name.includes(value)).forEach(name => { const p = document.createElement("p"); p.innerText = name; list.append(p); }); }); 这样一来是不是变得逻辑非常清晰,input 与 list 互不关心对方的具体实现,从而可以各自实现高内聚的业务逻辑,且完全没有互相的依赖。
其实我们上面的这段代码,就是一个状态管理器的原型。
// 共同的数据源 source.js const source = (() => { let data = ""; const listeners = []; return { subscribe(listener) { listeners.push(listener); }, setData(newData) { data = newData; listeners.forEach(listener => listener()); }, getData() { return data; } }; })(); 它对状态进行了封装,并提供了一套管理状态的范式。各个模块在这个范式的约定下,对状态进行操作,从而分离模块之间的依赖。
我们通过这个简单的状态管理器原型,成功分离了两个本就应该各自独立的前端模块,这也印证了状态管理器存在的价值。但是状态管理器所用的模式,并非前端领域独有,我们常用的各种 MessageQueue 系统,都是基于相同的模式在工作,也因为 MQ 的存在,大型分布式系统的各个模块间才得以各自独立,消除强依赖。所以这不是一个新的 Dogma,前后端在各自领域对解藕的探索,最终殊途同归。