面試的時候經常被問到 響應式 相關的內容,而Vue3.0 更新後,面試官又有了新的武器;
面試官:為什麼 Vue3.0 要重寫響應式系統?
懵逼樹上懵逼果,懵逼樹下你和我,面試官在問什麼,我該怎麼回答,完全不知道怎麼回事;
有些經驗的小夥伴可能會從解釋 Proxy 的好處開始簡單聊一下,比如: Proxy 是直接代理物件,而不是劫持物件的屬性;更好的陣列監控;
這樣的回答,勉強算是合格吧
那到底應該怎麼答呢?
面試官背後的出題邏輯
別急,咱們先整理一下思路,孫子兵法有云:“知己知彼,百戰不殆”;面試就像打仗,你來我往,所以我們需要換位思考,想一想,為什麼面試官會問這樣一個問題?面試官想從這個問題裡得到什麼回答?這個問題可以考察哪些技術點? 想清楚這個問題,再回到自己身上,這些技術點,你都掌握了嗎?
說得直白一點,面試就像考試,你需要先 讀題、審題才能答好這道題;
為什麼很多人認為 “面試造火箭,工作擰螺絲”?因為沒有換位思考,沒有想清楚面試題背後的邏輯;
那我們想清楚這個邏輯之後,需要我們做的就是提取技術點,整理思路,做出對應解答;當然,前提是你需要具備這些技術能力
那麼接下來,我就嘗試拆解一下這個面試題了,提取其中的知識點。
對於你來說,就是要看看這些知識點,你都掌握了多少?
為什麼 Vue3.0 要重寫響應式系統 ?
為什麼重寫?如果之前好好的,重寫就沒有意義,那之前存在什麼問題,現在是怎麼解決的?就是關鍵點了;
不知道你對 Vue2.x 的響應式掌握多少,是不是欠下了技術的債呢?沒關係,我來幫你還債,先梳理 Vue2.x 的響應式;
其實基於這個面試題,背後還有很多技術點,上面這些,是與當前題目有直接關係的,實際面試中,很有可能基於這些技術點,在進行深入交流,這裡就不擴充套件了,你能把現在這些問題理清楚,就算賺到了;
Vue2.x 響應式
其實關於這一點,在Vue 的官方文件中,早已經有過說明了,而且說得非常詳細;官方文件:https://cn.vuejs.org/v2/guide/reactivity.html
當你把一個普通的 JavaScript 物件傳入 Vue 例項作為 data 選項,Vue 將遍歷此物件所有的 property,並使用 Object.defineProperty 把這些 property 全部轉為 getter/setter。Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是 Vue 不支援 IE8 以及更低版本瀏覽器的原因。
這些 getter/setter 對使用者來說是不可見的,但是在內部它們讓 Vue 能夠追蹤依賴,在 property 被訪問和修改時通知變更。這裡需要注意的是不同瀏覽器在控制檯列印資料物件時對 getter/setter 的格式化並不同,所以建議安裝 vue-devtools 來獲取對檢查資料更加友好的使用者介面。
每個元件例項都對應一個 watcher 例項,它會在元件渲染的過程中把“接觸”過的資料 property 記錄為依賴。之後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的元件重新渲染。
我們使用官方給的一張圖示,來梳理整個流程;
我們先來看一段程式碼
響應式原理
data 中的 obj 就是一個普通的 JavaScript 物件,透過點選 Click 按鈕,將獲取到的隨機數賦值給 this.message ,而 this.message 指向的就是 data 中 obj 物件的 message 屬性;當message 發生資料改變時,頁面中 H1 標籤的內容會隨之改變,這個過程就是就是響應式的;那麼Vue 是如何實現的呢?
首先,Vue 內部使用 Object.defineProperty() 將 Data 中的每一個成員都轉換為 getter / setter 的形式;getter 用來依賴收集,setter 用來派發更新;而模板內容,最終會被編譯為 render 函式,在 render 函式中,我們能發現 _v(_s(message)) message 被訪問了,就會觸發 getter 來進行依賴收集,而在程式碼中的點選事件中,一旦事件處理程式被觸發執行,那麼 message 則會被修改,就會觸發 setter來進行派發更新;
雖然流程理清楚了,但是總感覺少點什麼,怎麼才能更通透呢?
我們用程式碼來模擬整個的實現過程;
defineProperty 模擬程式碼
defineProperty 的基本用法,直接看手冊就行了:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
我們來看看程式碼:
<div id="app"> hello </div> <script> // 模擬 Vue 中的 data 選項 let data = { msg: 'hello' } // 模擬 Vue 的例項 let vm = {} // 資料劫持:當訪問或者設定 vm 中的成員的時候,做一些干預操作 Object.defineProperty(vm, 'msg', { // 可列舉(可遍歷) enumerable: true, // 可配置(可以使用 delete 刪除,可以透過 defineProperty 重新定義) configurable: true, // 當獲取值的時候執行 get () { console.log('get: ', data.msg) return data.msg }, // 當設定值的時候執行 set (newValue) { console.log('set: ', newValue) if (newValue === data.msg) { return } data.msg = newValue // 資料更改,更新 DOM 的值 document.querySelector('#app').textContent = data.msg } }) // 測試 vm.msg = 'Hello World' console.log(vm.msg) </script>
你沒有看錯,加上註釋,一共 36行程式碼,這就是 Vue2.x 對響應式實現的整個流程;
繼續實現多個數據的響應式
<body> <div id="app"> hello </div> <script> // 模擬 Vue 中的 data 選項 let data = { msg: 'hello', count: 10 } // 模擬 Vue 的例項 let vm = {} proxyData(data) function proxyData(data) { // 遍歷 data 物件的所有屬性 Object.keys(data).forEach(key => { // 把 data 中的屬性,轉換成 vm 的 setter/setter Object.defineProperty(vm, key, { enumerable: true, configurable: true, get () { console.log('get: ', key, data[key]) return data[key] }, set (newValue) { console.log('set: ', key, newValue) if (newValue === data[key]) { return } data[key] = newValue // 資料更改,更新 DOM 的值 document.querySelector('#app').textContent = data[key] } }) }) } // 測試 vm.msg = 'Hello World' console.log(vm.msg) </script></body>
上面的程式碼只是模擬了 響應式 的原理,但Vue在實現中,肯定不會那麼簡單,接下來,我們看一下原始碼呀……
Vue2 原始碼解讀
首先找到響應式程式碼的處理位置:
看完Vue2.x 響應式的程式碼,我們再回過頭來思考最開始的問題,為什麼 Vue3.0 要重寫響應式系統 ?
為什麼重寫?如果之前好好的,重寫就沒有意義,那之前存在什麼問題,換句話問就是 defineProperty 有什麼問題?
Object.defineProperty 的問題
其實, defineProperty 的問題,在Vue2.x 的手冊中,已經說過了;“哎,很多人就是不看文件啊”
https://cn.vuejs.org/v2/guide/reactivity.html#%E5%AF%B9%E4%BA%8E%E6%95%B0%E7%BB%84
下面分別使用 Vue2 和 Vue3 實現了一個小功能,程式碼一模一樣,功能當然也一樣,但是,在 Vue2 中就會有Bug,而執行在vue3中的,則沒有任何問題;
Vue2:
<template> <div class="about"> <h1>This is an about page</h1> <p v-for="(v, k) in users"> {{ v.names }} </p> <button @click="changes">更新</button> </div></template><script>export default { data() { return { users: [ { id: 1, names: "路飛-v2" }, { id: 2, names: "鳴人-v2" }, ], }; }, methods: { changes() { // this.users[0] = {id:'0',names:'liuneng'} // this.users[1].names = 'lnsdsdfg' this.users[1] = { id: "1", names: "劉能-v2" }; }, },};</script><style lang="stylus" scoped></style>
Vue3:
<template> <div class="about"> <h1>This is an about page</h1> <p v-for="(v, k) in users"> {{ v.names }} </p> <button @click="changes">更新</button> </div></template><script>export default { data() { return { users: [ { id: 1, names: "路飛-v3" }, { id: 2, names: "鳴人-v3" }, ], }; }, methods: { changes() { // this.users[0] = {id:'0',names:'liuneng'} // this.users[1].names = 'lnsdsdfg' this.users[1] = { id: "1", names: "劉能-v3" }; }, },};</script>
其核心點在於 defineProperty 不能很好的實現對陣列下標的監控,而在 Vue2 的實現程式碼中,沒有更好的方案對此進行改善,尤大索性直接放棄了實現;關於這個問題,尤大也在 github 做過迴應,截個圖給大家看看;
那麼,Vue 目前還沒有解決的問題,Vue3中顯然是已經解決的了,問題是,Vue3 是如何解決的呢?前面在 Vue3 的程式碼中我們使用的是傳統的 Options Api 來實現的資料響應式, 而在 Vue3 中全新的 Composition Api 也實現了響應式系統,我們先來感受一下 Composition Api 的基礎用法
Composition API 的響應式系統
ref 響應式
Proxy 實現原理
使用 Proxy 實現的響應式程式碼,要比使用 defineProperty 的程式碼簡單得多,因為 Proxy 天然的能夠對整個物件做監聽,而不需要對資料行遍歷後做監聽,同時也就解決了陣列下標的問題;
我們來一段模擬程式碼看一下:
<div id="app"> hello </div> <script> // 模擬 Vue 中的 data 選項 let data = { msg: 'hello', count: 0 } // 模擬 Vue 例項 const vm = new Proxy(data, { // 執行代理行為的函式 // 當訪問 vm 的成員會執行 get (target, key) { console.log('get, key: ', key, target[key]) return target[key] }, // 當設定 vm 的成員會執行 set (target, key, newValue) { console.log('set, key: ', key, newValue) if (target[key] === newValue) { return } target[key] = newValue document.querySelector('#app').textContent = target[key] } }) // 測試 vm.msg = 'Hello World' console.log(vm.msg) </script>