Code前端首页关于Code前端联系我们

先给结论,JSX里不仅能用v-model,灵活度还比模板更高,但得换个书写姿势

terry 7小时前 阅读数 94 #Vue
文章标签 JSXmodel

这是最近两个月在前端交流群里被问得最多的Vue3新语法组合问题——毕竟很多从模板转JSX做复杂交互组件、做低代码平台自定义面板、或者适配跨端场景(比如uni-app用Vue3 + JSX写高性能原生兼容组件、Taro 3.6+做渲染层拆分)的开发者,刚上手总会摸不清:模板里v-model默认绑value/input,带修饰符.sync也直接换冒号绑定事件前缀,双向绑定的底层逻辑全帮你封装好,连修饰符事件的处理都省事儿;可JSX里全是显式的函数调用、原生/组件属性绑定,v-model的语法糖会不会就“失效”变复杂,甚至要自己去手写emit + update:xxx的完整逻辑? Vue3的核心编译器@vue/compiler-sfc,不管是处理单文件组件的template还是JSX/TSX文件的render函数,本质上都是把v-model这类语法糖,转换成了“绑定值到特定prop + 侦听update:特定prop事件 + 同步修改本地响应式数据”的三段式逻辑,不过template编译器是自动选“特定prop”(不带参数默认是modelValue),JSX编译器(vitejs/plugin-vue-jsx)需要你稍微明确点,但这种明确反而带来了更多可能性——比如不用像模板那样只能传字符串参数的v-model,JSX可以直接绑定变量参数,或者一次写多个v-model不带冲突。

基础用法对比:单文件组件的template vs render函数的JSX/TSX

先拿最简单的场景对比,比如做一个输入框的父组件,分别用两种写法实现双向绑定。

模板写法(给回忆一下,对比用)

模板里默认v-model绑子组件的modelValue,侦听update:modelValue事件,父组件只要写就行,连修饰符v-model.trim/v-model.number都能用,子组件只需要defineProps接收modelValue,defineEmits定义['update:modelValue'],然后在输入时emit('update:modelValue', 处理后的值)。

JSX/TSX的基础单v-model写法

JSX里没有template那样的冒号绑定修饰符的语法,vitejs/plugin-vue-jsx给我们留了两种语法糖替代方案,都是官方推荐的:

第一种是最接近模板、最常用的“v-model={[绑定的数据, 可选参数, 可选修饰符对象]}”数组语法,比如和刚才模板完全等价的父组件代码:

// MyInput的defineProps/defineEmits和模板完全一样,不用改!这是Vue3 JSX最爽的地方之一
import { ref } from 'vue'
import MyInput from './MyInput.vue'
export default {
  setup() {
    const inputVal = ref('')
    return () => (
      <div>
        {/* 数组第一个元素是响应式数据(ref/reactive里的key都可以,不过key会自动解包吗?等下修饰符部分说) */}
        {/* 第二个可选参数不传,就是默认modelValue */}
        {/* 第三个可选参数是修饰符对象,{ trim: true, number: true }这种,对象的key就是修饰符名,value是布尔值true */}
        <MyInput v-model={[inputVal, 'modelValue', { trim: true }]} />
        <p>当前输入内容:{inputVal.value}</p>
      </div>
    )
  }
}

哦对,刚才的问题,JSX数组语法里的第一个元素,如果是ref,编译器会自动帮你.value解包吗?不对,父组件render函数里直接输出ref.value,编译器才会解,但v-model数组第一个是ref本身,编译器处理的时候会直接用它的响应式引用来赋值和侦听,不用手动加.value;如果是reactive里的某个属性,比如form.name,那就直接传就行,编译器会自动生成访问路径。

第二种是显式三段式逻辑但用vue提供的withModifiers辅助函数(如果带修饰符的话)的写法,适合理解底层逻辑或者自定义场景需求,

// 不带修饰符的话,直接这么写就行
<MyInput
  modelValue={inputVal.value}
  onUpdate:modelValue={(newVal) => (inputVal.value = newVal)}
/>
// 带修饰符的话,用withModifiers包装处理函数
import { ref, withModifiers } from 'vue'
import MyInput from './MyInput.vue'
export default {
  setup() {
    const inputVal = ref('')
    // withModifiers第一个参数是原始处理函数,第二个是修饰符数组
    const handleUpdate = withModifiers(
      (newVal) => (inputVal.value = newVal),
      ['trim']
    )
    return () => (
      <div>
        <MyInput modelValue={inputVal.value} onUpdate:modelValue={handleUpdate} />
        <p>当前输入内容:{inputVal.value}</p>
      </div>
    )
  }
}

进阶用法1:一次绑定多个v-model(不带冲突,参数可以是字符串或变量)

