高級前端開發者必會的34道Vue面試題系列(一)
- 2020 年 3 月 30 日
- 筆記
前言
最近拿到一套高級前端的Vue面試題,從頭往下看了一遍,居然大部分都是一知半解的,遂準備一道一道的認真學習總結,立志做一位Vue高級開發者!

看一下你是否也對上面的34個問題一知半解,如果和我差不多的話就來一起學習吧!
如何理解MVVM原理?
提到MVVM,很多前端開發者都會想到Vue的雙向綁定,然而它們並不能劃等號,MVVM是一種軟體架構模式,而Vue只是一種在前端層面上的實現,其實不單在Vue里,在很多Web 框架應用里都有相關的實現。MVVM模式到底是什麼呢?要說到MVVM這種模式,則必須要提及另一種大多數開發者都能耳熟能詳的模式,就是MVC模式。
什麼是MVC?
在前幾年,前後端完全分離開之前,很多很火的後端框架都會說自己是支援MVC模式,像JAVA的SpringMVC、PHP的smarty、Nodejs的express和Koa,那麼MVC的模式到底是什麼樣的?先看看下面這張經典的MVC模型圖,Model(模型)、View(視圖)、 Controller(控制器)相互依賴關係的三部分組成模型。

認識一下這三部分具體是指什麼。
Model
這裡的Model在MVC中實際是數據模型的概念,可以把它當成從資料庫里查出來後的一條數據,或者是將查詢出來的元數據經過裁剪或者處理後的一個特定數據模型結構。
View
View是視圖,是將數據內容呈現給用戶肉眼的介面層,View層最終會將數據模型下的資訊,渲染顯示成人類能易於識別感知的部分。
Controller
Controller是數據模型與View之間的橋樑層,實際介面層的各種變化都要經過它來控制,而且像用戶從介面提交的數據也會經過Controller的組裝檢查生成數據模型,然後改變資料庫里的數據內容。
MVC的使用
像接觸過MVC框架的同學就知道,如果想把數據從資料庫里渲染到頁面上,先要查詢完資料庫後,將拿到的元數據進行一些處理,一般會刪掉無用的欄位,或者進行多個數據模型間的數據聚合,然後再給到頁面模板引擎(ejs,Thymeleaf等)進行數據組裝,最後組合完成進行渲染後生成HTML格式文件供瀏覽器展示使用。
像前面提到的各大支援MVC模式的Web開發框架,在前後端徹底分離之後就很少再提了。因為前端獨立開發發布,實際相對原來的MVC模式是少了View這一層,這也讓新的概念Restful出現在我們的視野里,很多新的框架又開始支援提供這種前端控制輕量級模式下的適配方案。
但是前後端分離的出現後,MVC就此沒有了嗎?當然不是。實際對於MVC模式沒有特別明確的概念,在前後端分離之後可以嘗試從不同的角度去看。可以理解整個系統在原先的MVC基礎上View層進行細化,把整個前端項目當成一個View層,也可以從前端視角去看,Restful介面返回的Json數據當成一個數據模型,作為MVC的Model層,而前端Javascript自身對數據的處理是Contrller層,真正的頁面渲染結果是View層。
下面以前端視角下的MVC模式中舉個例子,介面返回的數據Model模型與View頁面之間由Controller連接,來完成系統中的數據展示。
<!--view--> <html> ... <div> <span id="name"></span> <div id="data"></div> </div> ... </html> <script> // 生成model數據模型 function getDataApi() { // 模擬介面返回 return { name: 'mvc', data: 'mvc 數據資訊' } } // controller控制邏輯 function pageController() { const result = getDataApi(); document.getElementById('name').innerText = `姓名:${result.name}`; document.getElementById('data').innerText = result.data; } </script>
什麼是MVVM?
隨著前端對於控制邏輯的越來越輕量,MVVM模式作為MVC模式的一種補充出現了,萬變不離其宗,最終的目的都是將Model里的數據展示在View視圖上,而MVVM相比於MVC則將前端開發者所要控制的邏輯做到更加符合輕量級的要求。

