为了脱离前端鄙视链,于是自己写个框架 - React 党看完沉默了

前端鄙视链大家都懂:写 React 的看不起写 Vue 的,写 Vue 的看不起写 jQuery 的,写 jQuery 的看不起所有人。 本文作者,一个 React 一次都没用过的纯 Vue 党,写了个叫 vitarx 的框架。 结果 React 党来看:眼熟;Vue 党来看:眼熟;Solid 党来看:报警了,有人偷家。


序章:一段没人认得出的代码

tsx 复制代码
<div v-if={isLoggedIn}>欢迎回来</div>

你以为这是 Vue? v-if 这个指令太眼熟了------Vue 十年的招牌。

但等等,Vue 的指令写在 template 的字符串属性里:

html 复制代码
<!-- Vue -->
<div v-if="isLoggedIn">欢迎回来</div>

而这里,v-if 接的是花括号里的 TS 表达式------这是 JSX,React 的地盘。

那你以为这是 React? React 从来没有 v-if,它的信条是"一切皆 JS",条件渲染靠三元和 &&。

都不是。这是 vitarx:Vue 的指令语法,原封不动搬进 JSX 的花括号。

tsx 复制代码
// vitarx:Vue 的指令 × JSX 的表达式
<div v-if={type.value === 'admin'}>管理员</div>   // 假分支不建 DOM,切换即销毁重建
<span v-show={collapsed}>被折叠的</span>     // display 切换,DOM 常驻
<SearchInput v-model={keyword} />                  // 双向绑定
<div v-html={trustedHtml} />                 // innerHTML

Vue 的指令清单几乎全数在场------只是值从字符串变成了真正的 TS 表达式,由 TypeScript 亲自做类型检查。这一点 Vue 的模板做不到(模板表达式的类型检查至今是短板),React 又没有语法可查。

形是 Vue 的形,骨是 JSX 的骨,类型是 TS 的类型。认亲游戏,从这里开始。


第一回合:React 的「形」

tsx 复制代码
export default function Counter(props: { initial?: number }) {
  const count = ref(props.initial ?? 0)
  const inc = () => count.value++
  return <button onClick={inc}>{count}</button>
}

函数组件、props 进、JSX 出------React 的组件形态,像素级复刻 。useState 没了,换成 .value 的 ref,这是后话。

真正的反转藏在框架的"体质"里,先按下不表------因为它牵出的,是第三回合的主角。


第二回合:Vue 的「骨」

ts 复制代码
import { ref, reactive, computed, watch, watchEffect, nextTick,
         toRefs, provide, inject, untracked, createScope } from 'vitarx'

这份导入清单,Vue 开发者可以闭眼上手 :ref、reactive、computed、watch、watchEffect、toRefs、nextTick、provide/inject 一个不少;createScope 对应 Vue 3.2 的 EffectScope;组件写法也是熟悉的 defineExpose、mergeProps、h() 渲染函数。

但放大看,细节全是伏笔:

ts 复制代码
// computed 的 getter 第一个参数是【旧值】------Vue 没有
const total = computed((oldValue) => {
  console.log('上一次是', oldValue)
  return items.value.reduce((s, i) => s + i.score, 0)
})
ts 复制代码
// untracked:非追踪执行,读取不建立依赖------Solid 的 untrack 改了个名
untracked(() => console.log(snapshot.value))

还有一处刁钻的边界,构成 vitarx 最著名的"防呆设计":

tsx 复制代码
{count}               // ✅ 简单引用,自动等价 {count.value}
{count > 0 && <div/>} // ⚠️ 复合表达式不解包!必须显式写 count.value
                      //    否则 ref 对象恒为 truthy,条件永真且不追踪依赖

Vue 的响应式全家桶,Solid 的影子,vitarx 自己的规则。第二次扑空。


第三回合:Solid 的「魂」------这才是它真正的底色

回到第一回合埋的伏笔:这个"React 长相"的组件,体质和 React 完全不同。

React vitarx
组件函数执行次数 每次状态变化重跑一遍 一辈子只执行一次
更新方式 重渲染 + 虚拟 DOM diff 信号级精确更新:数据变哪,DOM 动哪
useMemo / useCallback 依赖数组优化税 不存在,也不需要
闭包陈旧值 经典事故现场 不存在,函数体没有"第二次"
依赖数组漏写 useEffect 事故之首 无依赖数组,自动追踪

组件函数只执行一次,没有任何"重渲染"概念------这不是 React 的灵魂,是 Solid 的哲学。而且 vitarx 走得更彻底:没有虚拟 DOM diff 这一层,更新由编译器直接定位到 DOM 节点。