模板里要绑定多个v-model,得加冒号参数,比如,这个功能在Vue3 JSX里也完全保留,而且数组语法里的第二个可选参数可以是变量,这是模板做不到的(模板参数只能是静态字符串)。

比如做一个多标签页的表单编辑器,每个标签页对应不同的字段,字段名是从后端接口返回的动态数组:

import { ref, reactive } from 'vue'
import MyTabForm from './MyTabForm.vue'
export default {
  setup() {
    const form = reactive({})
    const tabs = ref([
      { label: '姓名', field: 'userName' },
      { label: '手机号', field: 'phone' },
      { label: '地址', field: 'address' }
    ])
    // 假设接口返回tabs,先初始化form里的空字段
    tabs.value.forEach((tab) => (form[tab.field] = ''))
    return () => (
      <div>
        {/* 用map渲染多个标签页组件,这里假设每个标签页组件有一个tab prop,还有动态的v-model绑定 */}
        {/* 注意这里的第二个参数用的是tab.field变量,完全没问题! */}
        {tabs.value.map((tab) => (
          <MyTabForm key={tab.field} tab={tab} v-model={[form[tab.field], tab.field]} />
        ))}
        {/* 输出整个form看看,不用.value,因为render函数里直接用reactive的属性会解包吗?不对,reactive整体不会,但单个属性在JSX插值里会,v-model数组里单个属性直接传就行 */}
        <pre>{JSON.stringify(form, null, 2)}</pre>
      </div>
    )
  }
}

进阶用法2:JSX原生标签的v-model用法

刚才说的都是自定义组件的v-model,那原生标签(比如input、textarea、select、checkbox、radio)呢?会不会和自定义组件不一样?

其实也差不多,但原生标签有默认的prop和事件映射,vitejs/plugin-vue-jsx还做了额外的优化——比如原生input type="text"/textarea默认映射的是value/input事件,type="checkbox"/radio默认映射的是checked/change事件,select默认映射的是value/change事件,而且这些原生标签的默认修饰符(trim/number/lazy),用数组语法的第三个参数或者显式三段式逻辑的withModifiers都能直接生效。

比如写一个包含多个原生标签的小表单:

import { ref, reactive } from 'vue'
export default {
  setup() {
    const form = reactive({
      name: '',
      age: 0,
      bio: '',
      gender: '男',
      hobbies: [],
      agree: false
    })
    return () => (
      <div style={{ padding: '20px' }}>
        <div style={{ margin: '10px 0' }}>
          <label>姓名:</label>
          <input type="text" v-model={[form.name, 'value', { trim: true }]} />
        </div>
        <div style={{ margin: '10px 0' }}>
          <label>年龄:</label>
          {/* number修饰符会自动把输入的字符串转成数字,这里用数组语法的话会生效 */}
          <input type="number" v-model={[form.age, 'value', { number: true }]} />
        </div>
        <div style={{ margin: '10px 0' }}>
          <label>简介:</label>
          <textarea v-model={[form.bio]} rows="3" />
        </div>
        <div style={{ margin: '10px 0' }}>
          <label>性别:</label>
          <input type="radio" name="gender" value="男" v-model={[form.gender]} /> 男
          <input type="radio" name="gender" value="女" v-model={[form.gender]} /> 女
        </div>
        <div style={{ margin: '10px 0' }}>
          <label>爱好:</label>
          {/* checkbox如果绑定数组,会自动选中符合value的项 */}
          <input type="checkbox" value="编程" v-model={[form.hobbies]} /> 编程
          <input type="checkbox" value="阅读" v-model={[form.hobbies]} /> 阅读
          <input type="checkbox" value="运动" v-model={[form.hobbies]} /> 运动
        </div>
        <div style={{ margin: '10px 0' }}>
          <input type="checkbox" v-model={[form.agree]} /> 同意用户协议
        </div>
        <pre>{JSON.stringify(form, null, 2)}</pre>
      </div>
    )
  }
}

这里要注意一个小细节:原生radio的v-model数组语法里,第二个参数不用传,因为默认是checked,和input text的value不一样,但编译器会自动识别type属性,然后选对prop和事件。

进阶用法3:自定义v-model修饰符(子组件如何接收和处理修饰符)

模板里自定义v-model修饰符,子组件是通过defineProps接收一个名为modelModifiers的prop(如果v-model有参数的话,比如v-model:name,那prop名就是nameModifiers),然后在emit的时候根据修饰符做处理,这个逻辑在Vue3 JSX里也完全通用,而且不管父组件用的是数组语法还是显式三段式逻辑,子组件的接收方式都不变。

比如给刚才的MyInput组件加一个自定义修饰符capitalize,功能是把输入的第一个字母转成大写:

