Vue 开发者狂喜!我在 React 中完美复刻了 v-if/v-for 指令
前言
作为一名同时使用 Vue 和 React 的开发者,我深深被 Vue 的指令系统所吸引。 v-if 、 v-for 、 v-show 等指令让模板开发变得异常简洁高效。但在 React 中,我们却不得不使用略显冗长的三元表达式和 map 方法。这让我萌生了一个想法:能否在 React 中实现类似 Vue 的指令系统? 经过多次尝试,我找到了三种实现方案,其中 Babel 插件方案最为完美。下面我将详细介绍这些方案的实现思路和优劣对比。
为什么需要 React 指令?
- 传统 React 条件渲染
jsx
// 条件渲染
{
isShow && <div>显示内容</div>;
}
// 列表渲染
{
items.map((item) => <div key={item.id}>{item.name}</div>);
}
- 理想中的写法
html
<div r-if={isShow}>显示内容</div>
<div r-for={item in items} key={item.id}>{item.name}</div>
给人感觉就是很简洁 完美!
实现方案对比
方案一:高阶组件(不完美)
jsx
const If = ({ condition, children }) => condition ? children : null;
const For = ({ list, children }) => list.map((item, index) => children(item, index));
<If condition={isShow}>
<div>显示内容</div>
</If>
<For list={items}>
{(item, index) => <div key={index}>{item}</div>}
</For>
优缺点分析:
-
✅ 优点:遵循 React 设计理念,无需额外工具
-
❌ 缺点:
- 语法不够直观
- 嵌套层级增加
- 无法实现真正的指令效果
方案二:Babel 插件
实现思路
因为 React 的 JSX 本质上是 JavaScript 的语法糖,无法直接扩展类似 Vue 的模板指令系统,但是我们可以通过自定义 Babel 插件,在代码编译阶段将类似 r-if 的属性转换为 React 代码
- 核心原理:
通过 Babel AST 转换,将:
jsx
<div r-if={count > 4}>我大于4才能显示</div>
转换为:
jsx
{
count > 4 && <div>我大于4才能显示</div>;
}
我们都知道 React 代码的转换主要是通过 Babel 来完成的。 在之前 webpack 项目中 我们还得下载 babel-loader,用于转换 JSX 和 ES6+ 代码 我们现在用 vite 来做项目,@vitejs/plugin-react 插件已经内置了 Babel 配置,我们只需要进行相关配置即可。
接下来核心就是如何实现这个插件了 如果还不熟悉如何编写 bable 插件,可以先看一下下面的文档学习一下 扩展阅读 :
🔗 实战:如何编写 Babel 插件 (掘金)
🔗 掘金文章 (掘金)
🔗 掘金文章 (掘金)
如果觉得内容太多学习太烦,你可以直接看我的代码,其实思路很简单
- 首先 bable 插件一定是一个函数,函数的参数对象 这个对象有一个 types 属性,这个属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
js
export default function (babel) {
//babel 中 types 属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
const { types: t } = babel;
...
...
}
- 这个函数必须返回一个具有 visitor 属性的对象,具体原因你可以看一下文档,你也可以就当做就是格式如此
js
export default function (babel) {
//babel 中 types 属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
const { types: t } = babel;
return {
name: "react-directives",
visitor: {
// 在这里编写你的访问者函数
},
};
}
- 该对象内部是对各种类型的标签(比如 JSXElement)的处理逻辑,是一个个的函数,然后我们编写函数 JSXAttribute ,目的就是转换 jsx 的 Attribute 为我们需要的语法,这个函数名称无所谓
js
export default function (babel) {
//babel 中 types 属性是一个对象,这个对象有很多方法,我们可以通过这些方法来操作 ast 树
const { types: t } = babel;
return {
name: "react-directives",
visitor: {
JSXElement(path) {},
},
};
}
- visitor 的每个方法都接收两个参数:path 和 state。我们这次只关注 path,path 是一个对象,它包含了当前节点的信息,比如节点的类型、属性、子节点等。
- 我们可以通过 path 来操作当前节点。具体 path 中有哪些值 不用过多关注,我们要获取的就是两个东西,一个是属性的名称 一个是属性的值,
- 属性的名称 path.node.name.name ===> 'r-if'
- 属性的值 path.node.value.expression ===> {count > 4}
-
查找当前 JSX 属性节点的最近的 JSX 元素父节点并且将其替换为一个新的 JSX 表达式容器节点,该节点包含一个逻辑表达式,该表达式使用逻辑与运算符将条件和原始的 JSX 元素连接起来。
jsjsxElement.replaceWith( t.jSXExpressionContainer( t.logicalExpression("&&", condition, jsxElement.node) ) );
-
最后移除原来的属性 path.remove();
实现效果

完整代码
vite.config.js
js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
react({
babel: {
plugins: ["./babel-plugin-react-directives.js"],
},
}),
],
});
插件实现 babel-plugin-react-directives.js
js
export default function (babel) {
const { types: t } = babel;
return {
name: "react-directives",
visitor: {
JSXAttribute(path) {
if (path.node.name.name?.startsWith("r-")) {
const directive = path.node.name.name;
const condition = path.node.value?.expression;
if (directive === "r-if" && condition) {
const jsxElement = path.findParent((p) => p.isJSXElement());
jsxElement.replaceWith(
t.jSXExpressionContainer(
t.logicalExpression("&&", condition, jsxElement.node)
)
);
// 移除原来的属性
path.remove();
}
}
},
},
};
}
方案三:覆写createElement
我的思路就是在运行时重写createElement,然后在createElement中处理r-if指令,但是我没有成功,有懂的大佬可以留言交流一下,不知道是不是我的方式有问题
js
import React from "react";
const originalCreateElement = React.createElement;
const customCreateElement = function (type, props, ...children) {
// 处理 r-if 指令
if (props && props["r-if"] === false) {
return null;
}
if (props && typeof props["r-if"] !== "undefined") {
return props["r-if"]
? originalCreateElement(
type,
{ ...props, "r-if": undefined },
...children
)
: null;
}
return originalCreateElement(type, props, ...children);
};
export const applyDirectives = () => {
// 确保只应用一次
if (!React.__directivesApplied) {
React.__directivesApplied = true;
React.createElement = customCreateElement; // 实际应用覆写
}
};
然后在main.jsx 引入
js
import { applyDirectives } from "./directives";
applyDirectives();
结语
通过Babel插件,我们成功在React中实现了类似Vue的指令系统。这不仅让代码更加简洁,也为React开发者提供了一种新的开发体验。虽然这只是一个开始,但它展示了AST操作的强大能力。
你会考虑在项目中使用这种方案吗?欢迎在评论区分享你的看法!
🔗 项目源码地址