我想使用Vue作为一个非常薄的层来将现有模型对象绑定到视图。
下面是一个玩具应用程序,说明了我的问题。我有一个来自Web Audio API的GainNode对象,我想将它的value
绑定到滑块上。
在Angular中这很容易。双向绑定适用于任何对象,无论是不是Angular组件的一部分。在Vue中是否有类似的方法呢?
在真实应用程序中,我有大量的编程生成对象列表。我需要将它们绑定到组件中,例如<Knob v-for='channel in channels' v-model='channel.gainNode.gain.value'>
。
更新:我使用了下面的第二种解决方案,并且它看起来工作得很好,直到我尝试将v-model
绑定到同一音频参数的两个组件上。然后它就以一种完全神秘的方式不起作用,我无法调试。最终我放弃了并使用了getter/setter,虽然有更多的样板代码,但是有一个优点,那就是能够正常工作。
class MyApp {
constructor() {
// core model which I'd prefer to bind to
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8; // want to bind a control to this
// attempts to add reactivity
this.reactiveWrapper = Vue.reactive(this.audioNode.gain);
this.refWrapper = Vue.ref(this.audioNode.gain.value);
}
get gainValue() { return this.audioNode.gain.value; }
set gainValue(value) { this.audioNode.gain.value = value; }
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '#AppView',
data() { return {
// core model which I'd prefer to bind to
model: appModel,
// attempt to add reactivity
dataAliasAudioNode: appModel.audioNode }
}
});
app.mount('#mount');
<script type='text/x-template' id='AppView'></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id='mount'></div>
问题补充 #1: 在探索做到这一点的方法时,我发现(如前所述),如果我绑定到外部对象 (GainNode
Web音频API),它不是响应式的,但是如果我自己构建一个类似的外部对象,绑定到嵌套参数就是响应式的。以下是示例代码:
// my version Web Audio API's AudioContext, GainNode, and AudioParam
class AudioParamX {
constructor() {
this._value = 0;
}
get value() { return this._value; }
set value(v) { this._value = v; }
}
class ValueParamX extends AudioParamX {
}
class GainNodeX {
constructor() {
this.gain = new ValueParamX();
}
}
class AudioContextX {
createGain() {
return new GainNodeX();
}
}
//==================================================================
class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.xaudio = new AudioContextX();
this.xaudioNode = this.xaudio.createGain();
}
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '#AppView',
data() { return { model: appModel } }
});
app.mount('#mount');
<script type='text/x-template' id='AppView'></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id='mount'></div>
解决方法#1:
经过进一步探索,我想到了一个减少getter/setter代码的解决办法。我可以选择:
- 创建自己的
ref
版本(不知道为什么Vue.ref
不起作用)或者 - 使用
Proxy
代理对象,并在setter被调用时调用$forceUpdate
。
这两种方法都可行,但缺点是我必须将代理暴露为成员并绑定到它而不是原始对象。但这比同时暴露getter和setter更好,而且可以与v-model
一起使用。
class MyApp {
createWrapper(obj, field) {
return {
get [field]() { return obj[field]; },
set [field](v) { obj[field] = v; }
}
}
createProxy(obj) {
let update = () => this.forceUpdate();
return new Proxy(obj, {
get(target, prop) { return target[prop] },
set(target, prop, value) {
update();
return target[prop] = value
}
});
}
watch(obj, prop) {
hookSetter(obj, prop, () => this.forceUpdate());
}
constructor() {
this.audio = new AudioContext();
// core model which I'd prefer to bind to
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .1; // want to bind a control to this
this.audioNode.connect(this.audio.destination);
// attempts to add reactivity
this.wrapper = this.createWrapper(this.audioNode.gain, 'value');
this.proxy = this.createProxy(this.audioNode.gain);
}
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '<AppView :model="model" />',
data() { return { model: appModel } },
});
app.component('AppView', {
template: '#AppView',
props: ['model'],
mounted() {
this.model.forceUpdate = () => this.$forceUpdate();
}
})
app.mount('#mount');
<style>body { user-select: none; }</style>
<script type='text/x-template' id='AppView'></script>
<div id='mount'></div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
解决方法 #2:
另一个解决方法是修补我想要监视的访问器(accessor),并在其中调用$forceUpdate
。这个方法有最少的模板代码(boilerplate)。只需调用watch(obj, prop)
,该属性即可变为响应式。
对我来说,这是一个相当可接受的解决方案。然而,我不确定当我开始将事物移动到子组件中时,这些解决方案是否能很好地发挥作用。下一步我将尝试这样做。我仍然不明白为什么 Vue.reference
没有达到同样的效果。
我希望以最符合Vue本机的方式完成这个任务,这似乎是一个相当典型的用例。
class MyApp {
watch(obj, prop) {
hookObjectSetter(obj, prop, () => this.forceUpdate());
}
constructor() {
this.audio = new AudioContext();
// core model which I'd prefer to bind to
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .1; // want to bind a control to this
this.watch(this.audioNode.gain, 'value'); // make it reactive
}
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '<AppView :model="model" />',
data() { return { model: appModel } },
});
app.component('AppView', {
template: '#AppView',
props: ['model'],
mounted() {
this.model.forceUpdate = () => this.$forceUpdate();
}
})
app.mount('#mount');
function hookObjectSetter(obj, prop, callback) {
let descriptor = Object.getOwnPropertyDescriptor(obj, prop);
if (!descriptor) {
obj = Object.getPrototypeOf(obj);
descriptor = Object.getOwnPropertyDescriptor(obj, prop);
}
if (descriptor && descriptor.configurable) {
let set = descriptor.set || (v => descriptor.value = v);
let get = descriptor.get || (v => descriptor.value);
Object.defineProperty(obj, prop, {
configurable: false, // prevent double-hooking; sorry anybody else!
get,
set(v) {
callback();
return set.apply(this, arguments);
},
});
}
}
<script type='text/x-template' id='AppView'></script>
<div id='mount'></div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
targetTypeMap
中的东西”。这正确吗? - Mudv-model
绑定到目标属性,那么它将无法工作。我在我的帖子中更新了一些解决方法。解决方法2似乎比你写的少了一些样板代码(留下了可以被每个类重用的hookSetter
函数)。你看到任何问题吗? - MudyourObject.toString().slice(8, -1)
。在我的术语中,内置对象是从浏览器 API 创建的对象。你猜对了,任何不在 Vue 的targetTypeMap
中的东西都是无效的。注意:你自定义的类是“对象”类型,并且可以被响应化。 - Duannxwatch
方法将其降回到20个。因此,现在对我来说,这是最有吸引力的选项。我只希望我能找到一种更本地的方法来告诉Vue观察这些属性。 - Mud$forceUpdate
方法的缺点是它无法更新依赖于类属性的计算属性。因此,如果您希望在类中使用computed
,则必须使用setter/getter
方法。 - Duannx