gpt4 book ai didi

vue.js - Vue 绑定(bind)到外部对象

转载 作者:行者123 更新时间:2023-12-05 04:25:24 27 4
gpt4 key购买 nike

我正在尝试使用 Vue 作为一个非常薄的层来将现有模型对象绑定(bind)到 View 。

下面是一个说明我的问题的玩具应用程序。我有一个 GainNode对象,来自网络音频 API。我想绑定(bind)它的 value到 slider 。

这在 Angular 中是微不足道的。双向绑定(bind)适用于任何对象,无论是否是 Angular 组件的一部分。有没有办法在 Vue 中做类似的事情?

在实际应用中,我有大量以编程方式生成的对象。我需要将它们绑定(bind)到一个组件,例如<Knob v-for='channel in channels' v-model='channel.gainNode.gain.value'> .

更新:我使用的是解决方法 #2(如下),它似乎工作得很好,直到我尝试 v-model - 将两个组件绑定(bind)到相同的音频参数。然后它就无法正常工作,以我无法调试的完全神秘的方式。我最终放弃了,正在使用 getters/setters,这是更多的样板文件,但具有优势,你知道......实际工作。

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'>
<div>
model.audioNode.gain.value: {{model.audioNode.gain.value}}
</div>
<hr>
<div>
<div>Binding to getter/setter for <code>model.audioNode.gain.value</code> (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 through <code>model.reactiveWrapper</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.reactiveWrapper.value'>
</div>
<div>
<div>Binding through <code>model.refWrapper</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.refWrapper.value'>
</div>
<div>
<div>Binding through <code>dataAliasAudioNode.gain.value</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='dataAliasAudioNode.gain.value'>
</div>
</script>

<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id='mount'></div>


问题附录#1:在探索方法时,我发现(如前所述)如果我绑定(bind)到外部对象的嵌套部分(GainNode 网络音频API)它不是 react 性的,但如果我自己构建一个类似的外部对象,绑定(bind)到嵌套参数 react 性的。这是示例代码:

// 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'>
<div>
model.xaudioNode.gain.value: {{model.xaudioNode.gain.value}}
</div>
<div>
model.audioNode.gain.value: {{model.audioNode.gain.value}}
</div>
<hr>
<div>
<div>Binding to <code>model.xaudioNode.gain.value</code> works.</div>
<input type='range' min='0' max='1' step='.05' v-model='model.xaudioNode.gain.value'>
</div>
<div>
<div>Binding to <code>model.audioNode.gain.value</code> doesn't. Why?</div>
<input type='range' min='0' max='1' step='.05' v-model='model.audioNode.gain.value'>
</div>
</script>

<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id='mount'></div>


解决方法#1:

因此,经过更多探索后,我提出了一种解决方法,可以减少 getter/setter 的样板文件。我要么:

  1. 创建我自己的版本 ref (不知道 Vue.ref 不起作用)或
  2. Proxy对象并调用 $forceUpdate当调用 setter 时。

这两种方法都有效,缺点是我必须将代理公开为成员并绑定(bind)到,而不是原始对象。但这比同时暴露 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'>
<div>
<div>model.audioNode.gain.value: {{model.audioNode.gain.value}}</div>
<div>model.wrapper.value: {{model.wrapper.value}}</div>
<div>model.proxy.value: {{model.wrapper.value}}</div>
</div>
<hr>
<div>
<div>Binding directly to <code>model.audioNode.gain.value</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.05' v-model='model.audioNode.gain.value'>
</div>
<div>
<div>Binding through <code>model.wrapper.value</code> (works)</div>
<input type='range' min='0' max='1' step='.05' v-model='model.wrapper.value'>
</div>
<div>
<div>Binding through <code>model.proxy.value</code> (works)</div>
<input type='range' min='0' max='1' step='.05' v-model='model.proxy.value'>
</div>
</script>

<div id='mount'></div>

<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>


解决方法 #2:

另一种解决方法是修补我要监视的访问器并调用 $forceUpdate在里面。这具有最少 样板。我只是打电话watch(obj, prop)并且该属性变为响应式(Reactive)。

根据我的口味,这是一个相当可接受的解决方法。但是,当我开始将内容移动到子组件中时,我不确定这些变通方案的效果如何。接下来我要试试看。我也不明白为什么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'>
<div>
<div>model.audioNode.gain.value: {{model.audioNode.gain.value}}</div>
</div>
<hr>
<div>
<div>Binding directly to <code>model.audioNode.gain.value</code> with custom `watch` (works)</div>
<input type='range' min='0' max='1' step='.05' v-model='model.audioNode.gain.value'>
</div>
</script>

<div id='mount'></div>

<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

最佳答案

你的问题很令人兴奋,所以我决定花几个小时找出答案。

长见识

  • 内置对象(由浏览器 API 创建的对象)无法转换为 react 形式,因此改变其属性不会触发重新渲染
  • Vue 不是完全选择性的重新渲染,所以当它重新渲染模板时,一些甚至不是 react 性的 block 也会被更新。

让我们总结一下这个问题:

  • 当改变 Web Audio API 对象的属性时,响应式(Reactive)不起作用(实际上任何内置对象都是相同的)
  • 当在 setter 中改变相同的属性时,响应式(Reactive)确实起作用

说明

首先,我们需要知道当 Vue 在模板上渲染一个值时会发生什么?让我们考虑一下这个模板:

{{ model.audioNode.gain.value }}

如果model是响应式(Reactive)对象(由 reactiverefcomputed ... 创建),Vue 将创建一个 getter 将链上的每个对象转换为响应式(Reactive)。因此,这些以下对象将使用 Vue.reactive 转换为 react 形式功能:model.audioNode , model.audioNode.gain

但只是一些可以转换为 react 对象的类型。这是 the code from Vue reactive package

function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}