// 子组件MyInput.vue
<script setup>
// 接收modelValue和modelModifiers
const props = defineProps({
  modelValue: String,
  modelModifiers: {
    type: Object,
    default: () => ({})
  }
})
// 定义update:modelValue事件
const emit = defineEmits(['update:modelValue'])
// 处理输入事件
const handleInput = (e) => {
  let newVal = e.target.value
  // 如果有trim修饰符,先去空格?不过刚才说父组件用数组语法的trim修饰符,withModifiers会帮子组件处理吗?
  // 哦不对!官方的trim/number/lazy修饰符,是在父组件的编译器里处理的,也就是用数组语法的第三个参数或者显式三段式的withModifiers时,处理函数已经把trim/number/lazy的逻辑执行了,子组件收到的已经是处理后的值
  // 但自定义修饰符,必须子组件自己处理!
  if (props.modelModifiers.capitalize) {
    newVal = newVal.charAt(0).toUpperCase() + newVal.slice(1)
  }
  emit('update:modelValue', newVal)
}
</script>
<template>
  <input type="text" :value="modelValue" @input="handleInput" />
</template>

然后父组件用数组语法调用:

import { ref } from 'vue'
import MyInput from './MyInput.vue'
export default {
  setup() {
    const inputVal = ref('')
    return () => (
      <div>
        {/* 第三个参数里加capitalize: true就行 */}
        <MyInput v-model={[inputVal, 'modelValue', { capitalize: true }]} />
        <p>当前输入内容:{inputVal.value}</p>
      </div>
    )
  }
}

如果父组件用显式三段式逻辑调用,那自定义修饰符需要子组件自己接收,父组件不用做任何额外处理,因为自定义修饰符是通过prop传递给子组件的:

// 父组件显式三段式逻辑调用MyInput带capitalize修饰符
import { ref } from 'vue'
import MyInput from './MyInput.vue'
export default {
  setup() {
    const inputVal = ref('')
    const handleUpdate = (newVal) => (inputVal.value = newVal)
    return () => (
      <div>
        {/* 显式传modelModifiers prop!这个是自定义修饰符必须的,不管父组件用哪种写法 */}
        <MyInput
          modelValue={inputVal.value}
          onUpdate:modelValue={handleUpdate}
          modelModifiers={{ capitalize: true }}
        />
        <p>当前输入内容:{inputVal.value}</p>
      </div>
    )
  }
}

哦对,刚才的数组语法里,第三个参数的修饰符,除了官方的trim/number/lazy,自定义的都会自动传给子组件对应的Modifiers prop,不用显式传,这就是数组语法的方便之处。

常见问题和踩坑指南

JSX数组语法里的第一个元素传ref还是reactive的属性?

都可以,传ref的话,编译器会自动用它的响应式引用,不用手动加.value;传reactive的属性的话,编译器会自动生成访问路径,也不用手动加.value,但要注意,如果是reactive的属性,在父组件render函数里直接赋值的话,比如form.name = '张三',是没问题的,但如果是ref的话,在setup函数里赋值要加.value,在render函数里的事件处理函数里赋值也要加.value。

原生标签的v-model数组语法里,第二个参数可以省略吗?

可以,编译器会自动根据type属性选对默认prop:

  • type="text"/password/search/url/email/hidden等文本类:默认value
  • type="checkbox"/radio:默认checked
  • select:默认value
  • textarea:默认value

自定义组件的v-model参数如果是变量,子组件的prop和事件名怎么定义?

子组件的prop名就是变量的值,事件名就是update:变量的值,对应的Modifiers prop名就是变量的值Modifiers,比如变量是tab.field,值是userName,那子组件的defineProps里要接收userNameuserNameModifiers,defineEmits里要定义update:userName

为什么我的JSX v-model不生效?

检查这几个地方:

  • 父组件用的是数组语法吗?如果是显式三段式逻辑,有没有绑定对prop和事件?prop名和事件名有没有写错?
  • 子组件有没有defineProps接收对应的prop?有没有defineEmits定义对应的事件?
  • 子组件有没有在合适的时机emit事件?比如input事件、change事件、点击事件?
  • 子组件emit的参数对不对?比如原生input text的emit参数是e.target.value,自定义组件的emit参数是新的绑定值?
  • 如果是ref的话,有没有在setup函数里正确定义?有没有return出来?
  • 如果是@vitejs/plugin-vue-jsx的话,有没有在vite.config.js里正确配置?比如有没有安装插件?有没有在plugins数组里添加vueJsx()?

Vue3里用JSX开发组件,v-model不仅能用,而且灵活度比模板更高:数组语法最接近模板,适合大多数场景;显式三段式逻辑适合理解底层逻辑或者自定义场景;多个v-model可以绑定变量参数,原生标签和自定义组件的用法也几乎一致,自定义修饰符的接收和处理方式也和模板完全通用,只要掌握了“书写姿势”,JSX的v-model就能和模板里一样简单好用,甚至能解决模板里解决不了的动态绑定问题。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门