歡迎關注前端早茶,與廣東靚仔攜手共同進階
前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~
1. 請說一下響應式數據的原理
默認Vue在初始化數據時,會給data中的屬性使用Object.defineProperty重新定義所有屬性,當頁面到對應屬性時,會進行依賴收集(收集當前組件中的watcher)如果屬性發生變化會通知相關依賴進行更新操作
收集當前組件中的watcher,我進一步問你什么叫當前組件的 watcher
?我面試時經常聽到這種模糊的說法,感覺就是看了些造玩具的文章就說熟悉響應式原理了,起碼的流程要清晰一些:
- 由於 Vue 執行一個組件的
render
函數是由Watcher
去代理執行的,Watcher
在執行前會把Watcher
自身先賦值給Dep.target
這個全局變量,等待響應式屬性去收集它 - 這樣在哪個組件執行
render
函數時訪問了響應式屬性,響應式屬性就會精確的收集到當前全局存在的Dep.target
作為自身的依賴 - 在響應式屬性發生更新時通知
Watcher
去重新調用vm._update(vm._render())
進行組件的視圖更新
響應式部分,如果你想在簡歷上寫熟悉的話,還是要抽時間好好的去看一下源碼中真正的實現,而不是看這種模棱兩可的說法就覺得自己熟練掌握了。
2. 為什么Vue采用異步渲染
因為如果不采用異步更新,那么每次更新數據都會對當前租金按進行重新渲染,所以為了性能考慮,Vue會在本輪數據更新后,再去異步更新數據
什么叫本輪數據更新后,再去異步更新數據?
輪指的是什么,在 eventLoop
里的 task
和 microTask
,他們分別的執行時機是什么樣的,為什么優先選用 microTask
,這都是值得深思的好問題。
建議看看這篇文章: Vue源碼詳解之nextTick:MutationObserver只是浮雲,microtask才是核心!
3. nextTick實現原理
nextTick方法主要是使用了宏任務和微任務,定義一個異步方法,多次調用nextTick會將方法存在隊列中,通過這個異步方法清空當前隊列。所以這個nextTick方法就是異步方法
這句話說的很亂,典型的讓面試官忍不住想要深挖一探究竟的回答。(因為一聽你就不是真的懂)
正確的流程應該是先去 嗅探環境
,依次去檢測
Promise的then
-> MutationObserver的回調函數
-> setImmediate
-> setTimeout
是否存在,找到存在的就使用它,以此來確定回調函數隊列是以哪個 api 來異步執行。
在 nextTick
函數接受到一個 callback
函數的時候,先不去調用它,而是把它 push 到一個全局的 queue
隊列中,等待下一個任務隊列的時候再一次性的把這個 queue
里的函數依次執行。
這個隊列可能是 microTask
隊列,也可能是 macroTask
隊列,前兩個 api 屬於微任務隊列,后兩個 api 屬於宏任務隊列。
簡化實現一個異步合並任務隊列:
let pending = false // 存放需要異步調用的任務 const callbacks = [] function flushCallbacks () { pending = false // 循環執行隊列 for (let i = 0; i < callbacks.length; i++) { callbacks[i]() } // 清空 callbacks.length = 0 } function nextTick(cb) { callbacks.push(cb) if (!pending) { pending = true // 利用Promise的then方法 在下一個微任務隊列中把函數全部執行 // 在微任務開始之前 依然可以往callbacks里放入新的回調函數 Promise.resolve().then(flushCallbacks) } }
測試一下:
// 第一次調用 then方法已經被調用了 但是 flushCallbacks 還沒執行 nextTick(() => console.log(1)) // callbacks里push這個函數 nextTick(() => console.log(2)) // callbacks里push這個函數 nextTick(() => console.log(3)) // 同步函數優先執行 console.log(4) // 此時調用棧清空了,瀏覽器開始檢查微任務隊列,發現了 flushCallbacks 方法,執行。 // 此時 callbacks 里的 3 個函數被依次執行。 // 4 // 1 // 2 // 3
4. Vue優點
虛擬DOM把最終的DOM操作計算出來並優化,由於這個DOM操作屬於預處理操作,並沒有真實的操作DOM,所以叫做虛擬DOM。最后在計算完畢才真正將DOM操作提交,將DOM操作變化反映到DOM樹上
看起來說的很厲害,其實也沒說到點上。關於虛擬 DOM 的優缺點,直接看 Vue 作者尤雨溪本人的知乎回答,你會對它有進一步的理解:
網上都說操作真實 DOM 慢,但測試結果卻比 React 更快,為什么?
雙向數據綁定通過MVVM思想實現數據的雙向綁定,讓開發者不用再操作dom對象,有更多的時間去思考業務邏輯
開發者不操作dom對象,和雙向綁定沒太大關系。React不提供雙向綁定,開發者照樣不需要操作dom。雙向綁定只是一種語法糖,在表單元素上綁定 value
並且監聽 onChange
事件去修改 value
觸發響應式更新。
我建議真正想看模板被編譯后的原理的同學,可以去尤大開源的vue-template-explorer 網站輸入對應的模板,就會展示出對應的 render 函數。
運行速度更快,像比較與react而言,同樣都是操作虛擬dom,就性能而言,vue存在很大的優勢
為什么快,快在哪里,什么情況下快,有數據支持嗎?事實上在初始化數據量不同的場景是不好比較的,React
不需要對數據遞歸的進行 響應式定義
。
而在更新的場景下 Vue
可能更快一些,因為 Vue
的更新粒度是組件級別的,而 React
是遞歸向下的進行 reconciler
,React
引入了 Fiber
架構和異步更新,目的也是為了讓這個工作可以分在不同的 時間片
中進行,不要去阻塞用戶高優先級的操作。
Proxy是es6提供的新特性,兼容性不好,所以導致Vue3一致沒有正式發布讓開發者使用
Vue3 沒發布不是因為兼容性不好,工作正在有序推進中,新的語法也在不斷迭代,並且發布 rfc
征求社區意見。
Object.defineProperty的缺點:無法監控到數組下標的變化,導致直接通過數組的下標給數組設置值,不能實時響應
事實上可以,並且尤大說只是為了性能的權衡才不去監聽。數組下標本質上也就是對象的一個屬性。
5. React和Vue的比較
React默認是通過比較引用的方式(diff)進行的,React不精確監聽數據變化。
比較引用和 diff
有什么關系,難道 Vue 就不 diff
了嗎。
Vue2.0可以通過props實現雙向綁定,用vuex單向數據流的狀態管理框架
雙向綁定是 v-model
吧。
Vue 父組件通過props向子組件傳遞數據或回調
Vue 雖然可以傳遞回調,但是一般來說還是通過 v-on:change
或者 @change
的方式去綁定事件吧,這和回調是兩套機制。
模板渲染方式不同,Vue通過HTML進行渲染
事實上 Vue 是自己實現了一套模板引擎系統,HTML
可以被利用為模板的而已,你在 .vue
文件里寫的 template
和 HTML
本質上沒有關系。
React組合不同功能方式是通過HoC(高階組件),本質是高階函數
事實上高階函數只是社區提出的一種方案被 React 所采納而已,其他的方案還有 renderProps
和 最近流行的Hook
Vue 也可以利用高階函數 實現組合和復用。
6. diff算法的時間復雜度
兩個數的完全的diff算法是一個時間復雜度為o(n3), Vue進行了優化O(n3)復雜度的問題轉換成O(n)復雜度的問題(只比較同級不考慮跨級問題)在前端當中,你很少會跨級層級地移動Dom元素,所以Virtual Dom只會對同一個層級地元素進行對比
聽這個描述來說,React 沒有對 O(n3)
的復雜度進行優化?事實上 React 和 Vue 都只會對 tag
相同的同級節點進行 diff
,如果不同則直接銷毀重建,都是 O(n)
的復雜度。
7. 談談你對作用域插槽的理解
單個插槽當子組件模板只有一個沒有屬性的插槽時, 父組件傳入的整個內容片段將插入到插槽所在的 DOM 位置, 並替換掉插槽標簽本身。
跟 DOM 沒關系,是在虛擬節點樹的插槽位置替換。
如果不加key,那么vue會選擇復用節點(Vue的就地更新策略),導致之前節點的狀態被保留下來,會產生一系列的bug
不加 key 也不一定就會復用,關於 diff 和 key 的使用,建議大家還是找一些非造玩具的文章真正深入的看一下原理。
為什么 Vue 中不要用 index 作為 key?(diff 算法詳解)
8. 組件中的data為什么是函數
因為組件是用來復用的,JS里對象是引用關系,這樣作用域沒有隔離,而new Vue的實例,是不會被復用的,因此不存在引用對象問題
這句話反正我壓根沒聽懂,事實上如果組件里 data 直接寫了一個對象的話,那么如果你在模板中多次聲明這個組件,組件中的 data 會指向同一個引用。
此時如果在某個組件中對 data 進行修改,會導致其他組件里的 data 也被污染。 而如果使用函數的話,每個組件里的 data 會有單獨的引用,這個問題就可以避免了。
這個問題我同樣舉個例子來方便理解,假設我們有這樣的一個組件,其中的 data 直接使用了對象而不是函數:
var Counter = { template: `<span @click="count++"></span>` data: { count: 0 } }
注意,這里的 Counter.data
是一個引用,也就是它是在當前的運行環境下全局唯一
的,它在堆內存中占用了一部分空間。
然后我們在模板中調用兩次 Counter
組件:
<div> <Counter id="a" /> <Counter id="b" /> </div>
我們從原理出發,先看看它被編譯成什么樣的 render
函數:
function render() { with(this) { return _c('div', [_c('Counter'), _c('Counter')], 1) } }
每一個 Counter
會被 _c
所調用,也就是 createElement
,想象一下 createElement
內部會發生什么,它會直接拿着 Counter
上的 data
這個引用去創建一個組件。 也就是所有的 Counter
組件實例上的 data
都指向同一個引用。
此時假如 id 為 a 的 Counter 組件內部調用了 count++
,會去對 data
這個引用上的 count 屬性賦值,那么此時由於 id 為 b 的 Counter 組件內部也是引用的同一份 data,它也會感覺到變化而更新組件,這就造成了多個組件之間的數據混亂了。
9. computed和watch有什么區別
計算屬性是基於他們的響應式依賴進行緩存的,只有在依賴發生變化時,才會計算求值,而使用 methods,每次都會執行相應的方法
這也是一個一問就倒的回答,依賴變化是計算屬性就重新求值嗎?中間經歷了什么過程,為什么說 computed
是有緩存值的?隨便挑一個點深入問下去就站不住。 事實上 computed
會擁有自己的 watcher
,它內部有個屬性 dirty
開關來決定 computed
的值是需要重新計算還是直接復用之前的值。
以這樣的一個例子來說:
computed: { sum() { return this.count + 1 } }
首先明確兩個關鍵字:
「dirty」 從字面意義來講就是 臟
的意思,這個開關開啟了,就意味着這個數據是臟數據,需要重新求值了拿到最新值。
「求值」 的意思的對用戶傳入的函數進行執行,也就是執行 return this.count + 1
- 在
sum
第一次進行求值的時候會讀取響應式屬性count
,收集到這個響應式數據作為依賴。並且計算出一個值來保存在自身的value
上,把dirty
設為 false,接下來在模板里再訪問sum
就直接返回這個求好的值value
,並不進行重新的求值。 - 而
count
發生變化了以后會通知sum
所對應的watcher
把自身的dirty
屬性設置成 true,這也就相當於把重新求值的開關打開來了。這個很好理解,只有count
變化了,sum
才需要重新去求值。 - 那么下次模板中再訪問到
this.sum
的時候,才會真正的去重新調用sum
函數求值,並且再次把dirty
設置為 false,等待下次的開啟……
后續我會考慮單獨出一篇文章進行詳細講解。
10. Watch中的deep:true是如何實現的
當用戶指定了watch中的deep屬性為true時,如果當前監控的值是數組類型,會對對象中的每一項進行求值,此時會將當前watcher存入到對應屬性的依賴中,這樣數組中的對象發生變化時也會通知數據更新。
不光是數組類型,對象類型也會對深層屬性進行 依賴收集
,比如監聽了 obj
,假如設置了 deep: true
,那么對 obj.a.b.c = 5
這樣深層次的修改也一樣會觸發 watch 的回調函數。本質上是因為 Vue 內部對設置了 deep
的 watch,會進行遞歸的訪問
(只要此屬性也是響應式屬性
),而在此過程中也會不斷發生依賴收集。
在回答這道題的時候,同樣也要考慮到 遞歸收集依賴
對性能上的損耗和權衡,才是一份合格的回答。
11. action和mutation區別
mutation是同步更新數據(內部會進行是否為異步方式更新數據的檢測)
內部並不能檢測到是否異步更新,而是實例上有一個開關變量 _committing
,
- 只有在 mutation 執行之前才會把開關打開,允許修改 state 上的屬性。
- 並且在 mutation 同步執行完成后立刻關閉。
-
異步更新的話由於已經出了
mutation
的調用棧,此時的開關已經是關上的,自然能檢測到對 state 的修改並報錯。具體可以查看源碼中的withCommit
函數。這是一種很經典對於js單線程機制
的利用。Store.prototype._withCommit = function _withCommit (fn) { var committing = this._committing; this._committing = true; fn(); this._committing = committing; };
歡迎關注前端早茶,與廣東靚仔攜手共同進階
前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~