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

Vue2里用JSX怎么实现v model?要注意哪些细节?

terry 8小时前 阅读数 9 #Vue
文章标签 model

不少同学在Vue2项目里尝试用JSX写组件时,总会疑惑“原来模板里用v - model很方便,换成JSX该咋实现双向绑定?有没有需要注意的地方?”其实JSX和Vue模板的运行逻辑有差异,得先理解v - model本质,再一步步拆解JSX里的实现思路,今天就从原理、写法、细节这些角度,把Vue2 + JSX实现v - model的事儿讲明白。

先搞懂Vue2里v - model的本质,才能明白JSX咋改造

Vue的v - model是语法糖,核心逻辑就两点:传值监听变化,在模板里写 <input v - model="name" />,相当于做了这两件事:

  • 给input的value属性绑定name变量(传值);
  • 监听input的input事件,把输入的新值赋值给name(监听变化)。

如果是自定义组件,<MyInput v - model="name" />,默认情况下等价于 <MyInput :value="name" @input="name = $event" />,要是组件里用model选项配置了,

export default {
  model: {
    prop: 'checked',
    event: 'change'
  }
}

那v - model就变成 <MyInput :checked="name" @change="name = $event" />

但JSX和Vue模板不一样,JSX是“ JavaScript + XML ”的结合,它没有Vue模板那种“自动解析v - model语法糖”的能力,所以在JSX里,得手动把“传值 + 监听事件”这两步写出来,模拟v - model的效果。

JSX在Vue2里的基本运行逻辑,和模板有啥不同?

Vue2里用JSX,一般是在render函数里返回JSX结构(也可以用jsx - loader这类工具让单文件组件支持JSX),比如一个简单的函数式组件:

export default {
  render() {
    return <div>Hello JSX</div>;
  }
};

JSX在Vue里会被编译成h函数调用(h是createElement的别名),最终生成虚拟DOM。

和模板相比,JSX更“自由”——它本质是JS代码,可以写变量、函数调用、三元表达式等,但失去了模板里的指令语法糖(像v - model、v - bind、v - on这些模板指令,在JSX里得用JS的方式处理),所以模板里自动帮我们做的“v - model绑定value和input事件”,在JSX里得自己动手。

原生元素(如input)的JSX“v - model”咋写?

拿最常见的输入框举例,模板里一行v - model搞定,JSX得拆成“绑定value + 监听input事件”。

步骤1:给input的value传响应式数据

假设组件的data里有个username

export default {
  data() {
    return {
      username: '初始值'
    };
  },
  render() {
    return (
      <input 
        value={this.username} 
        // 下一步:绑定input事件 
      />
    );
  }
};

步骤2:监听input事件,更新数据

输入框触发input事件时,把输入的新值(event.target.value)赋值给username,所以要给input绑定onInput事件(JSX里事件用onXxx命名,对应模板的@xxx):

render() {
  return (
    <input 
      value={this.username} 
      onInput={(event) => {
        this.username = event.target.value;
      }} 
    />
  );
}

这样就实现了“输入时更新数据,数据变化时更新输入框value”的双向绑定,和模板的v - model效果一样。

对比模板写法,理解差异

模板里只需:

<input v - model="username" />

JSX里得手动写value和onInput,这就是因为JSX没有模板的指令解析逻辑,得用JS的方式显式处理。

自定义组件的JSX“v - model”咋处理?

自定义组件的v - model分两种情况:组件用默认的model(value + input)组件用model选项自定义了prop和事件

情况1:组件用默认的model(value + input)

假设我们写了个<MyInput>组件,它内部接收value prop,触发input事件传新值,父组件用JSX调用它时,要做这两步:

  • <MyInput>value prop,值是父组件的响应式数据;
  • 监听<MyInput>input事件,把事件参数赋值给父组件数据。

示例:
父组件代码:

export default {
  data() {
    return {
      user: '默认用户'
    };
  },
  components: {
    MyInput
  },
  render() {
    return (
      <MyInput 
        value={this.user} 
        onInput={(newValue) => {
          this.user = newValue;
        }} 
      />
    );
  }
};

子组件<MyInput>的简化逻辑(接收value,输入时触发input事件):

export default {
  props: ['value'],
  render() {
    return (
      <input 
        value={this.value} 
        onInput={(e) => {
          this.$emit('input', e.target.value);
        }} 
      />
    );
  }
};

这样父组件和子组件之间就通过valueinput事件实现了双向绑定,和模板里的<MyInput v - model="user" />效果一致。

情况2:组件用model选项自定义了prop和事件

如果子组件用model选项改了默认的prop和事件名,

export default {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: ['checked'],
  render() {
    return (
      <input 
        type="checkbox" 
        checked={this.checked} 
        onChange={(e) => {
          this.$emit('change', e.target.checked);
        }} 
      />
    );
  }
};

父组件用JSX调用时,要对应改成传checked prop,监听change事件:

export default {
  data() {
    return {
      isAgree: false
    };
  },
  components: {
    MyCheckbox
  },
  render() {
    return (
      <MyCheckbox 
        checked={this.isAgree} 
        onChange={(newVal) => {
          this.isAgree = newVal;
        }} 
      />
    );
  }
};

