Vue: Reactivity Fundamentals (Options API)
2022-03-15
1. Declaring Reactive State
使用 Options API 的话,我们用 data
option 来定义组件的 reactive state。
Option value 必须是一个 return an object 的 function。
当创建新的组件 instance 的时候,Vue 会调用这个 function,然后把 return object 加入到它的 reactivity 系统里面。
这个 object 的所有 top-level property 都会附加在组件上,可以用 this
来使用。
Any top-level properties of this object are proxied on the component instance (
this
in methods and lifecycle hooks)
export default {
data() {
return {
count: 1,
};
},
// `mounted` is a lifecycle hook which we will explain later
mounted() {
// `this` refers to the component instance.
console.log(this.count); // => 1
// data can be mutated as well
this.count = 2;
},
};
Lifecycle:property 什么时候加入 instance
这些 properties 只有当 instance 首次被创建的时候才会加进去,所以我们要确保它们都在 data
function 返回的那个 object 里面。
假如我们不确定 value 的话,可以用 null,undefined 或者其他 placeholder value 来给 property 赋值。
不加入 data
function 可以吗?
可以直接添加一个 component property,但是这种方式添加的 property 无法触发 reactive updates。
Naming convention
避免使用 $
和 _
来命名 top-level data
properties,因为它们是 reserved character。
Vue uses a
$
prefix when exposing its own built-in APIs via the component instance.It also reserves the prefix
_
for internal properties.
Reactive Proxy vs. Original
Vue 3 的 reactive data 是通过 JS proxies 来做的,所以 Vue 2 的一些 edge case(如下)在 Vue 3 里面不会出现。
export default {
data() {
return {
someObject: {},
};
},
mounted() {
const newObject = {};
this.someObject = newObject;
console.log(newObject === this.someObject); // false
},
};
解释:当你在赋值之后 access this.someObject
时,这个 value 是原始 newObject
的一个 reactive proxy,原始的 newObject 不会被改变。
所以我们应该记住要通过 this
的 property 来 access reactive state。
When you access
this.someObject
after assigning it, the value is a reactive proxy of the originalnewObject
. Unlike in Vue 2, the originalnewObject
is left intact and will not be made reactive: make sure to always access reactive state as a property ofthis
.
2. Declaring Methods
我们可以在 vue option 里面 define methods。
在 template 里面可以直接 access data option 的 property,但是在 methods 里面必须加 this.xxx.
Note: methods cannot be defined as arrow functions, as that prevents Vue from binding the appropriate this
value.
export default {
methods: {
increment: () => {
// BAD: no `this` access here!
},
},
};
Example
我们可以在 template 里面直接 access method,大部分时候这些 method 都是 event listener。
<div>
<button @click="items.push({id:items.length+1, label: newItem})">
Save Item
</button>
<button @click="saveItem">Save Item</button>
</div>
<script>
export default {
data() {
return {
someObject: {},
};
},
methods() {
saveItem(){
this.items.push({id:items.length+1, label: newItem});
this.newItem = "";
},
increment() {
this.count++
}
},
mounted() {
// methods can be called in lifecycle hooks, or other methods!
this.increment()
}
};
</script>
3. DOM 的更新时间
当你改变 reactive state 的时候,DOM 会自动更新,但是 DOM update 并不是同步进行的,Vue 会通过 buffer 来推迟到 update cycle 的下一步才进行更新,这样每个组件只需要更新一次。
Vue buffers them until the “next tick” in the update cycle to ensure that each component needs to update only once no matter how many state changes you have made.
我们可以使用 nextTick()
global API 来进行等待。
import { nextTick } from "vue";
export default {
methods: {
increment() {
this.count++;
nextTick(() => {
// access updated DOM
});
},
},
};
4. Deep Reactivity
Vue 的 state 是默认 deeply reactive 的,也就是说即使改变了 nested objects 或者 array,这种改变也会被 detect 到。
export default {
data() {
return {
obj: {
nested: { count: 0 },
arr: ["foo", "bar"],
},
};
},
methods: {
mutateDeeply() {
// these will work as expected.
this.obj.nested.count++;
this.obj.arr.push("baz");
},
},
};
创建 shallow reactive objects
假如我们只需要 track root-level reactivity,就要用到这个方法,但这个属于高阶用法。
5. Stateful Methods
有时候我们需要动态创建一个 method function,比如创建一个 debounced event handler:
import { debounce } from "lodash-es";
export default {
methods: {
// Debouncing with Lodash
click: debounce(function () {
// ... respond to click ...
}, 500),
},
};
上面这种做法会给 reused component 造成问题,因为 debounced function 是 stateful 的,也就是说它是根据 elasped time 来维持 internal state 的。假如多个 component instance 共享同一个 debounce function,它们之间就会互相干扰。
怎么解决?
在 created
lifecycle hook 里面创建这个 method,在 unmounted 的时候 clean up.
也就是把 stateful 和 lifecycle hook 关联在一起,保证它只跟每个 instance 相关。
export default {
created() {
// each instance now has its own copy of debounced handler
this.debouncedClick = _.debounce(this.click, 500);
},
unmounted() {
// also a good idea to cancel the timer
// when the component is removed
this.debouncedClick.cancel();
},
methods: {
click() {
// ... respond to click ...
},
},
};