ViewModel
在Model和View之間多了叫做View-Model的一層,將模型與視圖做了一層綁定關係,在理想情況下,數據模型返回什麼試圖就應該展示什麼,看看下面這個例子。
<!--view頁面--> <html> ... <div> <span vm-bind-key="name"></span> <div vm-bind-key="data"></div> </div> ... </html> <script> // 生成model數據模型 function getDataApi() { // 模擬介面返回 return { name: 'mvc', data: 'mvc 數據資訊' } } // ViewModel控制邏輯 function pageViewModel() { const result = getDataApi(); return result; } </script>
上面作為理想情況下例子,在ViewModel引入之後,視圖完全由介面返回數據驅動,由開發者所控制的邏輯非常輕量。不過這裡要說明的是,在MVVM模式下,Controller控制邏輯並非就沒了,像操作頁面DOM響應的邏輯被SDK(如Vue的內部封裝實現)統一實現了,像不操作介面返回的數據是因為服務端在數據返回給前端前已經操作好了。
例子里pageViewModel函數的實現是非常關鍵的一步,如何將數據模型與頁面視圖綁定起來呢?在目前的前端領域裡有三類實現,Angularjs的主動輪詢檢查新舊值變化更新視圖、Vue利用ES5的Object.defineProperty的getter/setter方法綁定、backbone的發布訂閱模式,從主動和被動的方式去實現了ViewModel的關係綁定,接下來主要看看Vue中的MVVM的實現。
Vue2.0中的MVVM實現

