Vue通过SFC绑定到外部对象



几周前,我问了一个非常类似的问题,试图以被动的方式将UI控件绑定到web音频AudioParam对象。我唯一能可靠地工作的就是在我的模型对象上使用getter/setter。当不使用组合API时,这还不错。这是在行动中的

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 { model: appModel } }
});
app.mount('#mount');
<script type='text/x-template' id='AppView'>
<div>
model.audioNode.gain.value: {{model.audioNode.gain.value}}
</div>
<hr>
<div>
<div>Binding to getter/setter (works)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.gainValue'>
</div>
<div>
<div>Binding directly to <code>model.audioNode.gain.value</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.audioNode.gain.value'>
</div>
<div>
<div>Binding to <code>reactive(model.audioNode.gain)</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.reactiveWrapper.value'>
</div>
<div>
<div>Binding to <code>ref(model.audioNode.gain.value)</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.refWrapper.value'>
</div>
</script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id='mount'></div>

现在我尝试在SFC(单文件组件(中使用组合API。我也有同样的问题,但这次的解决方案甚至需要更多样板。不幸的是,这不可能是一个可运行的片段,所以你必须相信我的话,就像上面的例子一样,试图在AudioParam上使用reactive是行不通的:

<script setup>
import { reactive, computed } from "vue";
let audio = new AudioContext();
let volume = audio.createGain(); // I want to bind to this
// this doesn't work
let reactiveGain = reactive(volume.gain);
// this also doesn't work
const gainComputed = computed({
get: () => volume.gain.value,
set: val => volume.gain.value = val,
})
// This does.
// Is it possible to achieve this effect without all this boilerplate?
// Also, such that I can still use `v-model` in my template?
class Model {
set gain(value) { volume.gain.value = value; }
get gain() { return volume.gain.value; }
}
let model = reactive(new Model());
</script>
<template>
<div>
Volume: {{ model.gain.toFixed(2) }}
</div>
<div>
This works
<input type="range" min="0" max="1" step=".01" v-model="model.gain">
</div>
<div>
This doesn't
<input type="range" min="0" max="1" step=".01" v-model="reactiveGain.value">
</div>
<div>
This doesn't
<input type="range" min="0" max="1" step=".01" v-model="gainComputed">
</div>
</template>

有没有更好的方法可以被动绑定到Vue之外的对象?

这种绑定的问题是它在一个方向上工作,可以在Vue模型更改时更新原始volume.gain.value,但当volume.gain类实例内部更改value时,不可能更新Vue模型。假定volume.gainAudioParam的实例,则value可以基于定义的约束(例如minValue(而改变。这将需要用扩展的反应性感知类来替换这些本地类,这取决于特定的类是否可能以及如何做到这一点。volume.gain是只读的,在AudioParamMyAudioParam的完全反应实例替换gain的情况下,需要从AudioContext开始扩展整个类层次结构。

Vue 3为类别的反应性提供了有限的支持。reactive将自己的可枚举属性转换为反应属性。不能期望其他成员自动受到影响。reactive可能会对那些没有特别考虑到它的类产生破坏性影响。

第三方类可以以任意的方式实现,只要它适合它们的正常使用,而它们与reactive的使用不能被视为一种,它们提供反应性的能力需要通过反复试验来确定。

很明显,volume.gain.value的原生行为已经依赖于get/set来使AudioParam对值的变化做出反应,因此value不是自己的可枚举属性,而是AudioParam.prototype上的描述符。最初的实现不起作用,因为volume.gain.value在这两种情况下都不是反应性的,并且它的更改无法导致视图更新。reactive不应该在它上使用,因为它无助于使value成为被动的,但可能会对基于其实现的类实例产生负面影响。

reactive(new Model())不需要类。Vue反应性主要设计用于普通物体。

考虑到绑定是单向的(模型值更改会更新其后面的本机值,但反之亦然(,因此明确定义本地状态并在更改时更新本机值是正确的。如果这是一种常见的情况,它可以帮助减少样板:

let createPropModel = (obj, key) => {
let state = ref(obj[key]);
watch(state, v => { obj[key] = v }, { immediate: true });
return state;
};
let gainRef = createPropModel(volume.gain, 'value');

或者,这可以通过可写计算来完成,但也应该涉及本地状态,并且实现不那么简单:

let createPropModel = (obj, key) => {
let state = ref(obj[key]);
return computed({
get: () => state.value,
set: (v) => { state.value = v; obj[key] = v },
});
};
let gainRef = createPropModel(volume.gain, 'value');

好吧,我能想到的最好的就是这个。我会把它作为我问题的潜在答案发布,但在我接受它之前,要等着看是否有人有更好的东西

我创建了一个用于包装AudioParam:的类

import { reactive } from "vue";
export function wrapReactive(obj, field) {
return reactive({
set value(value) { obj[field] = value; },
get value() { return obj[field]; }
});
}
<script setup>
import { wrapReactive } from "./wrapReactive.mjs";
let audio = new AudioContext();
let volume = audio.createGain();
let gain = wrapReactive(volume.gain, 'value');
</script>
<template>
<div>
Volume: {{ gain.value.toFixed(2) }}
</div>
<div>
<input type="range" min="0" max="1" step=".01" v-model="gain.value">
</div>
</template>

最新更新