正如我们所见,键入除 Object 以外的其他类型, Array , Map , Set , WeakMap , 和 WeakSet将无效。要知道你的对象是什么类型,你可以调用yourObject.toString() (what Vue is actually using)。任何不修改 toString 的自定义类方法将是 Object类型并且可以是 react 性的。在您的示例代码中 modelobject类型,model.audioNode类型是 GainNode .所以它不能被转换成一个 react 对象并且改变它的属性不会触发 Vue 重新渲染。

那么为什么 setter 方法有效

它实际上不起作用。让我们考虑一下这个片段:

class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8;
}

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 for <code>model.gainValue</code> (does NOT work)</div>
<input type='range' min='0' max='1' step='.1' :value="model.audioNode.gain.value" @input="model.gainValue=$event.target.value">
</div>
</script>

<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id='mount'></div>

上面代码片段中的 setter 不起作用。让我们考虑另一个片段:

class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8;
}

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 for <code>model.gainValue</code> (does work)</div>
<input type='range' min='0' max='1' step='.1' :value="model.gainValue" @input="model.gainValue=$event.target.value">
</div>
</script>

<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id='mount'></div>

<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id='mount'></div>

上面代码片段中的 setter 确实有效。看看那行 <input type='range' min='0' max='1' step='.1' :value="model.gainValue" @input="model.gainValue=$event.target.value">这实际上是您使用 v-model="model.gainValue" 时发生的情况.它起作用的原因是行 :value="model.gainValue"将随时触发 Vue 重新渲染 model.gainValue已更新。并且 Vue 不是完全选择性的重新渲染。 所以当整个模板被重新渲染时 block {{ model.audioNode.gain.value }}也会重新渲染。

为了证明 Vue 不是完全选择性的重新渲染,让我们考虑这个片段:

class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8;
}

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,
anIndependentProperty: 1
}
},
methods: {
update(event){
this.model.audioNode.gain.value = event.target.value
this.anIndependentProperty = event.target.value
}
}
});
app.mount('#mount');
<script type='text/x-template' id='AppView'>
<div>
model.audioNode.gain.value: {{model.audioNode.gain.value}}
</div>
<div>
anIndependentProperty: {{anIndependentProperty}}
</div>
<hr>
<div>
<div>anIndependentProperty trigger re-render so the template will be updated</div>
<input type='range' min='0' max='1' step='.1' :value="model.audioNode.gain.value" @input="update">
</div>
</script>

<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id='mount'></div>

在上面的例子中 anIndependentProperty是响应式(Reactive)的,它会在更新时触发 Vue 重新渲染。当 Vue 重新渲染模板 block 时 {{model.audioNode.gain.value}}也会更新。

解决方案

此解决方案仅适用于在模板中使用属性的情况。如果你想使用 computed从您的类属性中,您必须使用 setter/getter 方法滚动。

class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8;
}
}
let appModel = new MyApp();

let app = Vue.createApp({
template: '#AppView',
data() {
return {
model: appModel,
reactiveControl: 0
}
},
});
app.mount('#mount');
<script type='text/x-template' id='AppView'>
<input type="hidden" :value="reactiveControl">
<div>
<div>Binding to <code>model.audioNode.gain.value (works):</code> {{model.audioNode.gain.value}} </div>
<input type='range' min='0' max='1' step='.1' :value="model.audioNode.gain.value" @input="model.audioNode.gain.value=$event.target.value; reactiveControl++">
</div>
<div>
<div>Binding to other property <code>model.audioNode.channelCount (works):</code> {{model.audioNode.channelCount}}</div>
<input type='range' min='1' max='32' step='1' :value="model.audioNode.channelCount" @input="model.audioNode.channelCount=$event.target.value; reactiveControl++">
</div>
You can bind to any property now...
</script>

<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id='mount'></div>

请注意这一行:

<input type="hidden" :value="reactiveControl">

每当reactiveControl变量变化,模板会更新,其他变量也会更新。所以你只需要改变reactiveControl的值每当您更新类属性时。

关于vue.js - Vue 绑定(bind)到外部对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73285075/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com