Skip to content

警惕“伪声明式”编程:为什么你用着 React 却还在写 jQuery?

在业务code review,我见过太多披着 Vue 或 React 外衣的“屎山”。

尽管使用着hooks或者composition API,但依旧使用jquery那套逻辑。面试时人人都能背出“声明式是数据驱动”,可一旦落地到业务,很多人依然只会手动同步状态

这种**“伪声明式”**编程,才是现代前端项目维护性崩塌的根源。

命令式的缺陷

我们先不谈定义,谈谈痛点。

当我们说命令式编程(Imperative)难以维护时,我们到底在烦恼什么?不是 document.querySelector 写起来麻烦,而是它强依赖于执行的时序

在命令式逻辑中,UI 的当前状态取决于过往发生过的一系列事件的总和

看看这段典型的“过程导向”代码:

javascript
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)来手动同步状态,那无疑是在堆屎山。

请看这段我在真实项目中见过的“💩”:

javascript
// ❌ 典型的伪声明式写法
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)应该是这样的:

javascript
// ✅ 声明式写法
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 内部都要进行极其复杂的:

  1. 依赖收集 / 虚拟 DOM 生成
  2. Diff 算法比对
  3. 任务调度(Scheduling)
  4. 最终的 DOM Patch

如果你直接用原生的 for 循环拼接字符串然后 innerHTML,速度会快得多。

但我们为什么不这么做? 因为我们无法承受手动维护 10000 个 DOM 节点的心智负担。我们选择把性能压力转嫁给框架,让自己专注于业务逻辑。这是一个理性的交易。

何时必须回归命令式?

不要做原教旨主义者。在某些极端场景下,坚持声明式就是跟自己过不去。

有些 API 天生就是命令式的,比如:

  • Canvas / WebGL:你不可能声明一个“矩形”,你只能调用 ctx.rect()
  • Media APIvideo.play() 是一个动作,不是一个状态。
  • Focus 管理:虽然 React 有 autoFocus,但复杂的键盘导航依然需要手动 ref.current.focus()
  • 第三方库集成:ECharts、D3、Mapbox,这些库都需要你手动初始化实例并调用方法更新。

高阶开发者的处理方式:脏活留给自己,干净的接口留给别人。

把你所有的命令式脏代码(DOM 监听、实例销毁、坐标计算)全部封装在一个 useHook 或者 Component 内部。对外部使用者来说,他们依然只需要传递一个 data 属性,这就是封装的艺术

结语

从命令式到声明式,不是换个写法,而是思维维度的升维

  • 命令式是时间的编程:先做 A,再做 B。
  • 声明式是空间的编程:当数据是 X 时,视图必然是 Y。

不管你用 React 还是 Vue,请时刻审视你的代码:你是在描述结果,还是在堆砌步骤? 如果你的代码里充满了手动同步状态的胶水代码,那么你不仅没有享受框架的红利,反而背负了框架的沉重枷锁。

Last updated: