从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.modelValue
和 props['onUpdate:modelValue']
。后者是一个函数,可以调用它给modelValue赋予新的值。不过通常我们会使用emit函数,发送一个事件,二者是等价的。
v-model存在效率问题
为什么我感觉v-model已经过时了呢?因为几点吧
- v-model在组件上存在效率问题
- v-model写起来麻烦
- vue3 有更好的办法:组件间共享ref,reactive
写起来麻烦这一点,应该没啥好说的,在子组件中要想修改,需要抛一个事件,而不是直接赋值,
那么效率问题是什么呢?
前面提到,当我们这样写的时候:<Child v-model={text.value} />
其实是向子组件传递了两条属性:modelValue
和 onUpdate: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}} />
})