在 《Clean Architecture》裡第 6 章介紹 functional programming ,有提到一個很重要的觀念 - 不可變動性 (immutable)。
這是什麼意思呢?在 JavaScript 中,對於複雜型別 (常見的型別是 Object 或 Array) 的修改,會選擇替換掉物件本身,就能做到 immutable。
那這件事和 watch 有什麼關係呢?
在 Vue.js 的畫新畫面邏輯中「資料改變,畫面就改變」,要怎麼樣才是「正確的資料改變」呢?
在官網文件中的 API 介紹中,有完整的 watch 說明。
在此將它的介紹拆成兩個部份,會有助於觀念上的釐清。
對於四種簡單型別的 watch 方式。
第一種就是直接用一個和 data 同名的 watch 。讓資料在 set 的同時,可以呼叫一次這個放在 watch 的 function
export default {
data: {
a: 1
},
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
}
}
}
第二種,就是將 method 放到 watch 上面,因為有時候有些 method 會主動觸發,有時又要被動觸發,就必須這樣安排,不過另一種做法也可以放在第一種方法的 function 中再呼叫 someMethod
export default {
data: {
b: 2
},
watch: {
// string method name
b: 'someMethod',
},
methods: {
someMethod() {
// do something...
}
}
}
第三種做法,watch 裡放的就不只是 method 了。而是一個 config ,註冊了一個 handler 並且設定它要 deep:true
的這樣觸發。
第四種做法就出現 immediate
這個關鍵字,並且也是設定成 true。
export default {
data: {
c: 3,
d: 4
},
watch: {
// the callback will be called whenever any of the watched object properties change regardless of their nested depth
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// the callback will be called immediately after the start of the observation
d: {
handler: 'someMethod',
immediate: true
}
}
}
最後一做法,在範例程式碼中,有出現較深的資料結構。
如果 watch 較深的資料時,可以直接用 e.f
來監聰深度的結構改變。
export default {
data: {
e: {
f: {
g: 5
}
}
},
watch: {
// you can pass array of callbacks, they will be called one-by-one
e: [
'handle1',
function handle2 (val, oldVal) { /* ... */ },
{
handler: function handle3 (val, oldVal) { /* ... */ },
/* ... */
}
],
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
}
let user = {
name: 'Chris',
age: 18,
};
畫成記憶體圖,如下圖 (假設 JavaScript 是以 call by sharing 實作的話)
修改的話,可以畫成下圖,接下來就來解釋兩種修改方式。
如果是 mutable (可變的,就直接改掉 user 的成員變數所指向的位置),將 user.age
改成 19 的動作,就是指向 0xFF0400
指向 0xFF0408
。
程式碼如下
let user = {
name: 'Chris',
age: 18,
};
user.age = 19
對於修改記憶體位址的位置來說,就不是 user
被替換,而是 user.age
的位置被替換。
如果要 immutable 就是換掉 user 為目標。(修改的是藍色線的指向,要指向新的一個物件),將 user
改成 另一個 object 的動作,就是指向 0xFF0200
指向 0xFF0210
。
程式碼如下
let user = {
name: 'Chris',
age: 18,
};
user = {
name: 'Chris',
age: 19,
}
對於修改記憶體位址的位置來說,就是 user
被替換。
把上述的例子,拿來 vue 裡面放,就像這樣,也許會放在 vuex 不過意思差不多就是這樣。
有個 data 裡面有一個深一點的資料結構,也許是 object 也許是 array (在此用 object 當例子)
export default {
data () {
return {
user {
name: 'Chris',
age: 18
}
}
}
}
Vue.js 可以監聽的範圍,基本上只要是 return {}
這一層裡面的變數 watch 都可以正常連動。以這個例子來說,就是修改 user 就可以正常連動,但是有時候我們只是想要修改 age 怎辦?
就可以準備一個已經改好的物件,{ name: 'Chris', age: 19}
來替換掉 user 原本的物件就可以了。這樣一來,對 user 來說就不是修改,而是替換,也可以讓 vue.js 保證 watch 可以連動,不需要查詢這麼特別又複雜的使用方式。
export default {
data () {
return {
user {
name: 'Chris',
age: 18
}
}
},
watch: {
user() {
console.log('update other data');
}
}
methods: {
updateUserAge() {
this.user = {
...this.user,
age: 19
}
}
}
}
updateUserAge
就是一個簡單的做法,讓你在執行時, watch 的部份保證會連動到。Vue.js 會查覺到記憶體位址修改,所以要更新畫面。
有時候,vuex 的資料結構太深,mutate 只修改成員變數時,也會造成無法正確觸發 getters 的問題,導致畫面沒有正確更新
const store = new Vuex.Store({
state: {
user: {
name: 'Chris',
age: 18
}
},
mutations: {
userName (state, { name }) {
// 這樣可能 (我是說可能) 就會出現問題
state.user.name = name
}
}
})
但是,只要保持 immutable 的觀念,讓更新。在官網的介紹中,叫 Reactivity Rules
const store = new Vuex.Store({
state: {
user: {
name: 'Chris',
age: 18
}
},
mutations: {
userName (state, { name }) {
// 這樣一定可以更新
state.user = {
...state.user,
name
}
}
}
})
你好~想請教一下上述 user 物件的記憶體位置指派行為,如果我把 user.age 跟 user1.age都指派 19 ,那麼是兩個屬性上的記憶體位置都會指向 0xFF0408 嗎?
由於 JavaScript 的規範 ECMA 並沒有規定 JavaScript 的實作方式。參考自深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
這個問題可能會因為 JavaScript 引擎是 v8、Extreme 和 TraceMonkey 的不同,而所有不同。
我個人偏好使用 call by sharing 來解釋 JavaScript 的行為,所以如果都指派 19 ,就是會指向同一個 0xFF0408,就像都指派 'Chris' 一樣,都指向 0xFF0300
如果是另一種,看型別決定 call by value 還是 call by reference 的解釋,則會因為它是 primitive type 會直接寫在 user1.age
或 user.age
裡,所以就不會共用同一個 19
Spark 還是,其實是因為簡單型別,所以是不一樣的位址?