前端开发者或多或少都听说过,或者使用过 状态管理器
:
通常我们会选择 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
,前后端在各自领域对解藕的探索,最终殊途同归。