控制流更是直接从 Solid 家里搬来的:

tsx 复制代码
<For each={items} key="id">
  {(item, index) => <li>{item.name}</li>}
</For>

<Switch fallback={<div>未知状态</div>}>
  <Match when={status === 'loading'}>加载中...</Match>
  <Match when={status === 'error'}>出错了</Match>
  <Match when={status === 'success'}>加载成功</Match>
</Switch>

对照 Solid,连 fallback 的命名都一样。但 vitarx 有自己的铁律------响应式列表禁止 .map() 渲染:

tsx 复制代码
{items.map(i => <li>{i.name}</li>)}   // ❌ 无 diff 架构下,整列表推倒重建
<For each={items} key="id">...</For>    // ✅ keyed 复用 + 信号级增量更新

生命周期也站在 Solid 这一侧:清理用 onDispose(Solid 的 onCleanup 换了个名),不存在 onUnmounted:

tsx 复制代码
onMounted(() => { timer = setInterval(() => count.value++, 1000) })
onDispose(() => clearInterval(timer))   // 启动/清理一一配对,随组件作用域自动回收

第四回合:视图控制------Vue 的老朋友们压轴登场

tsx 复制代码
<Teleport to="body"><Modal /></Teleport>       // Vue 的 Teleport
<Transition name="fade" mode="out-in" appear>
  {show.value && <div class="box" />}
</Transition>                                  // Vue 的 Transition,6 个 CSS 类名一个不少
<Freeze is={currentTab} max={5} />             // KeepAlive 换了个名字,include/exclude/max 齐全
<Suspense fallback={<span>加载中</span>}>
  <HeavyChart />                               // lazy 从 vitarx 导入,不是 React.lazy
</Suspense>

v-enter-from → v-enter-active → v-enter-to,Vue 的过渡类名体系原样移植,外加 12 个 JS 钩子可配 GSAP。React 开发者看到这里,已经放弃了辨认。


第五回合:路由------三个人的影子,一个人的骨架

ts 复制代码
const router = createRouter({
  routes: defineRoutes(
    { path: '/', component: Home },
    { path: '/user/{id}', name: 'user', component: User, pattern: { id: /^\d+$/ } },
    { path: '/admin', component: AdminLayout, children: [
        { path: '', component: Dashboard }
      ] }
  ),
  mode: 'path',
  beforeEach: guard
})

配置式路由表、嵌套 children、全局守卫 ------vue-router 的骨架。但参数语法是 {id} 花括号,React Router 和 vue-router 都写 :id,没人这么写。它还玩了一手 Next.js 的文件路由:

bash 复制代码
src/pages/
├── index.tsx          →  /
├── user/[id].tsx      →  /user/{id}     (方括号文件名,Next.js 风格)
├── _layout.tsx        →  同目录布局(Remix 风格)
└── _config.ts         →  目录级路由配置

外加编译期宏 definePage(unplugin-vue-router 的既视感):

tsx 复制代码
definePage({
  name: 'userDetail',
  meta: { title: '用户详情', requiresAuth: true },
  pattern: { id: /^[0-9]+$/ }
})

vue-router 的守卫体系 + Next.js 的文件约定 + 自创的花括号参数------路由这块,它谁的都不认。


彩蛋抓包:defaultProps 不是遗风,是必然

tsx 复制代码
Button.defaultProps = { type: 'default', size: 'medium' }

React 已经废弃了 defaultProps,官方替代是参数解构默认值。vitarx 为什么不跟进?

因为这条路在它的响应式模型下是死路。 React 的 props 是每次重渲染重新传入的普通对象,入口解构无所谓快照;而 vitarx 的 props 编译为惰性 accessor,派生计算依赖"在动态上下文里读 props.x"建立追踪------一旦解构 const { x } = props:

  1. 函数体只执行一次 → x 变成永久快照,后续变化丢失;
  2. 想靠编译器救也不行------TSX 里的解构是任意位置的任意模式(别名、嵌套、rest、对象整体外传),编译器无法可靠重写回访问器。Vue 3.5 试过(reactive props destructure),也只有在 SFC 编译器全权接管模板的前提下才能成立,至今仍是实验特性。

所以 defaultProps 在 vitarx 反而是最自洽的设计 :它在视图构建阶段合并一次,props 依旧走 accessor,响应式链路完全不受污染。一个由响应式模型反向约束语法设计的典型案例------框架的每个"不合时宜",背后都是一条走不通的路。

