普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月6日首页

Vue 中的 JSX:让组件渲染更灵活的正确方式

作者 前端大付
2025年11月6日 16:01

在日常 Vue 项目中,你可能已经非常熟悉 template 写法:结构清晰、语义明确、直观易读。但当业务进入更复杂的阶段,你会发现:

  • 模板语法存在一定限制
  • 某些 UI 渲染逻辑十分动态
  • 条件/循环/组件嵌套变得越来越难写
  • h 函数(createVNode)看得懂,但自己写非常痛苦

这时,你可能会想:有没有一种方式既能保持 DOM 结构的直观性,又能充分利用 JavaScript 的灵活表达?

答案是:JSX

你可能会问:JSX 不是 React 的东西吗?
是,但 Vue 同样支持 JSX,并且在组件库、动态 UI 控件、高度抽象组件中大量使用。

本文将从三个核心问题带你理解 Vue 中的 JSX:

  1. JSX 的本质是什么?
  2. 为什么需要 JSX,它能解决什么问题?
  3. 在 Vue 中如何优雅地使用 JSX?

h 函数:理解 JSX 的前置知识

Vue 组件的 template 最终会被编译为一个 render 函数,render 函数会返回 虚拟 DOM(VNode)

也就是说,下面这段模板:

<h3>你好</h3>

最终会变成类似这样的 JavaScript:

h('h3', null, '你好')

也就是说:

h 函数 = 手写虚拟 DOM 的入口
JSX = h 函数的语法糖


为什么需要 JSX?来看一个真实例子

假设我们做一个动态标题组件 <Heading />,它根据 level 动态渲染 <h1> ~ <h6>

如果使用 template,你可能写成这样:

<h1 v-if="level === 1"><slot /></h1>
<h2 v-else-if="level === 2"><slot /></h2>
...
<h6 v-else-if="level === 6"><slot /></h6>

非常冗余、难拓展、维护成本高。

使用 h 函数可以简化为:

import { h, defineComponent } from 'vue'

export default defineComponent({
  props: { level: Number },
  setup(props, { slots }) {
    return () => h('h' + props.level, {}, slots.default())
  }
})

但写 h 函数并不优雅,标签、属性、事件都要自己构造。

这时 JSX 就来了。


在 Vue 中使用 JSX

① 安装 JSX 插件(Vite 项目)

npm install @vitejs/plugin-vue-jsx -D

② 在 vite.config.js 中启用

import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

export default {
  plugins: [vue(), vueJsx()]
}

③ 使用 JSX 改写 Heading 组件

import { defineComponent } from 'vue'

export default defineComponent({
  props: { level: Number },
  setup(props, { slots }) {
    const Tag = 'h' + props.level
    return () => <Tag>{slots.default()}</Tag>
  }
})

是不是比手写 h 爽太多了?
结构依然直观,但不受 template 语法局限。


JSX 的核心能力:灵活、动态、纯 JavaScript

举个再明显的例子:Todo 列表

import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const title = ref('')
    const todos = ref([])

    const addTodo = () => {
      if (title.value.trim()) {
        todos.value.push({ title: title.value })
        title.value = ''
      }
    }

    return () => (
      <div>
        <input vModel={title.value} />
        <button onClick={addTodo}>添加</button>
        <ul>
          {todos.value.length
            ? todos.value.map(t => <li>{t.title}</li>)
            : <li>暂无数据</li>}
        </ul>
      </div>
    )
  }
})

可以看到:

模板语法 JSX 对应写法
v-model vModel={value}
@click onClick={fn}
v-for array.map()
v-if 三元 / if 表达式

本质是 JavaScript,可以随意写逻辑。


JSX vs Template:应该如何选择?

对比点 template JSX
可读性 强,结构清晰 视业务复杂度而定
动态表达能力 较弱(语法受限) 非常强(JS 语法全支持)
编译优化 优秀,可静态提升 不如 template 友好
适用场景 普通业务 UI 高动态逻辑、组件库、渲染函数场景

一句话总结选择策略:

业务组件优先 template
高动态组件或组件库优先 JSX


JSX 并不是来替代 template 的,而是:

当 template 无法优雅表达渲染逻辑时,JSX 给你打开了一扇窗。

  • 它让组件变得更灵活
  • 它让写 render 函数变得不再痛苦
  • 它让 Vue 在复杂组件抽象层面更加强大

掌握 JSX,是从“会写 Vue”向“会设计 Vue 组件”的关键一步。

昨天以前首页

如何使用 Vuex 设计你的数据流

作者 前端大付
2025年11月3日 16:04

前端数据管理

解决这个问题的最常见的一种思路就是:专门定义一个全局变量,任何组件需要数据的时候都去这个全局变量中获取。一些通用的数据,比如用户登录信息,以及一个跨层级的组件通信都可以通过这个全局变量很好地实现。在下面的代码中我们使用 _store 这个全局变量存储数据。

// 比如挂一个全局的变量
window._store = {}

其他组件和这个量的交互

image.png

但这样就会产生一个问题,window._store 并不是响应式的,如果在 Vue 项目中直接使用,那么就无法自动更新页面。所以我们需要用 ref 和 reactive 去把数据包裹成响应式数据,并且提供统一的操作方法,这其实就是数据管理框架 Vuex 的雏形了。

Vuex是什么

