使用vue開發已經有一段時間了,本文主要是記錄平時使用過程中踩過的一些坑,以及一些心得。一方面可以自我總結提高,另一方面可以將自己的經驗分享出來。
一、為什么要用框架
現在前端行業發展飛快,我們在選用技術棧的時候,一方面要易於上手,另一方面要適合自己的項目。vue就是這樣一個前端框架,易於上手,有成熟的文檔可以參考、同樣有成熟的社區可以討論問題。最不濟可以閱讀源碼,vue的源碼還是比較易讀的。
為什么我們開發的時候需要使用框架?以前的jquery不好嗎?原生js開發不好嗎?使用這些框架能夠解決哪些痛點?我就來說說我自己的一些淺見。
1、組件化
組件化開發可以解耦,便於復用,我跟人而言組件化是模塊化更進一步的解耦。最初我們在開發前端項目的時候,瀏覽器原生並不支持模塊開發,我們需要使用一些庫來給模塊化的js提供一個運行平台,例如之前的 requireJs、seaJs等。如果你經歷過原始的開發再轉型到模塊化開發,你就會明白模塊化的好處。因為當你在沒有模塊的開發過程當中,你會發現所有的變量都是全局的,所有的方法都是全局的(當然局部變量除外)。當你在你的js文件當中使用了某個變量的時候,你都找不到這個變量是定義在當前的js文件當中還是定義在其他的js文件中,這就很頭疼了。還有就是你引用了別人的js文件,你無法確認你寫的方法是不是覆蓋了別人的方法。當你使用了模塊化開發的時候,你就能通過模塊引入的方式來進行你需要依賴的方法、類、對象、常量、變量的引入。並且模塊中所有定義的方法變量都不是全局的,這樣你會少了很多麻煩。
上面簡要談了談模塊化的好處,模塊化對你的js邏輯來說基本上是夠用了,但是我們前端不僅有js邏輯,還有html、css這些界面樣式。這些東西怎么去解耦呢?怎么擁有自己的局部作用域呢?怎么去復用呢?組件就很好地解決了這個問題。試想一下你需要在頁面上做一個擁有提交功能的按鈕,你需要在html中寫入元素,然后再在js中給這個按鈕添加事件。當你的頁面上有很多這種按鈕的時候,你需要去復制多份,這樣一沒有起到解耦的效果,二無法復用。現在如果瀏覽器廠商特意為你的需求定制了一個標簽,你只需要在你的html文件中使用這個標簽就能完成你所需要的功能,這樣是不是很簡單,如果你需要多份這樣的按鈕,你要做的只是將這個標簽寫到其他地方。當然瀏覽器不可能為你定制這樣一個標簽,所以你需要自己為自己定制這樣一個標簽。這就是組件化的思想。
2、數據綁定
這個可以說是前端三大框架真正能夠解決痛點的地方了,因為前面所說的組件化,瀏覽器馬上就要原生支持 webcomponent 了。在原來的開發模式中我們修改頁面中的某個元素中的value值,我們需要獲取到相應的元素,然后再修改元素的value。獲取元素的過程就是操作DOM的過程,瀏覽器為js提供了操作DOM的接口,前端就是展示頁面,也就是改變html及css。改變html和css需要使用腳本語言才能動態地修改,這就是js存在的重要意義。但是操作DOM的過程是繁瑣的,而數據綁定可以幫我們從操作DOM的過程中解放出來。試想一下我們如果只需要改變 value的值就可以讓頁面發生相應的更改,這樣我們就可以免去操作DOM的麻煩。我們稱這種改變UI的方式為數據綁定。
不管框架是如何實現數據綁定的,我們需要關心的只有數據,因為UI是被數據所驅動的。只要數據發生改變UI就能發生相應的更改。其實細想起來,我們做的UI頁面就是一個html DOM樹,這顆DOM樹完全可以通過內存中的對象來實現數據映射。框架要做的工作就是將內存中的對象與DOM樹發生綁定關系,其實內存中的這個對象就是我們經常看到的虛擬DOM。
二、vue的數據綁定原理
vue這個框架很好地實現了以上兩個優點。我曾經讀過一些vue的源碼,下面說說我對vue發生數據綁定的一些理解,如果掌握了vue的工作原理,我們可以在工作當中很好地解釋一些難以理解的問題,我認為還是很有必要的。
一般來說我們改變了數據瀏覽器並不知道需要重新渲染哪些DOM,瀏覽器只知道當我們顯式地改變了DOM的時候才會去重新渲染。很顯然改變數據然后通知瀏覽器去渲染就是我們的vue起的作用。那么vue是怎么知道我們改變了數據的呢?
第一個問題:vue如何知道我們修改了數據?
其實vue並不是萬能的,它並不知道我隨手寫的一個變量是否發生了改變。也就是說vue所監控的變量是有要求的,這個要求就是顯式地聲明在data和props中的對象屬性。也就是說我們改變data對象或者對象中的屬性的時候vue是知道我們修改了這些屬性的。vue是如何知道的呢?
在我們實例化Vue這個類的時候,它會遞歸地對data對象設置監聽,監聽的方式是 使用瀏覽器支持的 Object.defineProperty() 這個類靜態方法對這些屬性或對象設置get、set方法。這兩個方法有點類似鈎子函數,當你去改變data中的某個屬性時就會觸發set方法,當你去獲取data中的某個屬性時就會觸發get方法。很顯然你可以在get和set方法中執行一些操作去修改頁面中的DOM。
vue就是這樣對我們的數據進行監控的。前面說了 vue是在初始化的時候遞歸地對屬性進行監聽的,當你改變了data中某個屬性的引用的時候vue會重新對新的對象進行遞歸地監聽。注意這里說的是改變引用的時候才會觸發監聽綁定,也就是說當你的對象引用沒有發生改變,只是給對象增加了一個屬性的時候vue是無法對新的屬性進行監聽的。這也就是為什么你在沒有在data中聲明屬性而是后面添加的屬性vue沒有辦法監聽到的原因。通常我們還會犯一個錯誤,就是我們把開始聲明的對象引用改變了,但是新的引用中的屬性跟原來的引用的屬性有所區別,這樣原來的屬性就會丟失引用。例如
1 data () { 2 return { 3 pro1: { // 初始聲明的引用具有a b兩個屬性 4 a: 1, 5 b: 2 6 } 7 } 8 } 9 10 ... 11 12 // 后面的某個時刻 13 this.pro1 = {a: 3, c: 4}; 14 15 // 在這里你會發現原來 模板中與 pro1.b 發生綁定關系已經被丟失了
就上面的問題而言,我們平時的工作當中如何避免這種情況呢。現階段我采用的方法是 使用 Object.assign()靜態方法或者自己編寫一個merge方法,進行數據的合並操作。由於Object.assign()方法存在局限性,我們自己編寫的merge方法可以更加靈活,所以我們會采用merge方法進行vue的數據變更。merge方法的大概實現如下:
/** * 數據合並方法 * @param {Object} target 目標對象 * @param {Object} origin 源對象 * @param {String} stand 合並標准,默認為左樹標准 * @returns {void} 無返回值 */ export function mergeData (target, origin, stand = 'left') { if (!target || !origin) { return } if (Utils.dataType(target) !== Utils.dataType(origin)) { console.error('目標對象與源對象的數據類型不同,無法實現合並') return } let flag = stand === 'left' for (let prop in target) { if (Utils.dataType(target[prop]) === 'object') { // target[prop] = (target[prop].constructor === Array) ? [] : {}// 三元運算,將s[prop]初始化為數組或者對象 mergeData(target[prop], origin[prop]) } else if (Utils.dataType(target[prop]) === 'array') { // 兼容處理 if (!origin[prop]) { origin[prop] = [] } if (origin[prop].length > 0) { // 該條件是為了剔除重復的數據 target[prop].length = 0 } target[prop].push(...origin[prop]) } else { let defaultVal switch (Utils.dataType(target[prop])) { case 'object': defaultVal = flag ? (target[prop] || {}) : (origin[prop] || {}) break case 'array': // defaultVal = target[prop] || [] defaultVal = flag ? (target[prop] || []) : (origin[prop] || []) break case 'string': // defaultVal = target[prop] || '' defaultVal = flag ? (target[prop] || '') : (origin[prop] || '') break case 'number': // defaultVal = target[prop] || 0 defaultVal = flag ? (target[prop] || 0) : (origin[prop] || 0) break case 'boolean': // defaultVal = target[prop] || false defaultVal = flag ? (target[prop] || false) : (origin[prop] || false) break } target[prop] = (origin[prop] || defaultVal) } }; }
第二個問題:Vue怎么知道我們數據改變之后需要對哪些UI狀態做出修改?
三、組件數據共享
分組件開發當然能簡化我們的工作,數據綁定又讓我們只用關注於數據,因為vue接管了我們的UI,它可以將我們操作的數據動作映射為UI狀態的改變。所以我們在平時開發的過程當中需要關心數據的改變已經數據的傳遞。數據的傳遞指的是組件之間數據的傳遞,因為我們在開發工作當中往往都會有多個組件共享數據的情況。這些組件之間的關系可能是父子組件、兄弟組件、祖先后代組件這些關系。這里我想說一下最簡單的父子組件之間的數據傳遞方式。因為兄弟組件和祖先后代組件這種組件關系Vue提供了Vuex和事件總線這種解決方案。
從我的工作經驗來說父子組件之間的數據傳遞方式有兩大類,一類就是Vue官方推薦的做法,父傳子通過props來進行傳遞,子傳父通過$emit這種發布訂閱模式來進行。第二類就是父傳子通過props進行傳遞,子傳父通過改變對象屬性引用的方式來進行.
// 父組件模板 <ChildComponent :parent-props="testProp" /> // 父組件數據 data: { testProp: { pro1: 'a', pro2: 'b' } } // 子組件 props: ['testProp'], methods: { doing () { this.testProp.a = 'c' } }
<el-form :model="formData" :rules="formRules"> <el-form-item prop="name"> <el-input v-model="formData.name" /> </el-form-item> </el-form> // 數據 data: { formData: { name: '' }, formRules: { name: [ // ... ] } }