类似的"倔强"还有:

  • 无 Vue 式隐式属性透传 :给组件传 class,不在 props 里显式声明就被静默丢弃------把"组件 API"钉死在类型上;
  • v-model 只认 modelValue :自定义属性走 useModel(props, 'xxx') + WithModelEvent 类型,双向绑定的边界清清楚楚;
  • class 与 className 同用直接编译报错(E016):不留运行时玄学。

终章之一:基因检测报告

特征 认亲结果
(props) => JSX 函数组件、defaultProps React 的形态
只执行一次、信号级细粒度更新、无 vdom diff、untracked / onDispose、<For> / <Switch> / <Match> Solid 的灵魂
ref / reactive / computed / watch / provide / inject / nextTick / h() / mergeProps / defineExpose / EffectScope Vue 的响应式与组件 API
v-if / v-show / v-model / v-html / v-bind 指令、Transition / Teleport Vue 的模板语法与视图控制
Suspense + lazy、onError 错误边界 React / Vue 联名
路由 {id} 参数、[id].tsx 文件路由、definePage 宏 Next / Remix / unplugin-vue-router 的影子,骨架是 vue-router
computed 带旧值、<Freeze>、dynamic()、builder()、ref 解包边界、无隐式透传、编译期校验错误码(E006-E016) vitarx 独家

终章之二:它真正想做的事

认亲游戏到此结束。如果只把它当成"缝合怪",就错过了重点------那些熟悉的 API 之下,是五个一以贯之的设计主张:

一、把更新成本从 O(组件) 压到 O(节点)。 没有重渲染、没有虚拟 DOM diff,状态变化由信号直接驱动对应 DOM 节点。性能不靠开发者优化(useMemo/memo),靠架构本身。

二、"只执行一次"消灭一整类 bug。 没有第二次执行,就没有闭包陈旧值、没有 stale props、没有依赖数组焦虑。函数组件写过三年的人,都为 useEffect 的依赖数组付出过代价------这里的答案是:不存在。

三、类型安全贯穿到模板层。 指令的值是真正的 TS 表达式(Vue 模板做不到),IfBlock 让 v-if 链正确收窄类型,WithModelEvent 让双向绑定可推导,路由参数有 RouteIndexMap 类型映射。

四、编译器不只是转译,还是守门员。 branch / expr / accessor 三件套在编译期做静态优化;<Switch> 里混入非 <Match> 子元素、<For> 丢 key、class 撞 className------这些错误在编译期报错,而不是运行时才发现。

五、反直觉之处皆有出处。 无 fallthrough、v-model 收窄、ref 解包边界、保留 defaultProps------每一条"为什么不一样"背后,都是响应式模型推演后的必然,而不是标新立异。


结语

第一次写 vitarx,你会不断经历这样的时刻:

"咦,这不是 Vue 吗?"------写到一半发现组件函数只执行一次。 "那这肯定是 React。"------结果它没有重渲染,没有虚拟 DOM diff。 "......Solid?"------可它的状态明明是 ref(0),它的指令明明是 v-if。

它确实"缝合"了三家:React 给了它组件的形态,Vue 给了它响应式的骨架和模板的语法,Solid 给了它更新的哲学 。但缝合不等于拼凑------computed 比 Vue 多一个旧值参数,v-model 只认 modelValue,<For> 强制 keyed,&& 左侧必须显式布尔。每一条偏差,都是模型推演的结果。

它不打算让你把任何一家的肌肉记忆原样搬进来。它只是把三家的好东西摆上同一张桌,然后告诉你:

"你以为你认识我?先写三百行再说。"

相关推荐
zhiyouTech1 小时前
从“被看见“到“被信任“:关于信源评级机制的一点观察
大数据·前端·人工智能
Hashan1 小时前
Vibe Coding 下前后端怎么对接接口?后端不给力的兜底方案
前端·后端·vibecoding
你挚爱的强哥2 小时前
【sgKeyboard】自定义组件:虚拟键盘
前端·javascript·计算机外设
锅里游的鱼吖2 小时前
echarts自定义折线图
前端·javascript·数据库
hunteritself2 小时前
卷卷卷!GPT-6 Sol、Luna 正式发布,OpenAI 开始卷价格了
大数据·前端·人工智能·深度学习·transformer
何何____3 小时前
Vue 全局事件总线详解
前端·javascript·vue.js
涉密IT资质笔记4 小时前
涉密人员脱密期管理规范:期限分级模型、就业限制边界与违规认定标准
java·服务器·前端·网络·数据库
u0111026754 小时前
工具页案例图如何编写 alt让图片说明与处理场景对应
java·前端·javascript·图像处理·人工智能·算法·ai作画
事圆则缓4 小时前
Flutter 状态管理框架对比(五):GetX 的响应式、依赖注册与生命周期
前端·javascript·flutter