Vuex 就相当于我们项目中的大管家,集中式存储管理应用的所有组件的状态。

// 安装
npm install vuex@next
// store/index.js

import { createStore } from 'vuex'
const store = createStore({
state () {
return {
count: 666
}
},
mutations: {
add (state) {
state.count++
    }
}
})

在项目入口文件 src/main.js 中,使用 app.use(store) 进行注册,这样 Vue 和 Vuex就连接上了。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'

const app = createApp(App)
app.use(store)
.use(router)
.mount('#app')

新增一个 components/Count.vue

<template>
<div @click="add">
{{count}}
</div>
</template>

<script setup>
import { computed } from 'vue'
import {useStore} from 'vuex'
let store = useStore()
let count = computed(()=>store.state.count)
function add(){
store.commit('add')
}
</script>

实现一个 简单的点击累加器

此处有点区别在于 count 不是使用 ref 直接定义,而是使用计算属性返回了 store.state.count,也就是刚才在 src/store/index.js 中定义的 count。add 函数是用来修改数据,这里我们不能直接去操作 store.state.count +=1,因为这个数据属于 Vuex 统一管理,所以我们要使用 store.commit(‘add’) 去触发 Vuex 中的 mutation 去修改数据。

数据什么时候用 Vuex 来管理 什么时候用 ref 这样区分

对于一个数据,如果只是组件内部使用就是用 ref 管理;如果我们需要跨组件,跨页面共享的时候,我们就需要把数据从 Vue 的组件内部抽离出来,放在 Vuex 中去管理。

比如项目中的登录用户名,页面的右上角需要显示,有些信息弹窗也需要显示。这样的数据就需要放在 Vuex 中统一管理,每当需要抽离这样的数据的时候,我们都需要思考这个数据的初始化和更新逻辑。

image.png

手写mini Vuex

// src/store/gvuex.js

import { inject, reactive } from 'vue'
const STORE_KEY = '__store__'
function useStore() {
    return inject(STORE_KEY)
}
function createStore(options) {
return new Store(options)
}
class Store {
constructor(options) {
this._state = reactive({
data: options.state()
})
this._mutations = options.mutations
}
}
export { createStore, useStore }

上面的代码还暴露了 createStore 去创建 Store 的实例,并且可以在任意组件的 setup 函数内,使用 useStore 去获取 store 的实例

class Store {
// main.js入口处app.use(store)的时候,会执行这个函数
install(app) {
app.provide(STORE_KEY, this)
}
}

Store 类内部变量 _state 存储响应式数据,读取 state 的时候直接获取响应式数据 _state.data,并且提供了 commit 函数去执行用户配置好的 mutations。

import { inject, reactive } from 'vue'
const STORE_KEY = '__store__'
function useStore() {
return inject(STORE_KEY)
}
function createStore(options) {
return new Store(options)
}
class Store {
constructor(options) {
this.$options = options
this._state = reactive({
data: options.state
})
this._mutations = options.mutations
}
get state() {
return this._state.data
}
commit = (type, payload) => {
const entry = this._mutations[type]
entry && entry(this.state, payload)
}
install(app) {
app.provide(STORE_KEY, this)
}
}
export { createStore, useStore }

使用 这个 gvuex

import {useStore} from '../store/gvuex'
let store =useStore()
let count = computed(()=>store.state.count)
function add(){
store.commit('add')
}

Vuex 使用

Vuex 就是一个公用版本的 ref,提供响应式数据给整个项目使用

使用 getters 类似于 Vue 中的 computed

import { createStore } from 'vuex'
const store = createStore({
state () {
return {
count: 666
}
},
getters:{
double(state){
return state.count*2
}
},
mutations: {
add (state) {
state.count++
}
}
})
export default store

组件中使用 时

let double = computed(()=>store.getters.double)

异步数据获取

在 Vuex 中,mutation 的设计就是用来实现同步地修改数据。如果数据是异步修改的,我们需要一个新的配置action。现在我们模拟一个异步的场景,就是点击按钮之后的 1 秒,再去做数据的修改。

import { createStore } from 'vuex'

const store = createStore({
state () {
return {
count: 666
}
},
// ...
actions:{
asyncAdd({commit}){
setTimeout(()=>{
commit('add')
},1000)
}
}
})

export default store

action 并不是直接修改数据,而是通过 mutations 去修改,actions 的调用方式是使用 store.dispatch

function asyncAdd(){
store.dispatch('asyncAdd')
}

Vuex 在整体上的逻辑如下图所示,从宏观来说,Vue 的组件负责渲染页面,组件中用到跨页面的数据,就是用 state 来存储,但是 Vue 不能直接修改 state,而是要通过actions/mutations 去做数据的修改。

image.png

image.png

在决定一个数据是否用Vuex 来管理的时候,核心就是要思考清楚,这个数据是否有共享给其他页面或者是其他组件的需要。如果需要,就放置在 Vuex 中管理;如果不需要,就应该放在组件内部使用ref 或者 reactive 去管理。

Pinia

Vuex 由于在 API 的设计上,对 TypeScript 的类型推导的支持比较复杂,用起来很是痛苦。Pinia 的 API 的设计非常接近 Vuex5 的提案,首先,Pinia 不需要Vuex 自定义复杂的类型去支持 TypeScript,天生对类型推断就非常友好,并且对 VueDevtool 的支持也非常好

❌
❌