Vue2.0的MVVM實現中,對View-Model的實現本質利用的ES5的Object.defineProperty方法,當Object.defineProperty方法在給數據Model對象定義屬性的時候先掛載一些方法,在這些方法里實現與介面的值綁定響應關係,當應用的屬性被讀取或者寫入的時候便會觸發這些方法,從而達到數據模型里的值發生變化時同步響應到頁面上。
Vue的響應式原理
// html <body> <div> <span>{{name}}</span> <span>{{data}}</span> </div> <body> //js <script src="vue.js"></script> <script> // 生成model數據模型 function getDataApi() { // 模擬介面返回 return { name: 'mvc', data: 'mvc 數據資訊' } } new Vue({ el: 'body', data() { return { name:'', data: '', } }, mounted() { const result = getDataApi(); this.name = result.name; this.data = result.data; } }) </script>
當new Vue在實例化的時候,首先將data方法里返回的對象屬性都掛載上setter方法,而setter方法里將頁面上的屬性進行綁定,當頁面載入時,瀏覽器提供的DOMContentloaded事件觸發後,調用mounted掛載函數,開始獲取介面數據,獲取完成後給data里屬性賦值,賦值的時候觸發前面掛載好的setter方法,從而引起頁面的聯動,達到響應式效果。
簡易實現Object.defineProperty下的綁定原理
// html <body> <span id="name"></span> <body> <script> var data = { name: '' }; // Data Bindings Object.defineProperty(data, 'name', { get: function(){}, set: function(newValue){ // 頁面響應處理 document.getElementById('name').innerText = newValue data.name = value }, enumerable: true, configurable: true }); // 頁面DOM listener document.getElementById('name').onchange = function(e) { data.name = e.target.value; } </script>
實現Vue3.0版本的MVVM
這裡採用Vue3.0最新的實現方式,用Proxy和Reflect來替代Object.definePropertypry的方式。至於Vue3.0為何不再採用2.0中Object.defineProperty的原因,我會在後續詳寫,先來介紹一下ES6里的Proxy與Reflect。
Proxy
Proxy是ES6里的新構造函數,它的作用就是代理,簡單理解為有一個對象,不想完全對外暴露出去,想做一層在原對象操作前的攔截、檢查、代理,這時候你就要考慮Proxy了。
const myObj = { _id: '我是myObj的ID', name: 'mvvm', age: 25 } const myProxy = new Proxy(myObj, { get(target, propKey) { if (propKey === 'age') { console.log('年齡很私密,禁止訪問'); return '*'; } return target[propKey]; }, set(target, propKey, value, receiver) { if (propKey === '_id') { console.log('id無權修改'); return; } target[propKey] = value + (receiver.time || ''); }, // setPrototypeOf(target, proto) {}, // apply(target, object, args) {}, // construct(target, args) {}, // defineProperty(target, propKey, propDesc) {}, // deleteProperty(target, propKey) {}, // has(target, propKey) {}, // ownKeys(target) {}, // isExtensible(target) {}, // preventExtensions(target) {}, // getOwnPropertyDescriptor(target, propKey) {}, // getPrototypeOf(target) {}, }); myProxy._id = 34;// id無權修改 console.log(`age is: ${myProxy.age}`);//年齡很私密,禁止訪問 // age is: * myProxy.name = 'my name is Proxy'; console.log(myProxy); // { _id: '我是myObj的ID', name: 'my name is Proxy', age: 25} const newObj = { time: ` [${new Date()}]`, }; // 原對象原型鏈賦值 Object.setPrototypeOf(myProxy, newObj); myProxy.name = 'my name is newObj'; console.log(myProxy.name); //my name is newObj [Thu Mar 19 2020 18:33:22 GMT+0800 (GMT+08:00)]
Reflect
Reflect是ES6里的新的對象,非構造函數,不能用new操作符。可以把它跟Math類比,Math是處理JS中數學問題的方法函數集合,Reflect是JS中對象操作方法函數集合,它暴露出來的方法與Object構造函數所帶的靜態方法大部分重合,實際功能也類似,Reflect的出現一部分原因是想讓開發者不直接使用Object這一類語言層面上的方法,還有一部分原因也是為了完善一些功能。Reflect提供的方法還有一個特點,完全與Proxy構造函數里Hander參數對象中的鉤子屬性一一對應。

看下面一個改變對象原型的例子。
const myObj = { _id: '我是myObj的ID', name: 'mvvm', age: 25 } const myProxy = new Proxy(myObj, { get(target, propKey) { return target[propKey]; }, set(target, propKey, value, receiver) { target[propKey] = value + (receiver.time || ''); }, setPrototypeOf(target, proto) { if (proto.status === 'enable') { Reflect.setPrototypeOf(target, proto); return true; } return false; }, }); const newObj = { time: ` [${new Date()}]`, status: 'sable' }; // 原對象原型鏈賦值 const result1 = Reflect.setPrototypeOf(myProxy, { time: ` [${new Date()}]`, status: 'disable' }); myProxy.name = 'first set name' console.log(result1) //false console.log(myProxy.name); //first set name // 原對象原型鏈賦值 const result2 = Reflect.setPrototypeOf(myProxy, { time: ` [${new Date()}]`, status: 'enable' }); myProxy.name = 'second set name' console.log(result1) //true console.log(myProxy.name); //second set name [Thu Mar 19 2020 19:43:59 GMT+0800 (GMT+08:00)] /*當執行到這裡時直接報錯了*/ // 原對象原型鏈賦值 Object.setPrototypeOf(myProxy, { time: ` [${new Date()}]`, status: 'disable' }); myProxy.name = 'third set name' console.log(myProxy.name);
解釋一下上面的這段程式碼,通過Reflec.setPrototypeOf方法修改原對象原型時,必須經過Proxy里hander的掛載的setPrototypeOf掛載函數,在掛載函數里進行條件proto.status是否是enable篩選後,再決定是否真正修改原對象myObj的原型,最後返回true或者false來告知外部原型是否修改成功。
這裡還有一個關鍵點,就是在程式碼執行到原有的Object.setPrototypeOf方法時,程式則直接拋錯,這其實也是Reflect出現的一個原因,即使現在ES5里的Object有同樣的功能,但是Reflect實現的更友好,更適合開發者開發應用程式。
實現MVVM
接下來使用上面的Proxy和Reflect來實現MVVM,這裡將data和Proxy輸出到全局Window下,方便我們模擬數據雙向聯動的效果。
<!DOCTYPE html> <html> <div> name: <input id="name" /> age: <input id="age" /> </div> </html> <script> // 與頁面綁定 const data = { name: '', age: 0 } // 暴露到外部,便於查看效果 window.data = data; window.myProxy = new Proxy(data, { set(target, propKey, value) { // 改變數據Model時修改頁面 if (propKey === 'name') { document.getElementById('name').value = value; } else if (propKey === 'age') { document.getElementById('age').value = value; } Reflect.set(...arguments); }, }); // 頁面變化改變Model內數據 document.getElementById('name').onchange = function(e) { Reflect.set(data, 'name', e.target.value); } document.getElementById('age').onchange = function(e) { Reflect.set(data, 'age', e.target.value); } </script>

先列印了data,然後模擬有非同步數據過來,手動修改data里的數據window.myProxy.age=25,這時候頁面上的age聯動變化為25,再次列印了查看data。接下來在頁面上手動輸入name,輸入完成後觸發輸入框的onchange事件後,再次查看data,此時model里的數據已經變化為最新的與頁面保持一致的值。
總結
上面整篇內容介紹了MVC和MVVM兩種模式的差異性,還介紹了在Vue在2.0和3.0中MVVM的實現,最後利用Vue3.0中提供的原理思路來實現了一次View和Model的雙向綁定。那麼Vue2.0升級到3.0為什麼要從將雙向綁定的實現從Object.definePropertypry變成ES6的Proxy來實現呢?下一篇再講。
如上內容均為自己總結,難免會有錯誤或者認識偏差,如有問題,希望大家留言指正,以免誤人,若有什麼問題請留言,會儘力回答之。如果對你有幫助不要忘了分享給你的朋友或者點擊右下方的「在看」哦!也可以關注作者,查看歷史文章並且關注最新動態,助你早日成為一名全棧工程師!