你的问题很令人兴奋,所以我决定花几个小时来找出答案。
TLDR
- 内置对象(由浏览器 API 创建的对象)无法转换为响应式形式,因此改变其属性不会触发重新渲染
- Vue 不是完全选择性的重新渲染,因此当它重新渲染模板时,一些甚至不具有反应性的块也会被更新。
让我们总结一下这个问题:
- 当改变 Web Audio API 对象的属性时,反应式不起作用(实际上任何内置对象都是相同的)
- 当在 setter 中改变相同的属性时,反应式确实起作用
解释
首先,我们需要知道当 Vue 在模板上渲染值时会发生什么?让我们考虑一下这个模板:
{{ model.audioNode.gain.value }}
If model
是一个响应式对象(它是由reactive
, ref
, or computed
...),Vue 将创建一个 getter,将链上的每个对象转换为响应式对象。因此,以下这些对象将使用以下方法转换为反应形式:Vue.reactive
功能:model.audioNode
, model.audioNode.gain
但只有一些类型可以转换为响应式对象。这是来自 Vue 反应包的代码 https://github.com/vuejs/core/blob/main/packages/reactivity/src/reactive.ts#L43
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
, and WeakSet
将无效。要知道您的对象是什么类型,您可以调用yourObject.toString()
(Vue 实际使用的是什么 https://github.com/vuejs/core/blob/main/packages/shared/src/index.ts#L67)。任何不修改的自定义类toString
方法将是Object
类型并可以使其成为反应性的。在你的示例代码中model
is object
type, model.audioNode
是类型GainNode
。因此它不能转换为响应式对象,并且改变其属性也不会触发 Vue 重新渲染。
So 为什么 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>
上面代码片段中的设置器不起作用。让我们考虑另一个片段:
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>
上面代码片段中的设置器确实有效。看看那条线<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 并不是完全选择性重新渲染。所以当整个模板重新渲染块时{{ 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
是反应性的,每当更新时都会触发 Vue 重新渲染。当Vue重新渲染模板块时{{model.audioNode.gain.value}}
也会更新。
Solution
此解决方案仅适用于使用模板中的属性的情况。如果您想使用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
每当您更新类属性时。