一次關(guān)于 Vue 的自我模擬面試
(給前端大學(xué)加星標(biāo),提升前端技能.)
轉(zhuǎn)自:_楊溜溜
segmentfault.com/a/1190000023949535
前言
昨晚做了一個(gè)夢(mèng),夢(mèng)見(jiàn)自己到了一家大廠面試,面試官走近房間,坐了下來(lái):是楊溜溜吧?國(guó)際慣例,先來(lái)個(gè)自我介紹吧。
于是我巴拉巴拉開(kāi)始了長(zhǎng)達(dá)兩分鐘的自我介紹,與此同時(shí),面試官邊聽(tīng)邊看我的簡(jiǎn)歷,邊看邊皺眉,結(jié)束后問(wèn):看你之前的項(xiàng)目經(jīng)常用到Vue,對(duì)Vue熟悉嗎?
我嘴角一笑,心里暗喜:幸好有專(zhuān)門(mén)看Vue的面試題,看來(lái)這次穩(wěn)了。于是謙虛又裝逼的回答:還行吧,您隨便問(wèn)。
于是面試官看我口氣那么大,心想:?jiǎn)燕耄瑏?lái)了一個(gè)裝逼的,勞資今天就只問(wèn)Vue。
原文地址:
https://github.com/yacan8/blog/issues/31
來(lái),先介紹一下Vue的響應(yīng)式系統(tǒng)
Vue為MVVM框架,當(dāng)數(shù)據(jù)模型data變化時(shí),頁(yè)面視圖會(huì)得到響應(yīng)更新,其原理對(duì)data的getter/setter方法進(jìn)行攔截(Object.defineProperty或者Proxy),利用發(fā)布訂閱的設(shè)計(jì)模式,在getter方法中進(jìn)行訂閱,在setter方法中發(fā)布通知,讓所有訂閱者完成響應(yīng)。
在響應(yīng)式系統(tǒng)中,Vue會(huì)為數(shù)據(jù)模型data的每一個(gè)屬性新建一個(gè)訂閱中心作為發(fā)布者,而監(jiān)聽(tīng)器watch、計(jì)算屬性computed、視圖渲染template/render三個(gè)角色同時(shí)作為訂閱者,對(duì)于監(jiān)聽(tīng)器watch,會(huì)直接訂閱觀察監(jiān)聽(tīng)的屬性,對(duì)于計(jì)算屬性computed和視圖渲染template/render,如果內(nèi)部執(zhí)行獲取了data的某個(gè)屬性,就會(huì)執(zhí)行該屬性的getter方法,然后自動(dòng)完成對(duì)該屬性的訂閱,當(dāng)屬性被修改時(shí),就會(huì)執(zhí)行該屬性的setter方法,從而完成該屬性的發(fā)布通知,通知所有訂閱者進(jìn)行更新。
computed與watch的區(qū)別
計(jì)算屬性computed和監(jiān)聽(tīng)器watch都可以觀察屬性的變化從而做出響應(yīng),不同的是:
計(jì)算屬性computed更多是作為緩存功能的觀察者,它可以將一個(gè)或者多個(gè)data的屬性進(jìn)行復(fù)雜的計(jì)算生成一個(gè)新的值,提供給渲染函數(shù)使用,當(dāng)依賴(lài)的屬性變化時(shí),computed不會(huì)立即重新計(jì)算生成新的值,而是先標(biāo)記為臟數(shù)據(jù),當(dāng)下次computed被獲取時(shí)候,才會(huì)進(jìn)行重新計(jì)算并返回。
而監(jiān)聽(tīng)器watch并不具備緩存性,監(jiān)聽(tīng)器watch提供一個(gè)監(jiān)聽(tīng)函數(shù),當(dāng)監(jiān)聽(tīng)的屬性發(fā)生變化時(shí),會(huì)立即執(zhí)行該函數(shù)。
介紹一下Vue的生命周期
beforeCreate:是new Vue()之后觸發(fā)的第一個(gè)鉤子,在當(dāng)前階段data、methods、computed以及watch上的數(shù)據(jù)和方法都不能被訪問(wèn)。
created:在實(shí)例創(chuàng)建完成后發(fā)生,當(dāng)前階段已經(jīng)完成了數(shù)據(jù)觀測(cè),也就是可以使用數(shù)據(jù),更改數(shù)據(jù),在這里更改數(shù)據(jù)不會(huì)觸發(fā)updated函數(shù)。可以做一些初始數(shù)據(jù)的獲取,在當(dāng)前階段無(wú)法與Dom進(jìn)行交互,如果非要想,可以通過(guò)vm.$nextTick來(lái)訪問(wèn)Dom。
beforeMount:發(fā)生在掛載之前,在這之前template模板已導(dǎo)入渲染函數(shù)編譯。而當(dāng)前階段虛擬Dom已經(jīng)創(chuàng)建完成,即將開(kāi)始渲染。在此時(shí)也可以對(duì)數(shù)據(jù)進(jìn)行更改,不會(huì)觸發(fā)updated。
mounted:在掛載完成后發(fā)生,在當(dāng)前階段,真實(shí)的Dom掛載完畢,數(shù)據(jù)完成雙向綁定,可以訪問(wèn)到Dom節(jié)點(diǎn),使用$refs屬性對(duì)Dom進(jìn)行操作。
beforeUpdate:發(fā)生在更新之前,也就是響應(yīng)式數(shù)據(jù)發(fā)生更新,虛擬dom重新渲染之前被觸發(fā),你可以在當(dāng)前階段進(jìn)行更改數(shù)據(jù),不會(huì)造成重渲染。
updated:發(fā)生在更新完成之后,當(dāng)前階段組件Dom已完成更新。要注意的是避免在此期間更改數(shù)據(jù),因?yàn)檫@可能會(huì)導(dǎo)致無(wú)限循環(huán)的更新。
beforeDestroy:發(fā)生在實(shí)例銷(xiāo)毀之前,在當(dāng)前階段實(shí)例完全可以被使用,我們可以在這時(shí)進(jìn)行善后收尾工作,比如清除計(jì)時(shí)器。
destroyed:發(fā)生在實(shí)例銷(xiāo)毀之后,這個(gè)時(shí)候只剩下了dom空殼。組件已被拆解,數(shù)據(jù)綁定被卸除,監(jiān)聽(tīng)被移出,子實(shí)例也統(tǒng)統(tǒng)被銷(xiāo)毀。
為什么組件的data必須是一個(gè)函數(shù)
一個(gè)組件可能在很多地方使用,也就是會(huì)創(chuàng)建很多個(gè)實(shí)例,如果data是一個(gè)對(duì)象的話,對(duì)象是引用類(lèi)型,一個(gè)實(shí)例修改了data會(huì)影響到其他實(shí)例,所以data必須使用函數(shù),為每一個(gè)實(shí)例創(chuàng)建一個(gè)屬于自己的data,使其同一個(gè)組件的不同實(shí)例互不影響。
組件之間是怎么通信的
父子組件通信
父組件 -> 子組件:prop
子組件 -> 父組件:$on/$emit
獲取組件實(shí)例:使用$parent/$children,$refs.xxx,獲取到實(shí)例后直接獲取屬性數(shù)據(jù)或調(diào)用組件方法
兄弟組件通信
Event Bus:每一個(gè)Vue實(shí)例都是一個(gè)Event Bus,都支持$on/$emit,可以為兄弟組件的實(shí)例之間new一個(gè)Vue實(shí)例,作為Event Bus進(jìn)行通信。
Vuex:將狀態(tài)和方法提取到Vuex,完成共享
跨級(jí)組件通信
使用provide/inject
Event Bus:同兄弟組件Event Bus通信
Vuex:將狀態(tài)和方法提取到Vuex,完成共享
Vue事件綁定原理說(shuō)一下
每一個(gè)Vue實(shí)例都是一個(gè)Event Bus,當(dāng)子組件被創(chuàng)建的時(shí)候,父組件將事件傳遞給子組件,子組件初始化的時(shí)候是有$on方法將事件注冊(cè)到內(nèi)部,在需要的時(shí)候使用$emit觸發(fā)函數(shù),而對(duì)于原生native事件,使用addEventListener綁定到真實(shí)的DOM元素上。
slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的內(nèi)容分發(fā)機(jī)制,組件內(nèi)部的模板引擎使用slot元素作為承載分發(fā)內(nèi)容的出口。插槽slot是子組件的一個(gè)模板標(biāo)簽元素,而這一個(gè)標(biāo)簽元素是否顯示,以及怎么顯示是由父組件決定的。
slot又分三類(lèi),默認(rèn)插槽,具名插槽和作用域插槽。
默認(rèn)插槽:又名匿名查抄,當(dāng)slot沒(méi)有指定name屬性值的時(shí)候一個(gè)默認(rèn)顯示插槽,一個(gè)組件內(nèi)只有有一個(gè)匿名插槽。 具名插槽:帶有具體名字的插槽,也就是帶有name屬性的slot,一個(gè)組件可以出現(xiàn)多個(gè)具名插槽。 作用域插槽:默認(rèn)插槽、具名插槽的一個(gè)變體,可以是匿名插槽,也可以是具名插槽,該插槽的不同點(diǎn)是在子組件渲染作用域插槽時(shí),可以將子組件內(nèi)部的數(shù)據(jù)傳遞給父組件,讓父組件根據(jù)子組件的傳遞過(guò)來(lái)的數(shù)據(jù)決定如何渲染該插槽。
實(shí)現(xiàn)原理:當(dāng)子組件vm實(shí)例化時(shí),獲取到父組件傳入的slot標(biāo)簽的內(nèi)容,存放在vm.$slot中,默認(rèn)插槽為vm.$slot.default,具名插槽為vm.$slot.xxx,xxx 為插槽名,當(dāng)組件執(zhí)行渲染函數(shù)時(shí)候,遇到slot標(biāo)簽,使用$slot中的內(nèi)容進(jìn)行替換,此時(shí)可以為插槽傳遞數(shù)據(jù),若存在數(shù)據(jù),則可稱(chēng)該插槽為作用域插槽。
Vue模板渲染的原理是什么?
vue中的模板template無(wú)法被瀏覽器解析并渲染,因?yàn)檫@不屬于瀏覽器的標(biāo)準(zhǔn),不是正確的HTML語(yǔ)法,所有需要將template轉(zhuǎn)化成一個(gè)JavaScript函數(shù),這樣瀏覽器就可以執(zhí)行這一個(gè)函數(shù)并渲染出對(duì)應(yīng)的HTML元素,就可以讓視圖跑起來(lái)了,這一個(gè)轉(zhuǎn)化的過(guò)程,就成為模板編譯。
模板編譯又分三個(gè)階段,解析parse,優(yōu)化optimize,生成generate,最終生成可執(zhí)行函數(shù)render。
parse階段:使用大量的正則表達(dá)式對(duì)template字符串進(jìn)行解析,將標(biāo)簽、指令、屬性等轉(zhuǎn)化為抽象語(yǔ)法樹(shù)AST。 optimize階段:遍歷AST,找到其中的一些靜態(tài)節(jié)點(diǎn)并進(jìn)行標(biāo)記,方便在頁(yè)面重渲染的時(shí)候進(jìn)行diff比較時(shí),直接跳過(guò)這一些靜態(tài)節(jié)點(diǎn),優(yōu)化runtime的性能。 generate階段:將最終的AST轉(zhuǎn)化為render函數(shù)字符串。
template預(yù)編譯是什么?
對(duì)于 Vue 組件來(lái)說(shuō),模板編譯只會(huì)在組件實(shí)例化的時(shí)候編譯一次,生成渲染函數(shù)之后在也不會(huì)進(jìn)行編譯。因此,編譯對(duì)組件的 runtime 是一種性能損耗。
而模板編譯的目的僅僅是將template轉(zhuǎn)化為render function,這個(gè)過(guò)程,正好可以在項(xiàng)目構(gòu)建的過(guò)程中完成,這樣可以讓實(shí)際組件在 runtime 時(shí)直接跳過(guò)模板渲染,進(jìn)而提升性能,這個(gè)在項(xiàng)目構(gòu)建的編譯template的過(guò)程,就是預(yù)編譯。
那template和jsx的有什么分別?
對(duì)于 runtime 來(lái)說(shuō),只需要保證組件存在 render 函數(shù)即可,而我們有了預(yù)編譯之后,我們只需要保證構(gòu)建過(guò)程中生成 render 函數(shù)就可以。
在 webpack 中,我們使用vue-loader編譯.vue文件,內(nèi)部依賴(lài)的vue-template-compiler模塊,在 webpack 構(gòu)建過(guò)程中,將template預(yù)編譯成 render 函數(shù)。
與 react 類(lèi)似,在添加了jsx的語(yǔ)法糖解析器babel-plugin-transform-vue-jsx之后,就可以直接手寫(xiě)render函數(shù)。
所以,template和jsx的都是render的一種表現(xiàn)形式,不同的是:
JSX相對(duì)于template而言,具有更高的靈活性,在復(fù)雜的組件中,更具有優(yōu)勢(shì),而 template 雖然顯得有些呆滯。但是 template 在代碼結(jié)構(gòu)上更符合視圖與邏輯分離的習(xí)慣,更簡(jiǎn)單、更直觀、更好維護(hù)。
說(shuō)一下什么是Virtual DOM
Virtual DOM 是 DOM 節(jié)點(diǎn)在 JavaScript 中的一種抽象數(shù)據(jù)結(jié)構(gòu),之所以需要虛擬DOM,是因?yàn)闉g覽器中操作DOM的代價(jià)比較昂貴,頻繁操作DOM會(huì)產(chǎn)生性能問(wèn)題。虛擬DOM的作用是在每一次響應(yīng)式數(shù)據(jù)發(fā)生變化引起頁(yè)面重渲染時(shí),Vue對(duì)比更新前后的虛擬DOM,匹配找出盡可能少的需要更新的真實(shí)DOM,從而達(dá)到提升性能的目的。
介紹一下Vue中的Diff算法
在新老虛擬DOM對(duì)比時(shí)
首先,對(duì)比節(jié)點(diǎn)本身,判斷是否為同一節(jié)點(diǎn),如果不為相同節(jié)點(diǎn),則刪除該節(jié)點(diǎn)重新創(chuàng)建節(jié)點(diǎn)進(jìn)行替換 如果為相同節(jié)點(diǎn),進(jìn)行patchVnode,判斷如何對(duì)該節(jié)點(diǎn)的子節(jié)點(diǎn)進(jìn)行處理,先判斷一方有子節(jié)點(diǎn)一方?jīng)]有子節(jié)點(diǎn)的情況(如果新的children沒(méi)有子節(jié)點(diǎn),將舊的子節(jié)點(diǎn)移除) 比較如果都有子節(jié)點(diǎn),則進(jìn)行updateChildren,判斷如何對(duì)這些新老節(jié)點(diǎn)的子節(jié)點(diǎn)進(jìn)行操作(diff核心)。 匹配時(shí),找到相同的子節(jié)點(diǎn),遞歸比較子節(jié)點(diǎn)
在diff中,只對(duì)同層的子節(jié)點(diǎn)進(jìn)行比較,放棄跨級(jí)的節(jié)點(diǎn)比較,使得時(shí)間復(fù)雜從O(n^3)降低值O(n),也就是說(shuō),只有當(dāng)新舊children都為多個(gè)子節(jié)點(diǎn)時(shí)才需要用核心的Diff算法進(jìn)行同層級(jí)比較。
key屬性的作用是什么
在對(duì)節(jié)點(diǎn)進(jìn)行diff的過(guò)程中,判斷是否為相同節(jié)點(diǎn)的一個(gè)很重要的條件是key是否相等,如果是相同節(jié)點(diǎn),則會(huì)盡可能的復(fù)用原有的DOM節(jié)點(diǎn)。所以key屬性是提供給框架在diff的時(shí)候使用的,而非開(kāi)發(fā)者。
說(shuō)說(shuō)Vue2.0和Vue3.0有什么區(qū)別
重構(gòu)響應(yīng)式系統(tǒng),使用Proxy替換Object.defineProperty,使用Proxy優(yōu)勢(shì):
可直接監(jiān)聽(tīng)數(shù)組類(lèi)型的數(shù)據(jù)變化 監(jiān)聽(tīng)的目標(biāo)為對(duì)象本身,不需要像Object.defineProperty一樣遍歷每個(gè)屬性,有一定的性能提升 可攔截apply、ownKeys、has等13種方法,而Object.defineProperty不行 直接實(shí)現(xiàn)對(duì)象屬性的新增/刪除 新增Composition API,更好的邏輯復(fù)用和代碼組織
重構(gòu) Virtual DOM
模板編譯時(shí)的優(yōu)化,將一些靜態(tài)節(jié)點(diǎn)編譯成常量 slot優(yōu)化,將slot編譯為lazy函數(shù),將slot的渲染的決定權(quán)交給子組件 模板中內(nèi)聯(lián)事件的提取并重用(原本每次渲染都重新生成內(nèi)聯(lián)函數(shù)) 代碼結(jié)構(gòu)調(diào)整,更便于Tree shaking,使得體積更小
使用Typescript替換Flow
為什么要新增Composition API,它能解決什么問(wèn)題
Vue2.0中,隨著功能的增加,組件變得越來(lái)越復(fù)雜,越來(lái)越難維護(hù),而難以維護(hù)的根本原因是Vue的API設(shè)計(jì)迫使開(kāi)發(fā)者使用watch,computed,methods選項(xiàng)組織代碼,而不是實(shí)際的業(yè)務(wù)邏輯。
另外Vue2.0缺少一種較為簡(jiǎn)潔的低成本的機(jī)制來(lái)完成邏輯復(fù)用,雖然可以minxis完成邏輯復(fù)用,但是當(dāng)mixin變多的時(shí)候,會(huì)使得難以找到對(duì)應(yīng)的data、computed或者method來(lái)源于哪個(gè)mixin,使得類(lèi)型推斷難以進(jìn)行。
所以Composition API的出現(xiàn),主要是也是為了解決Option API帶來(lái)的問(wèn)題,第一個(gè)是代碼組織問(wèn)題,Compostion API可以讓開(kāi)發(fā)者根據(jù)業(yè)務(wù)邏輯組織自己的代碼,讓代碼具備更好的可讀性和可擴(kuò)展性,也就是說(shuō)當(dāng)下一個(gè)開(kāi)發(fā)者接觸這一段不是他自己寫(xiě)的代碼時(shí),他可以更好的利用代碼的組織反推出實(shí)際的業(yè)務(wù)邏輯,或者根據(jù)業(yè)務(wù)邏輯更好的理解代碼。
第二個(gè)是實(shí)現(xiàn)代碼的邏輯提取與復(fù)用,當(dāng)然mixin也可以實(shí)現(xiàn)邏輯提取與復(fù)用,但是像前面所說(shuō)的,多個(gè)mixin作用在同一個(gè)組件時(shí),很難看出property是來(lái)源于哪個(gè)mixin,來(lái)源不清楚,另外,多個(gè)mixin的property存在變量命名沖突的風(fēng)險(xiǎn)。而Composition API剛好解決了這兩個(gè)問(wèn)題。
都說(shuō)Composition API與React Hook很像,說(shuō)說(shuō)區(qū)別
從React Hook的實(shí)現(xiàn)角度看,React Hook是根據(jù)useState調(diào)用的順序來(lái)確定下一次重渲染時(shí)的state是來(lái)源于哪個(gè)useState,所以出現(xiàn)了以下限制
不能在循環(huán)、條件、嵌套函數(shù)中調(diào)用Hook 必須確保總是在你的React函數(shù)的頂層調(diào)用Hook useEffect、useMemo等函數(shù)必須手動(dòng)確定依賴(lài)關(guān)系
而Composition API是基于Vue的響應(yīng)式系統(tǒng)實(shí)現(xiàn)的,與React Hook的相比
聲明在setup函數(shù)內(nèi),一次組件實(shí)例化只調(diào)用一次setup,而React Hook每次重渲染都需要調(diào)用Hook,使得React的GC比Vue更有壓力,性能也相對(duì)于Vue來(lái)說(shuō)也較慢 Compositon API的調(diào)用不需要顧慮調(diào)用順序,也可以在循環(huán)、條件、嵌套函數(shù)中使用 響應(yīng)式系統(tǒng)自動(dòng)實(shí)現(xiàn)了依賴(lài)收集,進(jìn)而組件的部分的性能優(yōu)化由Vue內(nèi)部自己完成,而React Hook需要手動(dòng)傳入依賴(lài),而且必須必須保證依賴(lài)的順序,讓useEffect、useMemo等函數(shù)正確的捕獲依賴(lài)變量,否則會(huì)由于依賴(lài)不正確使得組件性能下降。
雖然Compositon API看起來(lái)比React Hook好用,但是其設(shè)計(jì)思想也是借鑒React Hook的。
SSR有了解嗎?原理是什么?
在客戶端請(qǐng)求服務(wù)器的時(shí)候,服務(wù)器到數(shù)據(jù)庫(kù)中獲取到相關(guān)的數(shù)據(jù),并且在服務(wù)器內(nèi)部將Vue組件渲染成HTML,并且將數(shù)據(jù)、HTML一并返回給客戶端,這個(gè)在服務(wù)器將數(shù)據(jù)和組件轉(zhuǎn)化為HTML的過(guò)程,叫做服務(wù)端渲染SSR。
而當(dāng)客戶端拿到服務(wù)器渲染的HTML和數(shù)據(jù)之后,由于數(shù)據(jù)已經(jīng)有了,客戶端不需要再一次請(qǐng)求數(shù)據(jù),而只需要將數(shù)據(jù)同步到組件或者Vuex內(nèi)部即可。除了數(shù)據(jù)意外,HTML也結(jié)構(gòu)已經(jīng)有了,客戶端在渲染組件的時(shí)候,也只需要將HTML的DOM節(jié)點(diǎn)映射到Virtual DOM即可,不需要重新創(chuàng)建DOM節(jié)點(diǎn),這個(gè)將數(shù)據(jù)和HTML同步的過(guò)程,又叫做客戶端激活。
使用SSR的好處:
有利于SEO:其實(shí)就是有利于爬蟲(chóng)來(lái)爬你的頁(yè)面,因?yàn)椴糠猪?yè)面爬蟲(chóng)是不支持執(zhí)行JavaScript的,這種不支持執(zhí)行JavaScript的爬蟲(chóng)抓取到的非SSR的頁(yè)面會(huì)是一個(gè)空的HTML頁(yè)面,而有了SSR以后,這些爬蟲(chóng)就可以獲取到完整的HTML結(jié)構(gòu)的數(shù)據(jù),進(jìn)而收錄到搜索引擎中。 白屏?xí)r間更短:相對(duì)于客戶端渲染,服務(wù)端渲染在瀏覽器請(qǐng)求URL之后已經(jīng)得到了一個(gè)帶有數(shù)據(jù)的HTML文本,瀏覽器只需要解析HTML,直接構(gòu)建DOM樹(shù)就可以。而客戶端渲染,需要先得到一個(gè)空的HTML頁(yè)面,這個(gè)時(shí)候頁(yè)面已經(jīng)進(jìn)入白屏,之后還需要經(jīng)過(guò)加載并執(zhí)行 JavaScript、請(qǐng)求后端服務(wù)器獲取數(shù)據(jù)、JavaScript 渲染頁(yè)面幾個(gè)過(guò)程才可以看到最后的頁(yè)面。特別是在復(fù)雜應(yīng)用中,由于需要加載 JavaScript 腳本,越是復(fù)雜的應(yīng)用,需要加載的 JavaScript 腳本就越多、越大,這會(huì)導(dǎo)致應(yīng)用的首屏加載時(shí)間非常長(zhǎng),進(jìn)而降低了體驗(yàn)感。
更多詳情查看 徹底理解服務(wù)端渲染-SSR原理 https://github.com/yacan8/blog/issues/30
結(jié)束
面試官點(diǎn)了點(diǎn)頭,嗯呢,這小伙還可以,懂得還挺多,可以弄進(jìn)來(lái)寫(xiě)業(yè)務(wù)。
我也暗自竊喜,幸虧沒(méi)問(wèn)到我不會(huì)的,然后我坐那傻笑,笑著笑著,突然聽(tīng)到我的鬧鈴響了,然后,我夢(mèng)醒了。
然后,新的搬磚的一天又開(kāi)始了。

