現在前端框架和之前的前端開發方式有一個重要的區別————基於資料驅動。我們不需要再去關注dom本身,而是將主要精力放在如何運算元據上面。實際開發中,可以抽象成
既然全部在完資料, 資料型別、演算法就跑不掉了。
本片介紹一個基於引用型別的vue黑科技, 在使用vue開發的時候可以更加方便。
引用型別
首先, 抄一段別人的部落格
1.JavaScript中的變數型別有哪些?
(1)值型別:字串(string)、數值(number)、布林值(boolean)、none、undefined
(2)引用型別:物件(Object)、陣列(Array)、函式(Function)
2.值型別和引用型別的區別
(1)值型別:1、佔用空間固定,儲存在棧中(當一個方法執行時,每個方法都會建立自己的記憶體棧,在這個方法內定義的變數將會逐個放入這塊棧記憶體裡,隨著方法的執行結束,這個方法的記憶體棧也將自然銷燬了。因此,所有在方法中定義的變數都是放在棧記憶體中的;棧中儲存的是基礎變數以及一些物件的引用變數,基礎變數的值是儲存在棧中,而引用變數儲存在棧中的是指向堆中的陣列或者物件的地址,這就是為何修改引用型別總會影響到其他指向這個地址的引用變數。)
2、儲存與複製的是值本身
3、使用typeof檢測資料的型別
4、基本型別資料是值型別
(2)引用型別:
1、佔用空間不固定,儲存在堆中(當我們在程式中建立一個物件時,這個物件將被儲存到執行時資料區中,以便反覆利用(因為物件的建立成本通常較大),這個執行時資料區就是堆記憶體。堆記憶體中的物件不會隨方法的結束而銷燬,即使方法結束後,這個物件還可能被另一個引用變數所引用(方法的引數傳遞時很常見),則這個物件依然不會被銷燬,只有當一個物件沒有任何引用變數引用它時,系統的垃圾回收機制才會在核實的時候回收它。)
2、儲存與複製的是指向物件的一個指標
3、使用instanceof檢測資料型別
4、使用new()方法構造出的物件是引用型
複製程式碼
基礎知識就是這些了。 舉個小栗子。
總之記住一句話, 值型別傳值, 引用型別傳址vue中的資料傳遞
在vue中的資料傳遞過程中, 是不涉及深拷貝的。 是通過props、vuex、v-bind等方法傳遞的引用型別都是傳遞的記憶體指標 在來個小栗子
<template>
<div class="content">
<ul>
<li v-for="(item, index) in list" :key="index" >
<span @click="changeItemValue(item)">{{item.title}}</span>
<span @click="deleteItem(index, list)">刪除</span>
</li>
<li @click="addItem(list)">新增item</li>
</ul>
</div>
</template>
<script>
export default {
name: 'index',
data () {
return {
list: [
{
title: 'index0',
},
{
title: 'index1'
},
{
title: 'index2'
},
{
title: 'index3'
},
{
title: 'index4'
}
]
}
},
methods: {
changeItemValue(item) {
item.title = item.title += '--|'
},
deleteItem(index, list) {
list.splice(index, 1)
},
addItem(list) {
list.push({
title: 'index' + list.length
})
}
}
}
</script>
<style lang="scss" scoped>
ul {
width: 500px;
margin: 200px auto;
li {
width: 200px;
height: 50px;
line-height: 50px;
border: 1px solid pink;
span:nth-child(2){
margin-left: 10px;
color:red;
}
}
}
</style>
複製程式碼
在上面的例子中, 我們都沒有使用this.list[index]
的方式來獲取需要修改的物件,
實際上, 方法裡面傳入的item就是this.list
裡對應的item的引用
這樣書寫會比通過傳入索引--> 通過索引在list尋找該物件--> 修改該物件要方便的多。
特別是在資料層級比較深的時候。通過索引來查詢可能會出現
changeItemValue(itemAIndex, itemBIndex, itemCindex, itemDIndex, value) {
this.list[itemAIndex].childList[itemBIndex].childList[itemCindex].childList[itemDIndex].title = value
}
複製程式碼
這酸爽~
我們舉一個引用的小場景
頁面的列表中, 每一個item有一個開關, 需要在儲存的時候取出全部全部選中的<template>
<div class="content">
<ul>
<li v-for="(item, index) in list" :key="index" >
<span>{{item.title}}</span>
<span @click="changeItemValue(item)">{{item.isSelect ? '選中' : '未選中'}}</span>
</li>
<li @click="save">儲存</li>
</ul>
</div>
</template>
<script>
export default {
name: 'index',
data () {
return {
list: [
{
title: 'index0',
isSelect: false
},
{
title: 'index1',
isSelect: false
},
{
title: 'index2',
isSelect: false
},
{
title: 'index3',
isSelect: false
},
{
title: 'index4',
isSelect: false
}
]
}
},
methods: {
changeItemValue(item) {
item.isSelect = !item.isSelect
},
save() {
const data = this.list.filter(_ => _.isSelect)
console.log(data)
}
}
}
</script>
複製程式碼
這得益於vue的訪問劫持方法, 在修改物件的時候, 可以直接觸發物件的觀察者, 觸發資料的更新和各種watch、computed、UI。 而vue的陣列型別則是由vue特殊處理過的,才能實現對push、splice等方法的更新,這部分可以翻翻vue原始碼。
接下來我們講一講通過props的方式向子元件傳遞的情況, 眾所周知,vue是不允許在元件內修改通過props傳入的值的。實際中呢:
如果傳入的資料是值型別的, 那麼不允許修改這個值 例如 this.string = ''
如果傳入的資料是引用型別, 那麼不允許修改這個資料的記憶體地址,反之呢,我們可以修改這個資料中的子資料
複製程式碼
感覺上這種操作是違反vue的單向資料流思想的, 但是實在是在開發中太好用了, 所以我只能說這是一種黑科技 來個例子, 我們修改一下上面的程式碼, 將li作為一個元件來管理一個物件
<template>
<div class="content">
<ul>
<Item v-for="(item, index) in list" :key="index" :item="item" />
<li @click="save">儲存</li>
</ul>
</div>
</template>
<script>
import Item from './Item'
export default {
name: 'index',
components: { Item },
data () {
return {
list: [
{
title: 'index0',
isSelect: false
},
{
title: 'index1',
isSelect: false
},
{
title: 'index2',
isSelect: false
},
{
title: 'index3',
isSelect: false
},
{
title: 'index4',
isSelect: false
}
]
}
},
methods: {
save() {
const data = this.list.filter(_ => _.isSelect)
console.log(data)
}
}
}
</script>
複製程式碼
<template>
<li>
<span>{{item.title}}</span>
<span @click="changeItemValue">{{item.isSelect ? '選中' : '未選中'}}</span>
</li>
</template>
<script>
export default {
name: 'Item',
props: ['item'],
methods: {
changeItemValue() {
this.item.isSelect = !this.item.isSelect
// 注意 如上面所說 在這裡直接修改item就會報錯, 反之 只修改item下面的值並不會
}
}
}
</script>
複製程式碼
執行起來, 和之前並沒有差異, 實際上props傳進去的也是這個物件的引用, 修改的時候父元件的值也被同步修改了。這樣我們可以在子元件裡面修改對應的值, 而不需要$emit到父元件去修改。在處理複雜資料的時候, 可以減少很多負擔 基於這種模式, 我們在處理一個複雜資料的編輯的時候, 就可以將每一塊相對獨立的子資料分別用元件去維護。 而且子元件的資料相對對立, 層級淺的時候, 我們還可以方便的使用computed計算屬性來實現一些資料的校驗,UI的處理。
在使用vuex的時候
在使用vudex做狀態管理的時候, 情況和pros差不多。
如果繫結的的資料是值型別的, 那麼不允許修改這個值 例如 this.string = ''
如果繫結的的資料是引用型別, 那麼不允許修改這個資料的記憶體地址,反之呢,我們可以修改這個資料中的子資料
複製程式碼
但是有一點, 在使用vue-devtools工具的中會有點差異,簡單來說通過這種方式修改了state中的值,在vue-devtools工具的vuex部分是不會更新的, 但是實際上資料是已經改變了。。 依舊是先前的那個例子, 我們將資料來源從data改為vuex
computed: {
list() {
return this.$store.state.list
}
}
複製程式碼
我們通過這種方式改變值之後,
在組建檢視, 我們能看到組建內的isSelect值已經更新了, 父元件的計算屬性中 第一個物件的值也更新了 但是在vuex檢視中 這個值沒有被更新, 列印出來的值也是更新了的。。 如果有強迫症的話, 可以手動更新一下 mutations: {
changeList(state, list) {
state.list = list
}
},
this.$store.commit('changeList', this.$store.state.list)
複製程式碼
應用
基於這種方法, 我們在處理複雜資料的時候, 可以將相對獨立的資料塊分割出來用一個單獨的vue元件來維護和修改。最後的修改結果都可以在原有的資料樹中體現,在提交的時候對這個跟資料進行處理就好。而不用每一次修改都emit到父元件中處理。
注意事項
- 基於這種值引用的形式, 在子元件修改相應值的時候, 初始值其實已經被汙染了, 所以有需要的話要做資料的深拷貝
- 在處理陣列的時候, vue底層對響應的陣列操作都有特殊處理過, 所以只要不直接修改陣列的引用地址, 都可以觸發資料的更新, 但是不能使用tihs.list = this.list.map(cb)類似的方法, 因為他們都會返回一個新的陣列
- 計算屬性和vuex的getter返回的值不能這樣處理, 準確的說, 這兩個值本身就不能修改, 但是通過計算屬性返回vuex的值例外
- 在直接修改物件子值的時候, watch會有異常, 無法正確的獲得oldVal的值。
- 在react中也可以類似的實現, 但是react不是基於資料訪問劫持的, 所以修改之後還要手動state一次, 微信小程式同理
- 在微信小程式中, 寫在模板中的函式是不能傳參的,通過data寫在dom上的值不能這麼操作, 組建傳入的值也是相當於深拷貝的, 不能這麼玩了
如上圖, 計算屬性的值正常更新了, 通過deep watch的值, 兩個都是新的值, 無法取得oldVal, 而不用deep的時候, 這個watch根本不會觸發。
寫在最後
通過這種方式, 在處理比較複雜的資料的時候有奇效, 但是隱隱約約還是有些怪異,表面穩如老狗,實際慌得不行。 也請大佬解惑
- 這樣處理是不是違反了單向資料流的思想
- 會不會有其他的未知的隱患
10/16 第一次更新
大家都在說複雜的引用型別難以維護的情況,我不得不吐槽一下了。。 我們經常拿到的資料是這樣的
[
{
"id": 592,
"catalogueCode": "catalogueCode",
"catalogueRule": "catalogueRule",
"catalogueName": "catalogueName",
"days": "3",
"expectedDate": null,
"groups": [
{
"groupName": "groupName",
"subsets": [
{
"items": [
{
"catalogueRule": "catalogueRule",
"ruleId": 1,
"ruleName": "ruleName",
"catalogueCode": "catalogueCode",
"ruleScore": 2
},
{
"catalogueRule": "catalogueRule",
"ruleId": 77,
"ruleName": "ruleName",
"catalogueCode": "catalogueCode",
"ruleScore": 2
}
]
}
]
}
],
"goals": [
{
"goalId": 642,
"catalogueName": "catalogueName",
"catalogueRule": "catalogueRule",
"catalogueCode": "catalogueCode",
"remark": null,
"resultId": 592,
"sortNum": null,
"measures": [
{
"measureId": 2541,
"catalogueCode": "catalogueCode",
"catalogueRule": "catalogueRule",
"catalogueName": "catalogueName",
"customizeId": null,
"sort": 0,
"checked": true,
"shortActivityMap": {
"key1": [
{
"catalogueName": "catalogueName",
"catalogueRule": "catalogueRule",
"catalogueCode": "catalogueCode",
"resultId": 2541,
"longActivityList": [],
"specialSecondType": "3",
"frequencyName": "bid",
"executionTime": "08:00,16:00",
"shortActivityId": 82
}
],
"key2": [
{
"catalogueName": "catalogueName",
"catalogueRule": "catalogueRule",
"catalogueCode": "catalogueCode",
"resultId": 2541,
"longActivityList": [],
"specialSecondType": "3",
"frequencyName": "bid",
"executionTime": "08:00,16:00",
"shortActivityId": 82
}
]
}
}
],
"appraisals": [
{
"appraisalId": 2048,
"catalogueName": "catalogueName",
"catalogueRule": "catalogueRule",
"catalogueCode": "catalogueCode",
"remark": null,
"resultId": null,
"targetCode": "targetCode",
"sortNum": null
}
]
}
],
"totalScore": 4
},
]
複製程式碼
這個層級相對還是比較少的, 最深資料接近6層。關鍵在於每一個資料節點都是有對應的增刪改的編輯需求, 並且不能分佈提交。 那麼這個頁面的編輯。怎麼處理?
- 方案一: 完全不元件化, 一把撈
- 方案二: 按照資料節點拆分成子元件, 通過props傳遞資料,每一個修改動作都一路emit到根元件進行。
乍一看起來完全OK, 但是位於第六層的資料要向上傳遞多少次呢。 對應的索引有多少? 根元件每一個層級的資料都要寫一個增刪改方法, 根元件又亂掉了 - 方案三: 將全部資料維護到vuex處理, 所有修改資料的方法使用commit在vuex中修改
這種方法,實際上是吧方案二的根元件資料處理搬到的vuex中, 並且省下了emit和props, 綜合起來算是比較好的方案了。 很遺憾的是, 同事不接受。。 - 方案四: 就是上文提到的方法。
關於維護: 在vue的谷歌開發外掛中是可以完整的看到資料的流向、改變的 每個一個元件維護了什麼,修改了什麼是和資料結構完全對應,並在當前清晰可見,維護起來起來應該還ok?
不過如評論所說, 如果vue認為修改props內部資料是缺陷或者是BUG,以後會修復的話那,那毫無疑問我要加班了。。
第二次寫在最後
我用的這種方法, 我覺得不踏實,我寫出來和大家一起討論,拋磚引玉, 希望有大佬能指點或者分享經驗就是我的目的, 評論一上來就開罵是咋回事。