在业务code review,我见过太多披着 Vue 或 React 外衣的“屎山”。
尽管使用着hooks或者composition API,但依旧使用jquery那套逻辑。面试时人人都能背出“声明式是数据驱动”,可一旦落地到业务,很多人依然只会手动同步状态。
这种**“伪声明式”**编程,才是现代前端项目维护性崩塌的根源。
命令式的缺陷
我们先不谈定义,谈谈痛点。
当我们说命令式编程(Imperative)难以维护时,我们到底在烦恼什么?不是 document.querySelector 写起来麻烦,而是它强依赖于执行的时序。
在命令式逻辑中,UI 的当前状态取决于过往发生过的一系列事件的总和。
看看这段典型的“过程导向”代码:
function handleUserLogin() {
// 1. 修改按钮状态
loginBtn.innerText = 'Loading...';
loginBtn.disabled = true;
// 2. 发起请求
api.login().then(() => {
// 3. 成功回调:手动隐藏弹窗,手动刷新头部用户信息
modal.style.display = 'none';
headerUser.innerText = 'Admin';
}).catch(() => {
// 4. 失败回调:手动恢复按钮
loginBtn.innerText = 'Login'; // 如果这里忘了写 disabled = false 怎么办?
// 按钮永远不可点击
});
}
这段代码最可怕的地方在于:你必须在每一个可能的出口(Success, Error, Timeout)手动复原 UI。 只要漏掉一个逻辑分支,界面就会进入一个“不一致”的中间态——按钮转着圈,但后台其实已经报错了。
这就是命令式的诅咒:你不仅要定义“去做什么”,还要时刻记得“撤销什么”。
用 useEffect 搞定一切的错误想法
转到 React/Vue 后,很多人以为自己就在写声明式代码了。错了。
如果你在组件里大量滥用 useEffect(React)或者 watch(Vue)来手动同步状态,那无疑是在堆屎山。
请看这段我在真实项目中见过的“💩”:
// ❌ 典型的伪声明式写法
const [list, setList] = useState([]);
const [total, setTotal] = useState(0);
// 监听 list 变化,手动算出 total 塞回去
useEffect(() => {
setTotal(list.length);
}, [list]);
// 监听 total 变化,再去触发别的副作用
useEffect(() => {
if (total > 0) {
trackEvent('list_loaded');
}
}, [total]);
这根本不是声明式编程。这就是把 jQuery 的 callback 换成了 useEffect 这种更晦涩的形式。
为什么它很烂?
- 1. 数据冗余:
total 完全可以由 list 派生,为什么要把它存为独立的状态? - 2. 渲染浪费:
setList 触发一次渲染,useEffect 运行后再 setTotal 触发第二次渲染。 - 3. 逻辑割裂:本来是一体的逻辑,被硬生生拆到了不同的 Hook 里,代码一多,你根本不知道这个
total 是谁改的。
真正的声明式(Declarative)应该是这样的:
// ✅ 声明式写法
const [list, setList] = useState([]);
// 核心:UI = f(State)。total 不是一个“变量”,它是一个“公式”。
// 在数学上,只要 list 确定,total 瞬间确定,不存在中间态。
const total = list.length;
// 副作用只关注“变化”本身,而不负责制造数据
useEffect(() => {
if (list.length > 0) {
trackEvent('list_loaded');
}
}, [list]); // 依赖真实的源头
声明式的本质是描述关系,而不是编排步骤。如果你发现自己在手动“把 A 赋值给 B”,请停下来,你很可能走歪了。
单一数据源
声明式编程对开发者的要求其实更高。它要求你在写 UI 之前,先要把数据结构设计得天衣无缝。
在命令式编程里,你可以“打补丁”——哪里显示不对改哪里。
在声明式编程里,数据结构如果错了,UI 就会全盘皆输。
最常见的错误就是违反单一数据源原则。
反面教材:
一个父组件传了个 props.initialUser 给子组件,子组件把它存进自己的 state.user 里,然后开始自己维护。
三个月后,父组件更新了 initialUser,子组件毫无反应。
于是开发者又加了一个 useEffect 来监听 props 变化并同步给 state。
这就是屎山的堆积过程。
正确做法:
要么子组件完全受控(只有 Props,没有 State),要么子组件完全独立。绝不要在中间搞暧昧的“同步逻辑”。
声明式的代价:性能和黑盒
声明式编程是不是完美的?当然不是。
作为工程师,我们要清醒地认识到:所有的声明式框架,本质上都是在牺牲运行时性能(Runtime Performance)来换取开发效率。
当你写下 <div v-for="item in 10000"> 时,你哪怕只改了一个数据,Vue/React 内部都要进行极其复杂的:
如果你直接用原生的 for 循环拼接字符串然后 innerHTML,速度会快得多。
但我们为什么不这么做?
因为我们无法承受手动维护 10000 个 DOM 节点的心智负担。我们选择把性能压力转嫁给框架,让自己专注于业务逻辑。这是一个理性的交易。
何时必须回归命令式?
不要做原教旨主义者。在某些极端场景下,坚持声明式就是跟自己过不去。
有些 API 天生就是命令式的,比如:
- • Canvas / WebGL:你不可能声明一个“矩形”,你只能调用
ctx.rect()。 - • Media API:
video.play() 是一个动作,不是一个状态。 - • Focus 管理:虽然 React 有
autoFocus,但复杂的键盘导航依然需要手动 ref.current.focus()。 - • 第三方库集成:ECharts、D3、Mapbox,这些库都需要你手动初始化实例并调用方法更新。
高阶开发者的处理方式:
脏活留给自己,干净的接口留给别人。
把你所有的命令式脏代码(DOM 监听、实例销毁、坐标计算)全部封装在一个 useHook 或者 Component 内部。对外部使用者来说,他们依然只需要传递一个 data 属性,这就是封装的艺术。
结语
从命令式到声明式,不是换个写法,而是思维维度的升维。
- • 声明式是空间的编程:当数据是 X 时,视图必然是 Y。
不管你用 React 还是 Vue,请时刻审视你的代码:你是在描述结果,还是在堆砌步骤? 如果你的代码里充满了手动同步状态的胶水代码,那么你不仅没有享受框架的红利,反而背负了框架的沉重枷锁。