这时候就完全模拟了模板中<MyCheckbox v - model="isAgree" />的效果,因为模板里v - model会自动根据子组件的model选项调整prop和事件名,JSX里得手动对应上。

实现JSX“v - model”时,这些细节容易踩坑!

响应式数据必须来自Vue的响应式系统

如果在JSX里给value传的是普通变量(不是data、computed、props里的),数据变化时UI不会更新,比如错误写法:

export default {
  render() {
    let username = '非响应式'; // 普通变量,不是Vue的响应式数据
    return (
      <input 
        value={username} 
        onInput={(e) => {
          username = e.target.value; // 改普通变量,UI不更新
        }} 
      />
    );
  }
};

得把username放到data里,用this.username访问,才能触发响应式更新。

事件处理函数里的this指向要注意

如果不用箭头函数,直接写onInput function(e) { ... },this可能指向不对(因为JSX里事件处理函数的this默认不是组件实例,除非绑定),所以推荐用箭头函数,保证this是组件实例:

onInput={(e) => {
  this.username = e.target.value; // 箭头函数,this指向组件
}}

或者在methods里定义函数,然后绑定this:

methods: {
  handleInput(e) {
    this.username = e.target.value;
  }
},
render() {
  return <input onInput={this.handleInput.bind(this)} />;
}

不过箭头函数更简洁,日常用得更多。

与模板v - model的“自动解包”差异

模板里v - model对数组、对象的修改是“自动响应式”的,但JSX里如果直接修改对象的属性,得确保触发响应式更新。

data() {
  return {
    form: { name: '张三' }
  };
}

模板里<input v - model="form.name" />能正常更新,但JSX里如果这么写:

<input 
  value={this.form.name} 
  onInput={(e) => {
    this.form.name = e.target.value; // 直接改对象属性,Vue2可能检测不到变化
  }} 
/>

这时候因为Vue2的响应式是基于对象的getter/setter,直接改属性可能不触发更新(虽然实际中很多情况能触发,但严格来说要遵循响应式规则),所以更安全的做法是用this.$set或者重新赋值对象:

onInput={(e) => {
  // 方法1:$set
  this.$set(this.form, 'name', e.target.value);
  // 方法2:重新赋值整个对象
  this.form = { ...this.form, name: e.target.value };
}}

多个输入项的双向绑定,要避免逻辑混乱

比如表单里有用户名、密码两个输入框,JSX里要分别处理value和事件:

data() {
  return {
    username: '',
    password: ''
  };
},
render() {
  return (
    <div>
      <input 
        value={this.username} 
        onInput={(e) => { this.username = e.target.value; }} 
        placeholder="用户名" 
      />
      <input 
        type="password" 
        value={this.password} 
        onInput={(e) => { this.password = e.target.value; }} 
        placeholder="密码" 
      />
    </div>
  );
}

可以把事件处理函数抽成通用方法,减少重复代码:

methods: {
  handleInput(key, e) {
    this[key] = e.target.value;
  }
},
render() {
  return (
    <div>
      <input 
        value={this.username} 
        onInput={(e) => this.handleInput('username', e)} 
        placeholder="用户名" 
      />
      <input 
        type="password" 
        value={this.password} 
        onInput={(e) => this.handleInput('password', e)} 
        placeholder="密码" 
      />
    </div>
  );
}

进阶:用computed优化JSX的双向绑定逻辑

当双向绑定的逻辑比较复杂(比如要做格式转换、联动其他数据),可以用computedgetset来封装。

比如用户输入手机号,要自动去掉空格,同时数据层存的是无空格的手机号,模板里v - model结合computed很方便,JSX里也能这么玩:

export default {
  data() {
    return {
      rawPhone: '' // 数据层存无空格的手机号
    };
  },
  computed: {
    formattedPhone: {
      get() {
        return this.rawPhone.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3'); // 显示时加空格分隔
      },
      set(newVal) {
        // 输入的新值(带空格),处理成无空格存到rawPhone
        this.rawPhone = newVal.replace(/\s/g, '');
      }
    }
  },
  render() {
    return (
      <input 
        value={this.formattedPhone} 
        onInput={(e) => {
          this.formattedPhone = e.target.value; // 触发computed的set
        }} 
      />
    );
  }
};

这样输入框里显示的是带空格的格式,而数据层存的是纯数字,逻辑被computed封装起来,JSX里只需要绑定formattedPhone的value和input事件,代码更简洁,也避免了在render里写复杂处理逻辑。

JSX实现v - model的核心思路

不管是原生元素还是自定义组件,JSX里模拟v - model的核心就是“手动绑定值 + 监听变化事件”

  1. 给元素或组件传对应的prop(如input的value、自定义组件的value/checked等);
  2. 监听对应的事件(如input的input事件、自定义组件的input/change等),在事件回调里更新响应式数据;
  3. 注意响应式数据的处理、this指向、复杂逻辑封装这些细节,避免踩坑。

虽然JSX没有模板v - model那样的“语法糖自动解析”,但手动拆分后,我们能更灵活地控制双向绑定的每一步,尤其是在处理复杂交互、自定义组件通信时,这种“显式控制”反而能减少隐藏的bug,多写几个例子,熟悉了JSX的事件和props绑定方式,实现v - model就会很顺手啦~

版权声明

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

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门