Vue3探索:v-model是否已经过时

从React转到Vue3,当我看到v-model的时候,感觉这应该是一个过时的设计了,把我的想法分享给大家。

V-model是什么?

v-model是一个语法糖,能更方便的实现数据的双向绑定。

双向绑定

const Demo = defineComponent({
  setup() {
    const text = ref('');
    return () => (
      <>
        <input v-model={text.value} />
        <div>{text.value}</div>
      </>
    )
  }
})
 

上面的代码即实现了一个input数据到text的双向绑定。它其实就相当于下面的代码的简写:

<input value={text.value} onInput={event => text.value = event.target.value} />
 

应用到组件上

const Parent = defineComponent({
  setup() {
    const text = ref('');
    return () => (
      <>
        <Child v-model={text.value} />
        <div>{text.value}</div>
      </>
    )
  }
})

const Child = defineComponent({
  props: {
    modelValue: String,
    'onUpdate:modelValue': Function
  },
  setup(props, { emit }) {
    const handleInput = event => {
      emit('update:modelValue', event.target.value);
      // or
      // props['onUpdate:modelValue'](event.target.value);
    }
    return () => <input value={props.modelValue} onInput={handleInput} />
  }
})
 

这里通过v-model,父组件把text.value绑定到子组件的 props.modelValue。text.value放生变化,props.modelValue同步改变,子组件可以通过事件反过来让text.value改变。

这里的v-model也是一个简写,它相当于:

<Child modelValue={text.value} onUpdate:modelValue={event => text.value=event} />
 

可能你疑惑modelValue是什么?modelValue是vue3中v-model默认的属性的名字。你也可以用其它的名字:
比如v-model={[text.value, 'name']},表示子组件将接收到一个 name的prop,和一个 onUpdate:name的prop。

也就是说 v-model={text.value}等价于v-model={[text.value, 'modelValue']}

我们再来看看子组件,子组件收到两条属性:props.modelValueprops['onUpdate:modelValue'] 。后者是一个函数,可以调用它给modelValue赋予新的值。不过通常我们会使用emit函数,发送一个事件,二者是等价的。

v-model存在效率问题

为什么我感觉v-model已经过时了呢?因为几点吧

  • v-model在组件上存在效率问题
  • v-model写起来麻烦
  • vue3 有更好的办法:组件间共享ref,reactive

写起来麻烦这一点,应该没啥好说的,在子组件中要想修改,需要抛一个事件,而不是直接赋值,

那么效率问题是什么呢?

前面提到,当我们这样写的时候:<Child v-model={text.value} /> 其实是向子组件传递了两条属性:
modelValueonUpdate:modelValue。问题就出在这第二条属性,它是一个函数,而这个函数在父组件每次渲染时都会重新生成,于是对于子组件来说,它的props发生了变化,于是子组件就一定会重新渲染。这就可能造成不必要的渲染。

一个例子:

const Parent = defineComponent({
  setup() {
    const text = ref('');
    const count = ref(0);
    return () => (
      <>
        <Child v-model={text.value} />
        <button onClick={() => count.value++}>我被点击了{count.value}次</button>
      </>
    )
  }
})

const Child = defineComponent({
  setup() {
    onRenderTracked((...args) => {
      console.log(args);
    })
    onUpdated(() => {
      console.log('Child component updated');
    })
    return () => {
      console.log('Child component start to render');
      return <div>Child</div>
    }
  }
})
 

当点击按钮时,父组件会重新渲染。同时我们在浏览器的console看到,子组件也跟着更新了,虽然它完全没有必要更新。

这个子组件甚至没有使用任何的属性,然而它还是更新了,而且这种情况下onRenderTracked也是不工作的,里面没有任何输出。

组件中共享ref/reactive变量

ref/reactive这两种响应式数据,如果被使用在多个组件中,每个组件都会响应的更新。我认为这种方式更简洁,更高效。

const Parent = defineComponent({
  setup() {
    const text = ref('');
    return () => (
      <>
        <Child text={text} />
        <div>{text.value}</div>
      </>
    )
  }
})

const Child = defineComponent({
  props: {
    text: { validator: isRef }
  },
  setup(props) {
    return () => <input v-model={props.text.value} />
  }
})
 

这个例子中,我们在父组件和子组件中共享了一个ref: text。当text发生变化了,两个组件都会进行响应更新。

v-model的模式就是父子组件中分别定义一个变量,然后进行双向绑定。

而共享ref、reactive则是,只有一个变量,多个组件同时对其响应。

有些时候我们需要限制一个prop是ref类型,这时我们可以使用 { validator: isRef }。reactive,readonly类型同理可以用 isReactive, isReadonly

肯定有人会说,你这样多个组件共享一个变量,会使代码变得不易读,不易维护。但这真的是事实么?

当看到代码中把一个ref/reactive传递给子组件时,我们显然能意识到,这个变量可能会被子组件修改。当看到prop有isRef/isReactive的限制时,我们也能意识到对这个变量的改变会影响到上层组件。从阅读性来讲,这和v-model没有什么本质的区别。和react那种 value/onChange模式也没有很大的区别。但是写起来却是简便的多。

而且我们还有readonly这一利器,如果一个object不希望被子组件修改,传一个readonly过去就行了。

下面的例子中,userReadonly以readonly的方式代理了一个reactive,两者会共同响应数据的变化。我们把readonly类型传给子组件,子组件可以使用,响应数据变化,但无法修改它。

const Parent = defineComponent({
  setup() {
    const user = reactive({
      name: 'name',
    })
    const userReadonly = readonly(user);
    return () => (
      <>
        <Child user={userReadonly} />
        <div onClick={() => user.name = 'another name'}>Parent: {user.name}</div>
      </>
    )
  }
})
const Child = defineComponent({
  props: {
    user: { validator: isReadonly }
  },
  setup(props) {
    return () => <div>Child: {props.user.name}</div>
  }
})
 

实际的代码中,一般来说一个变量实际上最多也就被传递3层,这基本上没有什么维护难度。

如果业务确实需要一个变量在很多很多的组件中使用,那么你应该使用provide/inject。

如何使用带v-model的组件库?

实际使用中,我们可能用到一些第三方组件库。其中的组件可能会提供v-model接口。如果要考虑效率问题,我们需要在setup里面定义onUpdate:xxx函数。因为setup只执行一次,所以这个函数不会改变,也就不会造成子组件的不必要渲染。

const Parent = defineComponent({
  const text = ref('');
  const onUpdateModel = val => text.value = val;
  return () => <Child modelValue={text.value} onUpdate:modelValue={onUpdateModel}} />
})
 

原创文章,作者:我心飞翔,如若转载,请注明出处:https://www.pipipi.net/12580.html

发表评论

登录后才能评论