前端面試 Vue篇

2023-02-17 10:50 更新

一、Vue 基礎(chǔ)


1. Vue的基本原理

當(dāng)一個Vue實例創(chuàng)建時,Vue會遍歷data中的屬性,用 Object.defineProperty(vue3.0使用proxy )將它們轉(zhuǎn)為 getter/setter,并且在內(nèi)部追蹤相關(guān)依賴,在屬性被訪問和修改時通知變化。 每個組件實例都有相應(yīng)的 watcher 程序?qū)嵗?,它會在組件渲染的過程中把屬性記錄為依賴,之后當(dāng)依賴項的setter被調(diào)用時,會通知watcher重新計算,從而致使它關(guān)聯(lián)的組件得以更新。


2. 雙向數(shù)據(jù)綁定的原理

Vue.js 是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數(shù)據(jù)變動時發(fā)布消息給訂閱者,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)。主要分為以下幾個步驟:

  1. 需要observe的數(shù)據(jù)對象進行遞歸遍歷,包括子屬性對象的屬性,都加上setter和getter這樣的話,給這個對象的某個值賦值,就會觸發(fā)setter,那么就能監(jiān)聽到了數(shù)據(jù)變化
  2. compile解析模板指令,將模板中的變量替換成數(shù)據(jù),然后初始化渲染頁面視圖,并將每個指令對應(yīng)的節(jié)點綁定更新函數(shù),添加監(jiān)聽數(shù)據(jù)的訂閱者,一旦數(shù)據(jù)有變動,收到通知,更新視圖
  3. Watcher訂閱者是Observer和Compile之間通信的橋梁,主要做的事情是: ①在自身實例化時往屬性訂閱器(dep)里面添加自己 ②自身必須有一個update()方法 ③待屬性變動dep.notice()通知時,能調(diào)用自身的update()方法,并觸發(fā)Compile中綁定的回調(diào),則功成身退。
  4. MVVM作為數(shù)據(jù)綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監(jiān)聽自己的model數(shù)據(jù)變化,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達到數(shù)據(jù)變化 -> 視圖更新;視圖交互變化(input) -> 數(shù)據(jù)model變更的雙向綁定效果。


3. 使用 Object.defineProperty() 來進行數(shù)據(jù)劫持有什么缺點?

在對一些屬性進行操作時,使用這種方法無法攔截,比如通過下標(biāo)方式修改數(shù)組數(shù)據(jù)或者給對象新增屬性,這都不能觸發(fā)組件的重新渲染,因為 Object.defineProperty 不能攔截到這些操作。更精確的來說,對于數(shù)組而言,大部分操作都是攔截不到的,只是 Vue 內(nèi)部通過重寫函數(shù)的方式解決了這個問題。

在 Vue3.0 中已經(jīng)不使用這種方式了,而是通過使用 Proxy 對對象進行代理,從而實現(xiàn)數(shù)據(jù)劫持。使用Proxy 的好處是它可以完美的監(jiān)聽到任何方式的數(shù)據(jù)改變,唯一的缺點是兼容性的問題,因為 Proxy 是 ES6 的語法。

4. MVVM、MVC、MVP的區(qū)別

MVC、MVP 和 MVVM 是三種常見的軟件架構(gòu)設(shè)計模式,主要通過分離關(guān)注點的方式來組織代碼結(jié)構(gòu),優(yōu)化開發(fā)效率。

在開發(fā)單頁面應(yīng)用時,往往一個路由頁面對應(yīng)了一個腳本文件,所有的頁面邏輯都在一個腳本文件里。頁面的渲染、數(shù)據(jù)的獲取,對用戶事件的響應(yīng)所有的應(yīng)用邏輯都混合在一起,這樣在開發(fā)簡單項目時,可能看不出什么問題,如果項目變得復(fù)雜,那么整個文件就會變得冗長、混亂,這樣對項目開發(fā)和后期的項目維護是非常不利的。

(1)MVC

MVC 通過分離 Model、View 和 Controller 的方式來組織代碼結(jié)構(gòu)。其中 View 負(fù)責(zé)頁面的顯示邏輯,Model 負(fù)責(zé)存儲頁面的業(yè)務(wù)數(shù)據(jù),以及對相應(yīng)數(shù)據(jù)的操作。并且 View 和 Model 應(yīng)用了觀察者模式,當(dāng) Model 層發(fā)生改變的時候它會通知有關(guān) View 層更新頁面。Controller 層是 View 層和 Model 層的紐帶,它主要負(fù)責(zé)用戶與應(yīng)用的響應(yīng)操作,當(dāng)用戶與頁面產(chǎn)生交互的時候,Controller 中的事件觸發(fā)器就開始工作了,通過調(diào)用 Model 層,來完成對 Model 的修改,然后 Model 層再去通知 View 層更新。


(2)MVVM

MVVM 分為 Model、View、ViewModel:

  • Model代表數(shù)據(jù)模型,數(shù)據(jù)和業(yè)務(wù)邏輯都在Model層中定義;
  • View代表UI視圖,負(fù)責(zé)數(shù)據(jù)的展示;
  • ViewModel負(fù)責(zé)監(jiān)聽Model中數(shù)據(jù)的改變并且控制視圖的更新,處理用戶交互操作;

Model和View并無直接關(guān)聯(lián),而是通過ViewModel來進行聯(lián)系的,Model和ViewModel之間有著雙向數(shù)據(jù)綁定的聯(lián)系。因此當(dāng)Model中的數(shù)據(jù)改變時會觸發(fā)View層的刷新,View中由于用戶交互操作而改變的數(shù)據(jù)也會在Model中同步。

這種模式實現(xiàn)了 Model和View的數(shù)據(jù)自動同步,因此開發(fā)者只需要專注于數(shù)據(jù)的維護操作即可,而不需要自己操作DOM。


(3)MVP

MVP 模式與 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用觀察者模式,來實現(xiàn)當(dāng) Model 層數(shù)據(jù)發(fā)生變化的時候,通知 View 層的更新。這樣 View 層和 Model 層耦合在一起,當(dāng)項目邏輯變得復(fù)雜的時候,可能會造成代碼的混亂,并且可能會對代碼的復(fù)用性造成一些問題。MVP 的模式通過使用 Presenter 來實現(xiàn)對 View 層和 Model 層的解耦。MVC 中的Controller 只知道 Model 的接口,因此它沒有辦法控制 View 層的更新,MVP 模式中,View 層的接口暴露給了 Presenter 因此可以在 Presenter 中將 Model 的變化和 View 的變化綁定在一起,以此來實現(xiàn) View 和 Model 的同步更新。這樣就實現(xiàn)了對 View 和 Model 的解耦,Presenter 還包含了其他的響應(yīng)邏輯。

5. Computed 和 Watch 的區(qū)別

對于Computed:

  • 它支持緩存,只有依賴的數(shù)據(jù)發(fā)生了變化,才會重新計算
  • 不支持異步,當(dāng)Computed中有異步操作時,無法監(jiān)聽數(shù)據(jù)的變化
  • computed的值會默認(rèn)走緩存,計算屬性是基于它們的響應(yīng)式依賴進行緩存的,也就是基于data聲明過,或者父組件傳遞過來的props中的數(shù)據(jù)進行計算的。
  • 如果一個屬性是由其他屬性計算而來的,這個屬性依賴其他的屬性,一般會使用computed
  • 如果computed屬性的屬性值是函數(shù),那么默認(rèn)使用get方法,函數(shù)的返回值就是屬性的屬性值;在computed中,屬性有一個get方法和一個set方法,當(dāng)數(shù)據(jù)發(fā)生變化時,會調(diào)用set方法。

對于Watch:

  • 它不支持緩存,數(shù)據(jù)變化時,它就會觸發(fā)相應(yīng)的操作
  • 支持異步監(jiān)聽
  • 監(jiān)聽的函數(shù)接收兩個參數(shù),第一個參數(shù)是最新的值,第二個是變化之前的值
  • 當(dāng)一個屬性發(fā)生變化時,就需要執(zhí)行相應(yīng)的操作
    • 監(jiān)聽數(shù)據(jù)必須是data中聲明的或者父組件傳遞過來的props中的數(shù)據(jù),當(dāng)發(fā)生變化時,會觸發(fā)其他操作,函數(shù)有兩個的參數(shù):
    • immediate:組件加載立即觸發(fā)回調(diào)函數(shù)deep:深度監(jiān)聽,發(fā)現(xiàn)數(shù)據(jù)內(nèi)部的變化,在復(fù)雜數(shù)據(jù)類型中使用,例如數(shù)組中的對象發(fā)生變化。需要注意的是,deep無法監(jiān)聽到數(shù)組和對象內(nèi)部的變化。

當(dāng)想要執(zhí)行異步或者昂貴的操作以響應(yīng)不斷的變化時,就需要使用watch。

總結(jié):

  • computed 計算屬性 : 依賴其它屬性值,并且 computed 的值有緩存,只有它依賴的屬性值發(fā)生改變,下一次獲取 computed 的值時才會重新計算 computed 的值。
  • watch 偵聽器 : 更多的是觀察的作用,無緩存性,類似于某些數(shù)據(jù)的監(jiān)聽回調(diào),每當(dāng)監(jiān)聽的數(shù)據(jù)變化時都會執(zhí)行回調(diào)進行后續(xù)操作。

運用場景:

  • 當(dāng)需要進行數(shù)值計算,并且依賴于其它數(shù)據(jù)時,應(yīng)該使用 computed,因為可以利用 computed 的緩存特性,避免每次獲取值時都要重新計算。
  • 當(dāng)需要在數(shù)據(jù)變化時執(zhí)行異步或開銷較大的操作時,應(yīng)該使用 watch,使用 watch 選項允許執(zhí)行異步操作 ( 訪問一個 API ),限制執(zhí)行該操作的頻率,并在得到最終結(jié)果前,設(shè)置中間狀態(tài)。這些都是計算屬性無法做到的。

6. Computed 和 Methods 的區(qū)別

可以將同一函數(shù)定義為一個 method 或者一個計算屬性。對于最終的結(jié)果,兩種方式是相同的

不同點:

  • computed: 計算屬性是基于它們的依賴進行緩存的,只有在它的相關(guān)依賴發(fā)生改變時才會重新求值;
  • method 調(diào)用總會執(zhí)行該函數(shù)。

7. slot是什么?有什么作用?原理是什么?

slot又名插槽,是Vue的內(nèi)容分發(fā)機制,組件內(nèi)部的模板引擎使用slot元素作為承載分發(fā)內(nèi)容的出口。插槽slot是子組件的一個模板標(biāo)簽元素,而這一個標(biāo)簽元素是否顯示,以及怎么顯示是由父組件決定的。slot又分三類,默認(rèn)插槽,具名插槽和作用域插槽。

  • 默認(rèn)插槽:又名匿名插槽,當(dāng)slot沒有指定name屬性值的時候一個默認(rèn)顯示插槽,一個組件內(nèi)只有有一個匿名插槽。
  • 具名插槽:帶有具體名字的插槽,也就是帶有name屬性的slot,一個組件可以出現(xiàn)多個具名插槽。
  • 作用域插槽:默認(rèn)插槽、具名插槽的一個變體,可以是匿名插槽,也可以是具名插槽,該插槽的不同點是在子組件渲染作用域插槽時,可以將子組件內(nèi)部的數(shù)據(jù)傳遞給父組件,讓父組件根據(jù)子組件的傳遞過來的數(shù)據(jù)決定如何渲染該插槽。

實現(xiàn)原理:當(dāng)子組件vm實例化時,獲取到父組件傳入的slot標(biāo)簽的內(nèi)容,存放在 vm.$slot中,默認(rèn)插槽為 vm.$slot.default,具名插槽為 vm.$slot.xxx,xxx 為插槽名,當(dāng)組件執(zhí)行渲染函數(shù)時候,遇到slot標(biāo)簽,使用 $slot中的內(nèi)容進行替換,此時可以為插槽傳遞數(shù)據(jù),若存在數(shù)據(jù),則可稱該插槽為作用域插槽。

8. 過濾器的作用,如何實現(xiàn)一個過濾器

根據(jù)過濾器的名稱,過濾器是用來過濾數(shù)據(jù)的,在Vue中使用 filters來過濾數(shù)據(jù),filters不會修改數(shù)據(jù),而是過濾數(shù)據(jù),改變用戶看到的輸出(計算屬性 computed ,方法 methods 都是通過修改數(shù)據(jù)來處理數(shù)據(jù)格式的輸出顯示)。

使用場景:

  • 需要格式化數(shù)據(jù)的情況,比如需要處理時間、價格等數(shù)據(jù)格式的輸出 / 顯示。
  • 比如后端返回一個 年月日的日期字符串,前端需要展示為 多少天前 的數(shù)據(jù)格式,此時就可以用 ?fliters?過濾器來處理數(shù)據(jù)。

過濾器是一個函數(shù),它會把表達式中的值始終當(dāng)作函數(shù)的第一個參數(shù)。過濾器用在插值表達式 {{ }} 和 v-bind 表達式 中,然后放在操作符“ | ”后面進行指示。

例如,在顯示金額,給商品價格添加單位:

<li>商品價格:{{item.price | filterPrice}}</li>

 filters: {
    filterPrice (price) {
      return price ? ('¥' + price) : '--'
    }
  }

9. 如何保存頁面的當(dāng)前的狀態(tài)

既然是要保持頁面的狀態(tài)(其實也就是組件的狀態(tài)),那么會出現(xiàn)以下兩種情況:

  • 前組件會被卸載
  • 前組件不會被卸載

那么可以按照這兩種情況分別得到以下方法:

組件會被卸載:

(1)將狀態(tài)存儲在LocalStorage / SessionStorage

只需要在組件即將被銷毀的生命周期 componentWillUnmount (react)中在 LocalStorage / SessionStorage 中把當(dāng)前組件的 state 通過 JSON.stringify() 儲存下來就可以了。在這里面需要注意的是組件更新狀態(tài)的時機。

比如從 B 組件跳轉(zhuǎn)到 A 組件的時候,A 組件需要更新自身的狀態(tài)。但是如果從別的組件跳轉(zhuǎn)到 B 組件的時候,實際上是希望 B 組件重新渲染的,也就是不要從 Storage 中讀取信息。所以需要在 Storage 中的狀態(tài)加入一個 flag 屬性,用來控制 A 組件是否讀取 Storage 中的狀態(tài)。

優(yōu)點
  • 兼容性好,不需要額外庫或工具。
  • 簡單快捷,基本可以滿足大部分需求。
缺點
  • 狀態(tài)通過 JSON 方法儲存(相當(dāng)于深拷貝),如果狀態(tài)中有特殊情況(比如 Date 對象、Regexp 對象等)的時候會得到字符串而不是原來的值。(具體參考用 JSON 深拷貝的缺點)
  • 如果 B 組件后退或者下一頁跳轉(zhuǎn)并不是前組件,那么 flag 判斷會失效,導(dǎo)致從其他頁面進入 A 組件頁面時 A 組件會重新讀取 Storage,會造成很奇怪的現(xiàn)象

(2)路由傳值

通過 react-router 的 Link 組件的 prop —— to 可以實現(xiàn)路由間傳遞參數(shù)的效果。

在這里需要用到 state 參數(shù),在 B 組件中通過 history.location.state 就可以拿到 state 值,保存它。返回 A 組件時再次攜帶 state 達到路由狀態(tài)保持的效果。

優(yōu)點
  • 簡單快捷,不會污染 LocalStorage / SessionStorage。
  • 可以傳遞 Date、RegExp 等特殊對象(不用擔(dān)心 JSON.stringify / parse 的不足)
缺點
  • 如果 A 組件可以跳轉(zhuǎn)至多個組件,那么在每一個跳轉(zhuǎn)組件內(nèi)都要寫相同的邏輯。

組件不會被卸載:

(1)單頁面渲染

要切換的組件作為子組件全屏渲染,父組件中正常儲存頁面狀態(tài)。

優(yōu)點
  • 代碼量少
  • 不需要考慮狀態(tài)傳遞過程中的錯誤
缺點
  • 增加 A 組件維護成本
  • 需要傳入額外的 prop 到 B 組件
  • 無法利用路由定位頁面

除此之外,在Vue中,還可以是用keep-alive來緩存頁面,當(dāng)組件在keep-alive內(nèi)被切換時組件的activated、deactivated這兩個生命周期鉤子函數(shù)會被執(zhí)行

被包裹在keep-alive中的組件的狀態(tài)將會被保留:

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</kepp-alive>

router.js

{
  path: '/',
  name: 'xxx',
  component: ()=>import('../src/views/xxx.vue'),
  meta:{
    keepAlive: true // 需要被緩存
  }
},

10. 常見的事件修飾符及其作用

  • ?.stop?:等同于 JavaScript 中的 ?event.stopPropagation()? ,防止事件冒泡;
  • ?.prevent? :等同于 JavaScript 中的 ?event.preventDefault()? ,防止執(zhí)行預(yù)設(shè)的行為(如果事件可取消,則取消該事件,而不停止事件的進一步傳播);
  • ?.capture? :與事件冒泡的方向相反,事件捕獲由外到內(nèi);
  • ?.self? :只會觸發(fā)自己范圍內(nèi)的事件,不包含子元素;
  • ?.once? :只會觸發(fā)一次。

11. v-if、v-show、v-html 的原理

  • v-if會調(diào)用addIfCondition方法,生成vnode的時候會忽略對應(yīng)節(jié)點,render的時候就不會渲染;
  • v-show會生成vnode,render的時候也會渲染成真實節(jié)點,只是在render過程中會在節(jié)點的屬性中修改show屬性值,也就是常說的display;
  • v-html會先移除節(jié)點下的所有節(jié)點,調(diào)用html方法,通過addProp添加innerHTML屬性,歸根結(jié)底還是設(shè)置innerHTML為v-html的值。

13. v-if和v-show的區(qū)別

  • 手段:v-if是動態(tài)的向DOM樹內(nèi)添加或者刪除DOM元素;v-show是通過設(shè)置DOM元素的display樣式屬性控制顯隱;
  • 編譯過程:v-if切換有一個局部編譯/卸載的過程,切換過程中合適地銷毀和重建內(nèi)部的事件監(jiān)聽和子組件;v-show只是簡單的基于css切換;
  • 編譯條件:v-if是惰性的,如果初始條件為假,則什么也不做;只有在條件第一次變?yōu)檎鏁r才開始局部編譯; v-show是在任何條件下,無論首次條件是否為真,都被編譯,然后被緩存,而且DOM元素保留;
  • 性能消耗:v-if有更高的切換消耗;v-show有更高的初始渲染消耗;
  • 使用場景:v-if適合運營條件不大可能改變;v-show適合頻繁切換。

14. v-model 是如何實現(xiàn)的,語法糖實際是什么?

(1)作用在表單元素上

動態(tài)綁定了 input 的 value 指向了 messgae 變量,并且在觸發(fā) input 事件的時候去動態(tài)把 message設(shè)置為目標(biāo)值:

<input v-model="sth" />
//  等同于
<input 
    v-bind:value="message" 
    v-on:input="message=$event.target.value"
>
//$event 指代當(dāng)前觸發(fā)的事件對象;
//$event.target 指代當(dāng)前觸發(fā)的事件對象的dom;
//$event.target.value 就是當(dāng)前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;

(2)作用在組件上

在自定義組件中,v-model 默認(rèn)會利用名為 value 的 prop和名為 input 的事件

本質(zhì)是一個父子組件通信的語法糖,通過prop和$.emit實現(xiàn)。因此父組件 v-model 語法糖本質(zhì)上可以修改為:

<child :value="message"  @input="function(e){message = e}"></child>

在組件的實現(xiàn)中,可以通過 v-model屬性來配置子組件接收的prop名稱,以及派發(fā)的事件名稱。

例子:

// 父組件
<aa-input v-model="aa"></aa-input>
// 等價于
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>

// 子組件:
<input v-bind:value="aa" v-on:input="onmessage"></aa-input>

props:{value:aa,}
methods:{
    onmessage(e){
        $emit('input',e.target.value)
    }
}

默認(rèn)情況下,一個組件上的v-model 會把 value 用作 prop且把 input 用作 event。但是一些輸入類型比如單選框和復(fù)選框按鈕可能想使用 value prop 來達到不同的目的。使用 model 選項可以回避這些情況產(chǎn)生的沖突。js 監(jiān)聽input 輸入框輸入數(shù)據(jù)改變,用oninput,數(shù)據(jù)改變以后就會立刻出發(fā)這個事件。通過input事件把數(shù)據(jù)$emit 出去,在父組件接受。父組件設(shè)置v-model的值為input $emit過來的值。

15. v-model 可以被用在自定義組件上嗎?如果可以,如何使用?

可以。v-model 實際上是一個語法糖,如:

<input v-model="searchText">

實際上相當(dāng)于:

<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

用在自定義組件上也是同理:

<custom-input v-model="searchText">

相當(dāng)于:

<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>

顯然,custom-input 與父組件的交互如下:

  1. 父組件將 ?searchText?變量傳入custom-input 組件,使用的 prop 名為 ?value?;
  2. custom-input 組件向父組件傳出名為 ?input?的事件,父組件將接收到的值賦值給 ?searchText?;

所以,custom-input 組件的實現(xiàn)應(yīng)該類似于這樣:

Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    >
  `
})

16. data為什么是一個函數(shù)而不是對象

JavaScript中的對象是引用類型的數(shù)據(jù),當(dāng)多個實例引用同一個對象時,只要一個實例對這個對象進行操作,其他實例中的數(shù)據(jù)也會發(fā)生變化。

而在Vue中,更多的是想要復(fù)用組件,那就需要每個組件都有自己的數(shù)據(jù),這樣組件之間才不會相互干擾。

所以組件的數(shù)據(jù)不能寫成對象的形式,而是要寫成函數(shù)的形式。數(shù)據(jù)以函數(shù)返回值的形式定義,這樣當(dāng)每次復(fù)用組件的時候,就會返回一個新的data,也就是說每個組件都有自己的私有數(shù)據(jù)空間,它們各自維護自己的數(shù)據(jù),不會干擾其他組件的正常運行。

17. 對keep-alive的理解,它是如何實現(xiàn)的,具體緩存的是什么?

如果需要在組件切換的時候,保存一些組件的狀態(tài)防止多次渲染,就可以使用 keep-alive 組件包裹需要保存的組件。

(1)keep-alive

keep-alive有以下三個屬性:

  • include 字符串或正則表達式,只有名稱匹配的組件會被匹配;
  • exclude 字符串或正則表達式,任何名稱匹配的組件都不會被緩存;
  • max 數(shù)字,最多可以緩存多少組件實例。

注意:keep-alive 包裹動態(tài)組件時,會緩存不活動的組件實例。

主要流程

  1. 判斷組件 name ,不在 include 或者在 exclude 中,直接返回 vnode,說明該組件不被緩存。
  2. 獲取組件實例 key ,如果有獲取實例的 key,否則重新生成。
  3. key生成規(guī)則,cid +"∶∶"+ tag ,僅靠cid是不夠的,因為相同的構(gòu)造函數(shù)可以注冊為不同的本地組件。
  4. 如果緩存對象內(nèi)存在,則直接從緩存對象中獲取組件實例給 vnode ,不存在則添加到緩存對象中。 5.最大緩存數(shù)量,當(dāng)緩存組件數(shù)量超過 max 值時,清除 keys 數(shù)組內(nèi)第一個組件。

(2)keep-alive 的實現(xiàn)

const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正則,數(shù)組

export default {
  name: 'keep-alive',
  abstract: true, // 抽象組件,是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出現(xiàn)在父組件鏈中。

  props: {
    include: patternTypes, // 匹配的組件,緩存
    exclude: patternTypes, // 不去匹配的組件,不緩存
    max: [String, Number], // 緩存組件的最大實例數(shù)量, 由于緩存的是組件實例(vnode),數(shù)量過多的時候,會占用過多的內(nèi)存,可以用max指定上限
  },

  created() {
    // 用于初始化緩存虛擬DOM數(shù)組和vnode的key
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed() {
    // 銷毀緩存cache的組件實例
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted() {
    // prune 削減精簡[v.]
    // 去監(jiān)控include和exclude的改變,根據(jù)最新的include和exclude的內(nèi)容,來實時削減緩存的組件的內(nèi)容
    this.$watch('include', (val) => {
      pruneCache(this, (name) => matches(val, name))
    })
    this.$watch('exclude', (val) => {
      pruneCache(this, (name) => !matches(val, name))
    })
  },
}

render函數(shù):

  1. 會在 keep-alive 組件內(nèi)部去寫自己的內(nèi)容,所以可以去獲取默認(rèn) slot 的內(nèi)容,然后根據(jù)這個去獲取組件
  2. keep-alive 只對第一個組件有效,所以獲取第一個子組件。
  3. 和 keep-alive 搭配使用的一般有:動態(tài)組件 和router-view
render () {
  //
  function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
    if (Array.isArray(children)) {
  for (let i = 0; i < children.length; i++) {
    const c = children[i]
    if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
      return c
    }
  }
  }
  }
  const slot = this.$slots.default // 獲取默認(rèn)插槽
  const vnode: VNode = getFirstComponentChild(slot)// 獲取第一個子組件
  const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 組件參數(shù)
  if (componentOptions) { // 是否有組件參數(shù)
    // check pattern
    const name: ?string = getComponentName(componentOptions) // 獲取組件名
    const { include, exclude } = this
    if (
      // not included
      (include && (!name || !matches(include, name))) ||
      // excluded
      (exclude && name && matches(exclude, name))
    ) {
      // 如果不匹配當(dāng)前組件的名字和include以及exclude
      // 那么直接返回組件的實例
      return vnode
    }

    const { cache, keys } = this

    // 獲取這個組件的key
    const key: ?string = vnode.key == null
      // same constructor may get registered as different local components
      // so cid alone is not enough (#3269)
      ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
      : vnode.key

    if (cache[key]) {
      // LRU緩存策略執(zhí)行
      vnode.componentInstance = cache[key].componentInstance // 組件初次渲染的時候componentInstance為undefined

      // make current key freshest
      remove(keys, key)
      keys.push(key)
      // 根據(jù)LRU緩存策略執(zhí)行,將key從原來的位置移除,然后將這個key值放到最后面
    } else {
      // 在緩存列表里面沒有的話,則加入,同時判斷當(dāng)前加入之后,是否超過了max所設(shè)定的范圍,如果是,則去除
      // 使用時間間隔最長的一個
      cache[key] = vnode
      keys.push(key)
      // prune oldest entry
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }
    // 將組件的keepAlive屬性設(shè)置為true
    vnode.data.keepAlive = true // 作用:判斷是否要執(zhí)行組件的created、mounted生命周期函數(shù)
  }
  return vnode || (slot && slot[0])
}

keep-alive 具體是通過 cache 數(shù)組緩存所有組件的 vnode 實例。當(dāng) cache 內(nèi)原有組件被使用時會將該組件 key 從 keys 數(shù)組中刪除,然后 push 到 keys數(shù)組最后,以便清除最不常用組件。

實現(xiàn)步驟:

  1. 獲取 keep-alive 下第一個子組件的實例對象,通過他去獲取這個組件的組件名
  2. 通過當(dāng)前組件名去匹配原來 include 和 exclude,判斷當(dāng)前組件是否需要緩存,不需要緩存,直接返回當(dāng)前組件的實例vNode
  3. 需要緩存,判斷他當(dāng)前是否在緩存數(shù)組里面:
    • 存在,則將他原來位置上的 key 給移除,同時將這個組件的 key 放到數(shù)組最后面(LRU)
    • 不存在,將組件 key 放入數(shù)組,然后判斷當(dāng)前 key數(shù)組是否超過 max 所設(shè)置的范圍,超過,那么削減未使用時間最長的一個組件的 key
  4. 最后將這個組件的 keepAlive 設(shè)置為 true

(3)keep-alive 本身的創(chuàng)建過程和 patch 過程

緩存渲染的時候,會根據(jù) vnode.componentInstance(首次渲染 vnode.componentInstance 為 undefined) 和 keepAlive 屬性判斷不會執(zhí)行組件的 created、mounted 等鉤子函數(shù),而是對緩存的組件執(zhí)行 patch 過程∶ 直接把緩存的 DOM 對象直接插入到目標(biāo)元素中,完成了數(shù)據(jù)更新的情況下的渲染過程。

首次渲染

  • 組件的首次渲染∶判斷組件的 abstract 屬性,才往父組件里面掛載 DOM
// core/instance/lifecycle
function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) { // 判斷組件的abstract屬性,才往父組件里面掛載DOM
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
  • 判斷當(dāng)前 keepAlive 和 componentInstance 是否存在來判斷是否要執(zhí)行組件 prepatch 還是執(zhí)行創(chuàng)建 componentlnstance
// core/vdom/create-component
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) { // componentInstance在初次是undefined!!!
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode) // prepatch函數(shù)執(zhí)行的是組件更新的過程
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

prepatch 操作就不會在執(zhí)行組件的 mounted 和 created 生命周期函數(shù),而是直接將 DOM 插入

(4)LRU (least recently used)緩存策略

LRU 緩存策略∶ 從內(nèi)存中找出最久未使用的數(shù)據(jù)并置換新的數(shù)據(jù)。

LRU(Least rencently used)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù),其核心思想是"如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高"。 最常見的實現(xiàn)是使用一個鏈表保存緩存數(shù)據(jù),詳細(xì)算法實現(xiàn)如下∶

  • 新數(shù)據(jù)插入到鏈表頭部
  • 每當(dāng)緩存命中(即緩存數(shù)據(jù)被訪問),則將數(shù)據(jù)移到鏈表頭部
  • 鏈表滿的時候,將鏈表尾部的數(shù)據(jù)丟棄。

18. $nextTick 原理及作用

Vue 的 nextTick 其本質(zhì)是對 JavaScript 執(zhí)行原理 EventLoop 的一種應(yīng)用。

nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法來模擬對應(yīng)的微/宏任務(wù)的實現(xiàn),本質(zhì)是為了利用 JavaScript 的這些異步回調(diào)任務(wù)隊列來實現(xiàn) Vue 框架中自己的異步回調(diào)隊列。

nextTick 不僅是 Vue 內(nèi)部的異步隊列的調(diào)用方法,同時也允許開發(fā)者在實際項目中使用這個方法來滿足實際應(yīng)用中對 DOM 更新數(shù)據(jù)時機的后續(xù)邏輯處理

nextTick 是典型的將底層 JavaScript 執(zhí)行原理應(yīng)用到具體案例中的示例,引入異步更新隊列機制的原因∶

  • 如果是同步更新,則多次對一個或多個屬性賦值,會頻繁觸發(fā) UI/DOM 的渲染,可以減少一些無用渲染
  • 同時由于 VirtualDOM 的引入,每一次狀態(tài)發(fā)生變化后,狀態(tài)變化的信號會發(fā)送給組件,組件內(nèi)部使用 VirtualDOM 進行計算得出需要更新的具體的 DOM 節(jié)點,然后對 DOM 進行更新操作,每次更新狀態(tài)后的渲染過程需要更多的計算,而這種無用功也將浪費更多的性能,所以異步渲染變得更加至關(guān)重要

Vue采用了數(shù)據(jù)驅(qū)動視圖的思想,但是在一些情況下,仍然需要操作DOM。有時候,可能遇到這樣的情況,DOM1的數(shù)據(jù)發(fā)生了變化,而DOM2需要從DOM1中獲取數(shù)據(jù),那這時就會發(fā)現(xiàn)DOM2的視圖并沒有更新,這時就需要用到了 nextTick了。

由于Vue的DOM操作是異步的,所以,在上面的情況中,就要將DOM2獲取數(shù)據(jù)的操作寫在 $nextTick中。

this.$nextTick(() => {
    // 獲取數(shù)據(jù)的操作...
})

所以,在以下情況下,會用到nextTick:

  • 在數(shù)據(jù)變化后執(zhí)行的某個操作,而這個操作需要使用隨數(shù)據(jù)變化而變化的DOM結(jié)構(gòu)的時候,這個操作就需要方法在 ?nextTick()?的回調(diào)函數(shù)中。
  • 在vue生命周期中,如果在created()鉤子進行DOM操作,也一定要放在 ?nextTick()?的回調(diào)函數(shù)中。

因為在created()鉤子函數(shù)中,頁面的DOM還未渲染,這時候也沒辦法操作DOM,所以,此時如果想要操作DOM,必須將操作的代碼放在 nextTick()的回調(diào)函數(shù)中。

19. Vue 中給 data 中的對象屬性添加一個新的屬性時會發(fā)生什么?如何解決?

<template> 
   <div>
      <ul>
         <li v-for="value in obj" :key="value"> {{value}} </li> 
      </ul> 
      <button @click="addObjB">添加 obj.b</button> 
   </div>
</template>

<script>
    export default { 
       data () { 
          return { 
              obj: { 
                  a: 'obj.a' 
              } 
          } 
       },
       methods: { 
          addObjB () { 
              this.obj.b = 'obj.b' 
              console.log(this.obj) 
          } 
      }
   }
</script>

點擊 button 會發(fā)現(xiàn),obj.b 已經(jīng)成功添加,但是視圖并未刷新。這是因為在Vue實例創(chuàng)建時,obj.b并未聲明,因此就沒有被Vue轉(zhuǎn)換為響應(yīng)式的屬性,自然就不會觸發(fā)視圖的更新,這時就需要使用Vue的全局 api $set():

addObjB () (
   this.$set(this.obj, 'b', 'obj.b')
   console.log(this.obj)
}

$set()方法相當(dāng)于手動的去把obj.b處理成一個響應(yīng)式的屬性,此時視圖也會跟著改變了。

20. Vue中封裝的數(shù)組方法有哪些,其如何實現(xiàn)頁面更新

在Vue中,對響應(yīng)式處理利用的是Object.defineProperty對數(shù)據(jù)進行攔截,而這個方法并不能監(jiān)聽到數(shù)組內(nèi)部變化,數(shù)組長度變化,數(shù)組的截取變化等,所以需要對這些操作進行hack,讓Vue能監(jiān)聽到其中的變化。


那Vue是如何實現(xiàn)讓這些數(shù)組方法實現(xiàn)元素的實時更新的呢,下面是Vue中對這些方法的封裝:

// 緩存數(shù)組原型
const arrayProto = Array.prototype;
// 實現(xiàn) arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要進行功能拓展的方法
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function(method) {
  // 緩存原生數(shù)組方法
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    // 執(zhí)行并緩存原生數(shù)組功能
    const result = original.apply(this, args);
    // 響應(yīng)式處理
    const ob = this.__ob__;
    let inserted;
    switch (method) {
    // push、unshift會新增索引,所以要手動observer
      case "push":
      case "unshift":
        inserted = args;
        break;
      // splice方法,如果傳入了第三個參數(shù),也會有索引加入,也要手動observer。
      case "splice":
        inserted = args.slice(2);
        break;
    }
    // 
    if (inserted) ob.observeArray(inserted);// 獲取插入的值,并設(shè)置響應(yīng)式監(jiān)聽
    // notify change
    ob.dep.notify();// 通知依賴更新
    // 返回原生數(shù)組方法的執(zhí)行結(jié)果
    return result;
  });
});

簡單來說就是,重寫了數(shù)組中的那些原生方法,首先獲取到這個數(shù)組的__ob__,也就是它的Observer對象,如果有新的值,就調(diào)用observeArray繼續(xù)對新的值觀察變化(也就是通過 target__proto__ == arrayMethods來改變了數(shù)組實例的型),然后手動調(diào)用notify,通知渲染watcher,執(zhí)行update。

21. Vue 單頁應(yīng)用與多頁應(yīng)用的區(qū)別

概念:

  • SPA單頁面應(yīng)用(SinglePage Web Application),指只有一個主頁面的應(yīng)用,一開始只需要加載一次js、css等相關(guān)資源。所有內(nèi)容都包含在主頁面,對每一個功能模塊組件化。單頁應(yīng)用跳轉(zhuǎn),就是切換相關(guān)組件,僅僅刷新局部資源。
  • MPA多頁面應(yīng)用 (MultiPage Application),指有多個獨立頁面的應(yīng)用,每個頁面必須重復(fù)加載js、css等相關(guān)資源。多頁應(yīng)用跳轉(zhuǎn),需要整頁資源刷新。

區(qū)別:


22. Vue template 到 render 的過程

vue的模版編譯過程主要如下:template -> ast -> render函數(shù)

vue 在模版編譯版本的碼中會執(zhí)行 compileToFunctions 將template轉(zhuǎn)化為render函數(shù):

// 將模板編譯為render函數(shù)
const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)

CompileToFunctions中的主要邏輯如下∶

(1)調(diào)用parse方法將template轉(zhuǎn)化為ast(抽象語法樹)

constast = parse(template.trim(), options)
  • parse的目標(biāo):把tamplate轉(zhuǎn)換為AST樹,它是一種用 JavaScript對象的形式來描述整個模板。
  • 解析過程:利用正則表達式順序解析模板,當(dāng)解析到開始標(biāo)簽、閉合標(biāo)簽、文本的時候都會分別執(zhí)行對應(yīng)的 回調(diào)函數(shù),來達到構(gòu)造AST樹的目的。

AST元素節(jié)點總共三種類型:type為1表示普通元素、2為表達式、3為純文本

(2)對靜態(tài)節(jié)點做優(yōu)化

optimize(ast,options)

這個過程主要分析出哪些是靜態(tài)節(jié)點,給其打一個標(biāo)記,為后續(xù)更新渲染可以直接跳過靜態(tài)節(jié)點做優(yōu)化

深度遍歷AST,查看每個子樹的節(jié)點元素是否為靜態(tài)節(jié)點或者靜態(tài)節(jié)點根。如果為靜態(tài)節(jié)點,他們生成的DOM永遠(yuǎn)不會改變,這對運行時模板更新起到了極大的優(yōu)化作用。

(3)生成代碼

const code = generate(ast, options)

generate將ast抽象語法樹編譯成 render字符串并將靜態(tài)部分放到 staticRenderFns 中,最后通過 new Function(render) 生成render函數(shù)。

23. Vue data 中某一個屬性的值發(fā)生改變后,視圖會立即同步執(zhí)行重新渲染嗎?

不會立即同步執(zhí)行重新渲染。Vue 實現(xiàn)響應(yīng)式并不是數(shù)據(jù)發(fā)生變化之后 DOM 立即變化,而是按一定的策略進行 DOM 的更新。Vue 在更新 DOM 時是異步執(zhí)行的。只要偵聽到數(shù)據(jù)變化, Vue 將開啟一個隊列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更。

如果同一個watcher被多次觸發(fā),只會被推入到隊列中一次。這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計算和 DOM 操作是非常重要的。然后,在下一個的事件循環(huán)tick中,Vue 刷新隊列并執(zhí)行實際(已去重的)工作。

24. 簡述 mixin、extends 的覆蓋邏輯

(1)mixin 和 extends

mixin 和 extends均是用于合并、拓展組件的,兩者均通過 mergeOptions 方法實現(xiàn)合并。

  • mixins 接收一個混入對象的數(shù)組,其中混入對象可以像正常的實例對象一樣包含實例選項,這些選項會被合并到最終的選項中。Mixin 鉤子按照傳入順序依次調(diào)用,并在調(diào)用組件自身的鉤子之前被調(diào)用。
  • extends 主要是為了便于擴展單文件組件,接收一個對象或構(gòu)造函數(shù)。

68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6a7065672f313530303630342f313630393531383438303237322d38636231616630312d613461382d346435342d393162622d3535343661616661633531302e6a7065673f782d

(2)mergeOptions 的執(zhí)行過程

  • 規(guī)范化選項(normalizeProps、normalizelnject、normalizeDirectives)
  • 對未合并的選項,進行判斷
if(!child._base) {
    if(child.extends) {
        parent = mergeOptions(parent, child.extends, vm)
    }
    if(child.mixins) {
        for(let i = 0, l = child.mixins.length; i < l; i++){
            parent = mergeOptions(parent, child.mixins[i], vm)
        }
    }
}
  • 合并處理。根據(jù)一個通用 Vue 實例所包含的選項進行分類逐一判斷合并,如 props、data、 methods、watch、computed、生命周期等,將合并結(jié)果存儲在新定義的 options 對象里。
  • 返回合并結(jié)果 options。

25. 描述下Vue自定義指令

在 Vue2.0 中,代碼復(fù)用和抽象的主要形式是組件。然而,有的情況下,你仍然需要對普通 DOM 元素進行底層操作,這時候就會用到自定義指令。

一般需要對DOM元素進行底層操作時使用,盡量只用來操作 DOM展示,不修改內(nèi)部的值。當(dāng)使用自定義指令直接修改 value 值時綁定v-model的值也不會同步更新;如必須修改可以在自定義指令中使用keydown事件,在vue組件中使用 change事件,回調(diào)中修改vue數(shù)據(jù);

(1)自定義指令基本內(nèi)容

  • 全局定義:?Vue.directive("focus",{})?
  • 局部定義:?directives:{focus:{}}?
  • 鉤子函數(shù):指令定義對象提供鉤子函數(shù)
bind:只調(diào)用一次,指令第一次綁定到元素時調(diào)用。在這里可以進行一次性的初始化設(shè)置。
inSerted:被綁定元素插入父節(jié)點時調(diào)用(僅保證父節(jié)點存在,但不一定已被插入文檔中)。
update:所在組件的VNode更新時調(diào)用,但是可能發(fā)生在其子VNode更新之前調(diào)用。指令的值可能發(fā)生了改變,也可能沒有。但是可以通過比較更新前后的值來忽略不必要的模板更新。
ComponentUpdate:指令所在組件的 VNode及其子VNode全部更新后調(diào)用。
unbind:只調(diào)用一次,指令與元素解綁時調(diào)用。
  • 鉤子函數(shù)參數(shù)
el:綁定元素
bing: 指令核心對象,描述指令全部信息屬性
name
value
oldValue
expression
arg
modifers
vnode  虛擬節(jié)點
oldVnode:上一個虛擬節(jié)點(更新鉤子函數(shù)中才有用)

(2)使用場景

  • 普通DOM元素進行底層操作的時候,可以使用自定義指令
  • 自定義指令是用來操作DOM的。盡管Vue推崇數(shù)據(jù)驅(qū)動視圖的理念,但并非所有情況都適合數(shù)據(jù)驅(qū)動。自定義指令就是一種有效的補充和擴展,不僅可用于定義任何的DOM操作,并且是可復(fù)用的。

(3)使用案例

初級應(yīng)用:

  • 鼠標(biāo)聚焦
  • 下拉菜單
  • 相對時間轉(zhuǎn)換
  • 滾動動畫

高級應(yīng)用:

  • 自定義指令實現(xiàn)圖片懶加載
  • 自定義指令集成第三方插件

26. 子組件可以直接改變父組件的數(shù)據(jù)嗎?

子組件不可以直接改變父組件的數(shù)據(jù)。這樣做主要是為了維護父子組件的單向數(shù)據(jù)流。每次父級組件發(fā)生更新時,子組件中所有的 prop 都將會刷新為最新的值。如果這樣做了,Vue 會在瀏覽器的控制臺中發(fā)出警告。

Vue提倡單向數(shù)據(jù)流,即父級 props 的更新會流向子組件,但是反過來則不行。這是為了防止意外的改變父組件狀態(tài),使得應(yīng)用的數(shù)據(jù)流變得難以理解,導(dǎo)致數(shù)據(jù)流混亂。如果破壞了單向數(shù)據(jù)流,當(dāng)應(yīng)用復(fù)雜時,debug 的成本會非常高。

只能通過 $emit 派發(fā)一個自定義事件,父組件接收到后,由父組件修改。

27. Vue是如何收集依賴的?

在初始化 Vue 的每個組件時,會對組件的 data 進行初始化,就會將由普通對象變成響應(yīng)式對象,在這個過程中便會進行依賴收集的相關(guān)邏輯,如下所示∶

function defieneReactive (obj, key, val){
  const dep = new Dep();
  ...
  Object.defineProperty(obj, key, {
    ...
    get: function reactiveGetter () {
      if(Dep.target){
        dep.depend();
        ...
      }
      return val
    }
    ...
  })
}

以上只保留了關(guān)鍵代碼,主要就是 const dep = new Dep()實例化一個 Dep 的實例,然后在 get 函數(shù)中通過 dep.depend() 進行依賴收集。

(1)Dep

Dep是整個依賴收集的核心,其關(guān)鍵代碼如下:

class Dep {
  static target;
  subs;

  constructor () {
    ...
    this.subs = [];
  }
  addSub (sub) {
    this.subs.push(sub)
  }
  removeSub (sub) {
    remove(this.sub, sub)
  }
  depend () {
    if(Dep.target){
      Dep.target.addDep(this)
    }
  }
  notify () {
    const subs = this.subds.slice();
    for(let i = 0;i < subs.length; i++){
      subs[i].update()
    }
  }
}

Dep 是一個 class ,其中有一個關(guān) 鍵的靜態(tài)屬性 static,它指向了一個全局唯一 Watcher,保證了同一時間全局只有一個 watcher 被計算,另一個屬性 subs 則是一個 Watcher 的數(shù)組,所以 Dep 實際上就是對 Watcher 的管理,再看看 Watcher 的相關(guān)代碼∶

(2)Watcher

class Watcher {
  getter;
  ...
  constructor (vm, expression){
    ...
    this.getter = expression;
    this.get();
  }
  get () {
    pushTarget(this);
    value = this.getter.call(vm, vm)
    ...
    return value
  }
  addDep (dep){
        ...
    dep.addSub(this)
  }
  ...
}
function pushTarget (_target) {
  Dep.target = _target
}

Watcher 是一個 class,它定義了一些方法,其中和依賴收集相關(guān)的主要有 get、addDep 等。

(3)過程

在實例化 Vue 時,依賴收集的相關(guān)過程如下∶

初 始 化 狀 態(tài) initState , 這 中 間 便 會 通 過 defineReactive 將數(shù)據(jù)變成響應(yīng)式對象,其中的 getter 部分便是用來依賴收集的。

初始化最終會走 mount 過程,其中會實例化 Watcher ,進入 Watcher 中,便會執(zhí)行 this.get() 方法,

updateComponent = () => {
  vm._update(vm._render())
}
new Watcher(vm, updateComponent)

get 方法中的 pushTarget 實際上就是把 Dep.target 賦值為當(dāng)前的 watcher。

this.getter.call(vm,vm),這里的 getter 會執(zhí)行 vm._render() 方法,在這個過程中便會觸發(fā)數(shù)據(jù)對象的 getter。那么每個對象值的 getter 都持有一個 dep,在觸發(fā) getter 的時候會調(diào)用 dep.depend() 方法,也就會執(zhí)行 Dep.target.addDep(this)。剛才 Dep.target 已經(jīng)被賦值為 watcher,于是便會執(zhí)行 addDep 方法,然后走到 dep.addSub() 方法,便將當(dāng)前的 watcher 訂閱到這個數(shù)據(jù)持有的 dep 的 subs 中,這個目的是為后續(xù)數(shù)據(jù)變化時候能通知到哪些 subs 做準(zhǔn)備。所以在 vm._render() 過程中,會觸發(fā)所有數(shù)據(jù)的 getter,這樣便已經(jīng)完成了一個依賴收集的過程。

28. 對 React 和 Vue 的理解,它們的異同

相似之處:

  • 都將注意力集中保持在核心庫,而將其他功能如路由和全局狀態(tài)管理交給相關(guān)的庫;
  • 都有自己的構(gòu)建工具,能讓你得到一個根據(jù)最佳實踐設(shè)置的項目模板;
  • 都使用了Virtual DOM(虛擬DOM)提高重繪性能;
  • 都有props的概念,允許組件間的數(shù)據(jù)傳遞;
  • 都鼓勵組件化應(yīng)用,將應(yīng)用分拆成一個個功能明確的模塊,提高復(fù)用性。

不同之處 :

1)數(shù)據(jù)流

Vue默認(rèn)支持?jǐn)?shù)據(jù)雙向綁定,而React一直提倡單向數(shù)據(jù)流

2)虛擬DOM

Vue2.x開始引入"Virtual DOM",消除了和React在這方面的差異,但是在具體的細(xì)節(jié)還是有各自的特點。

  • Vue宣稱可以更快地計算出Virtual DOM的差異,這是由于它在渲染過程中,會跟蹤每一個組件的依賴關(guān)系,不需要重新渲染整個組件樹。
  • 對于React而言,每當(dāng)應(yīng)用的狀態(tài)被改變時,全部子組件都會重新渲染。當(dāng)然,這可以通過 PureComponent/shouldComponentUpdate這個生命周期方法來進行控制,但Vue將此視為默認(rèn)的優(yōu)化。

3)組件化

React與Vue最大的不同是模板的編寫。

  • Vue鼓勵寫近似常規(guī)HTML的模板。寫起來很接近標(biāo)準(zhǔn) HTML元素,只是多了一些屬性。
  • React推薦你所有的模板通用JavaScript的語法擴展——JSX書寫。

具體來講:React中render函數(shù)是支持閉包特性的,所以import的組件在render中可以直接調(diào)用。但是在Vue中,由于模板中使用的數(shù)據(jù)都必須掛在 this 上進行一次中轉(zhuǎn),所以 import 一個組件完了之后,還需要在 components 中再聲明下。

4)監(jiān)聽數(shù)據(jù)變化的實現(xiàn)原理不同

  • Vue 通過 getter/setter 以及一些函數(shù)的劫持,能精確知道數(shù)據(jù)變化,不需要特別的優(yōu)化就能達到很好的性能
  • React 默認(rèn)是通過比較引用的方式進行的,如果不優(yōu)化(PureComponent/shouldComponentUpdate)可能導(dǎo)致大量不必要的vDOM的重新渲染。這是因為 Vue 使用的是可變數(shù)據(jù),而React更強調(diào)數(shù)據(jù)的不可變。

5)高階組件

react可以通過高階組件(HOC)來擴展,而Vue需要通過mixins來擴展。

高階組件就是高階函數(shù),而React的組件本身就是純粹的函數(shù),所以高階函數(shù)對React來說易如反掌。相反Vue.js使用HTML模板創(chuàng)建視圖組件,這時模板無法有效的編譯,因此Vue不能采用HOC來實現(xiàn)。

6)構(gòu)建工具

兩者都有自己的構(gòu)建工具:

  • React ==> Create React APP
  • Vue ==> vue-cli

7)跨平臺

  • React ==> React Native
  • Vue ==> Weex

29. Vue的優(yōu)點

  • 輕量級框架:只關(guān)注視圖層,是一個構(gòu)建數(shù)據(jù)的視圖集合,大小只有幾十 ?kb ?;
  • 簡單易學(xué):國人開發(fā),中文文檔,不存在語言障礙 ,易于理解和學(xué)習(xí);
  • 雙向數(shù)據(jù)綁定:保留了 ?angular ?的特點,在數(shù)據(jù)操作方面更為簡單;
  • 組件化:保留了 ?react ?的優(yōu)點,實現(xiàn)了 ?html ?的封裝和重用,在構(gòu)建單頁面應(yīng)用方面有著獨特的優(yōu)勢;
  • 視圖,數(shù)據(jù),結(jié)構(gòu)分離:使數(shù)據(jù)的更改更為簡單,不需要進行邏輯代碼的修改,只需要操作數(shù)據(jù)就能完成相關(guān)操作;
  • 虛擬DOM:?dom? 操作是非常耗費性能的,不再使用原生的 ?dom ?操作節(jié)點,極大解放 ?dom ?操作,但具體操作的還是 ?dom ?不過是換了另一種方式;
  • 運行速度更快:相比較于 ?react ?而言,同樣是操作虛擬 ?dom?,就性能而言, ?vue ?存在很大的優(yōu)勢。

30. assets和static的區(qū)別

相同點: assets 和 static 兩個都是存放靜態(tài)資源文件。項目中所需要的資源文件圖片,字體圖標(biāo),樣式文件等都可以放在這兩個文件下,這是相同點

不相同點:assets 中存放的靜態(tài)資源文件在項目打包時,也就是運行 npm run build 時會將 assets 中放置的靜態(tài)資源文件進行打包上傳,所謂打包簡單點可以理解為壓縮體積,代碼格式化。而壓縮后的靜態(tài)資源文件最終也都會放置在 static 文件中跟著 index.html 一同上傳至服務(wù)器。static 中放置的靜態(tài)資源文件就不會要走打包壓縮格式化等流程,而是直接進入打包好的目錄,直接上傳至服務(wù)器。因為避免了壓縮直接進行上傳,在打包時會提高一定的效率,但是 static 中的資源文件由于沒有進行壓縮等操作,所以文件的體積也就相對于 assets 中打包后的文件提交較大點。在服務(wù)器中就會占據(jù)更大的空間。

建議: 將項目中 template需要的樣式文件js文件等都可以放置在 assets 中,走打包這一流程。減少體積。而項目中引入的第三方的資源文件如 iconfoont.css 等文件可以放置在 static 中,因為這些引入的第三方文件已經(jīng)經(jīng)過處理,不再需要處理,直接上傳。

31. delete和Vue.delete刪除數(shù)組的區(qū)別

  • ?delete ?只是被刪除的元素變成了 ?empty/undefined? 其他的元素的鍵值還是不變。
  • ?Vue.delete? 直接刪除了數(shù)組 改變了數(shù)組的鍵值。

32. vue如何監(jiān)聽對象或者數(shù)組某個屬性的變化

當(dāng)在項目中直接設(shè)置數(shù)組的某一項的值,或者直接設(shè)置對象的某個屬性值,這個時候,你會發(fā)現(xiàn)頁面并沒有更新。這是因為Object.defineProperty()限制,監(jiān)聽不到變化。

解決方式:

  • this.$set(你要改變的數(shù)組/對象,你要改變的位置/key,你要改成什么value)
this.$set(this.arr, 0, "OBKoro1"); // 改變數(shù)組
this.$set(this.obj, "c", "OBKoro1"); // 改變對象
  • 調(diào)用以下幾個數(shù)組的方法
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()

vue源碼里緩存了array的原型鏈,然后重寫了這幾個方法,觸發(fā)這幾個方法的時候會observer數(shù)據(jù),意思是使用這些方法不用再進行額外的操作,視圖自動進行更新。 推薦使用splice方法會比較好自定義,因為splice可以在數(shù)組的任何位置進行刪除/添加操作

vm.$set 的實現(xiàn)原理是:

  • 如果目標(biāo)是數(shù)組,直接使用數(shù)組的 splice 方法觸發(fā)相應(yīng)式;
  • 如果目標(biāo)是對象,會先判讀屬性是否存在、對象是否是響應(yīng)式,最終如果要對屬性進行響應(yīng)式處理,則是通過調(diào)用 defineReactive 方法進行響應(yīng)式處理( defineReactive 方法就是 Vue 在初始化對象時,給對象屬性采用 Object.defineProperty 動態(tài)添加 getter 和 setter 的功能所調(diào)用的方法)

33. 什么是 mixin ?

  • Mixin 使我們能夠為 Vue 組件編寫可插拔和可重用的功能。
  • 如果希望在多個組件之間重用一組組件選項,例如生命周期 hook、 方法等,則可以將其編寫為 mixin,并在組件中簡單的引用它。
  • 然后將 mixin 的內(nèi)容合并到組件中。如果你要在 mixin 中定義生命周期 hook,那么它在執(zhí)行時將優(yōu)化于組件自已的 hook。

34. Vue模版編譯原理

vue中的模板template無法被瀏覽器解析并渲染,因為這不屬于瀏覽器的標(biāo)準(zhǔn),不是正確的HTML語法,所有需要將template轉(zhuǎn)化成一個JavaScript函數(shù),這樣瀏覽器就可以執(zhí)行這一個函數(shù)并渲染出對應(yīng)的HTML元素,就可以讓視圖跑起來了,這一個轉(zhuǎn)化的過程,就成為模板編譯。模板編譯又分三個階段,解析parse,優(yōu)化optimize,生成generate,最終生成可執(zhí)行函數(shù)render。

  • 解析階段:使用大量的正則表達式對template字符串進行解析,將標(biāo)簽、指令、屬性等轉(zhuǎn)化為抽象語法樹AST。
  • 優(yōu)化階段:遍歷AST,找到其中的一些靜態(tài)節(jié)點并進行標(biāo)記,方便在頁面重渲染的時候進行diff比較時,直接跳過這一些靜態(tài)節(jié)點,優(yōu)化runtime的性能。
  • 生成階段:將最終的AST轉(zhuǎn)化為render函數(shù)字符串。

35. 對SSR的理解

SSR也就是服務(wù)端渲染,也就是將Vue在客戶端把標(biāo)簽渲染成HTML的工作放在服務(wù)端完成,然后再把html直接返回給客戶端

SSR的優(yōu)勢:

  • 更好的SEO
  • 首屏加載速度更快

SSR的缺點:

  • 開發(fā)條件會受到限制,服務(wù)器端渲染只支持beforeCreate和created兩個鉤子;
  • 當(dāng)需要一些外部擴展庫時需要特殊處理,服務(wù)端渲染應(yīng)用程序也需要處于Node.js的運行環(huán)境;
  • 更多的服務(wù)端負(fù)載。

36. Vue的性能優(yōu)化有哪些

(1)編碼階段

  • 盡量減少data中的數(shù)據(jù),data中的數(shù)據(jù)都會增加getter和setter,會收集對應(yīng)的watcher
  • v-if和v-for不能連用
  • 如果需要使用v-for給每項元素綁定事件時使用事件代理
  • SPA 頁面采用keep-alive緩存組件
  • 在更多的情況下,使用v-if替代v-show
  • key保證唯一
  • 使用路由懶加載、異步組件
  • 防抖、節(jié)流
  • 第三方模塊按需導(dǎo)入
  • 長列表滾動到可視區(qū)域動態(tài)加載
  • 圖片懶加載

(2)SEO優(yōu)化

  • 預(yù)渲染
  • 服務(wù)端渲染SSR

(3)打包優(yōu)化

  • 壓縮代碼
  • Tree Shaking/Scope Hoisting
  • 使用cdn加載第三方模塊
  • 多線程打包happypack
  • splitChunks抽離公共文件
  • sourceMap優(yōu)化

(4)用戶體驗

  • 骨架屏
  • PWA
  • 還可以使用緩存(客戶端緩存、服務(wù)端緩存)優(yōu)化、服務(wù)端開啟gzip壓縮等。

37. 對 SPA 單頁面的理解,它的優(yōu)缺點分別是什么?

SPA( single-page application )僅在 Web 頁面初始化時加載相應(yīng)的 HTML、JavaScript 和 CSS。一旦頁面加載完成,SPA 不會因為用戶的操作而進行頁面的重新加載或跳轉(zhuǎn);取而代之的是利用路由機制實現(xiàn) HTML 內(nèi)容的變換,UI 與用戶的交互,避免頁面的重新加載。

優(yōu)點:

  • 用戶體驗好、快,內(nèi)容的改變不需要重新加載整個頁面,避免了不必要的跳轉(zhuǎn)和重復(fù)渲染;
  • 基于上面一點,SPA 相對對服務(wù)器壓力?。?/li>
  • 前后端職責(zé)分離,架構(gòu)清晰,前端進行交互邏輯,后端負(fù)責(zé)數(shù)據(jù)處理;

缺點:

  • 初次加載耗時多:為實現(xiàn)單頁 Web 應(yīng)用功能及顯示效果,需要在加載頁面的時候?qū)?JavaScript、CSS 統(tǒng)一加載,部分頁面按需加載;
  • 前進后退路由管理:由于單頁應(yīng)用在一個頁面中顯示所有的內(nèi)容,所以不能使用瀏覽器的前進后退功能,所有的頁面切換需要自己建立堆棧管理;
  • SEO 難度較大:由于所有的內(nèi)容都在一個頁面中動態(tài)替換顯示,所以在 SEO 上其有著天然的弱勢。

38. template和jsx的有什么分別?

對于 runtime 來說,只需要保證組件存在 render 函數(shù)即可,而有了預(yù)編譯之后,只需要保證構(gòu)建過程中生成 render 函數(shù)就可以。在 webpack 中,使用 vue-loader編譯.vue文件,內(nèi)部依賴的 vue-template-compiler模塊,在 webpack 構(gòu)建過程中,將template預(yù)編譯成 render 函數(shù)。與 react 類似,在添加了jsx的語法糖解析器 babel-plugin-transform-vue-jsx之后,就可以直接手寫render函數(shù)。

所以,template和jsx的都是render的一種表現(xiàn)形式,不同的是:JSX相對于template而言,具有更高的靈活性,在復(fù)雜的組件中,更具有優(yōu)勢,而 template 雖然顯得有些呆滯。但是 template 在代碼結(jié)構(gòu)上更符合視圖與邏輯分離的習(xí)慣,更簡單、更直觀、更好維護。

39. vue初始化頁面閃動問題

使用vue開發(fā)時,在vue初始化之前,由于div是不歸vue管的,所以我們寫的代碼在還沒有解析的情況下會容易出現(xiàn)花屏現(xiàn)象,看到類似于{{message}}的字樣,雖然一般情況下這個時間很短暫,但是還是有必要讓解決這個問題的。

首先:在css里加上以下代碼:

[v-cloak] {
    display: none;
}

如果沒有徹底解決問題,則在根元素加上 style="display: none;" :style="{display: 'block'}"

40. extend 有什么作用

這個 API 很少用到,作用是擴展組件生成一個構(gòu)造器,通常會與 $mount 一起使用。

// 創(chuàng)建組件構(gòu)造器
let Component = Vue.extend({
  template: '<div>test</div>'
})
// 掛載到 #app 上
new Component().$mount('#app')
// 除了上面的方式,還可以用來擴展已有的組件
let SuperComponent = Vue.extend(Component)
new SuperComponent({
    created() {
        console.log(1)
    }
})
new SuperComponent().$mount('#app')

41. mixin 和 mixins 區(qū)別

mixin 用于全局混入,會影響到每個組件實例,通常插件都是這樣做初始化的。

Vue.mixin({
    beforeCreate() {
        // ...邏輯
        // 這種方式會影響到每個組件的 beforeCreate 鉤子函數(shù)
    }
})

雖然文檔不建議在應(yīng)用中直接使用 mixin,但是如果不濫用的話也是很有幫助的,比如可以全局混入封裝好的 ajax 或者一些工具函數(shù)等等。

mixins 應(yīng)該是最常使用的擴展組件的方式了。如果多個組件中有相同的業(yè)務(wù)邏輯,就可以將這些邏輯剝離出來,通過 mixins 混入代碼,比如上拉下拉加載數(shù)據(jù)這種邏輯等等。

另外需要注意的是 mixins 混入的鉤子函數(shù)會先于組件內(nèi)的鉤子函數(shù)執(zhí)行,并且在遇到同名選項的時候也會有選擇性的進行合并。

42. MVVM的優(yōu)缺點?

優(yōu)點:

  • 分離視圖(View)和模型(Model),降低代碼耦合,提?視圖或者邏輯的重?性: ?如視圖(View)可以獨?于Model變化和修改,?個ViewModel可以綁定不同的"View"上,當(dāng)View變化的時候Model不可以不變,當(dāng)Model變化的時候View也可以不變。你可以把?些視圖邏輯放在?個ViewModel??,讓很多view重?這段視圖邏輯
  • 提?可測試性: ViewModel的存在可以幫助開發(fā)者更好地編寫測試代碼
  • ?動更新dom: 利?雙向綁定,數(shù)據(jù)更新后視圖?動更新,讓開發(fā)者從繁瑣的?動dom中解放

缺點:

  • Bug很難被調(diào)試: 因為使?雙向綁定的模式,當(dāng)你看到界?異常了,有可能是你View的代碼有Bug,也可能是Model的代碼有問題。數(shù)據(jù)綁定使得?個位置的Bug被快速傳遞到別的位置,要定位原始出問題的地?就變得不那么容易了。另外,數(shù)據(jù)綁定的聲明是指令式地寫在View的模版當(dāng)中的,這些內(nèi)容是沒辦法去打斷點debug的
  • ?個?的模塊中model也會很?,雖然使??便了也很容易保證了數(shù)據(jù)的?致性,當(dāng)時?期持有,不釋放內(nèi)存就造成了花費更多的內(nèi)存
  • 對于?型的圖形應(yīng)?程序,視圖狀態(tài)較多,ViewModel的構(gòu)建和維護的成本都會?較?。

二、生命周期


1. 說一下Vue的生命周期

Vue 實例有?個完整的?命周期,也就是從開始創(chuàng)建、初始化數(shù)據(jù)、編譯模版、掛載Dom -> 渲染、更新 -> 渲染、卸載 等?系列過程,稱這是Vue的?命周期。

  1. beforeCreate(創(chuàng)建前):數(shù)據(jù)觀測和初始化事件還未開始,此時 data 的響應(yīng)式追蹤、event/watcher 都還沒有被設(shè)置,也就是說不能訪問到data、computed、watch、methods上的方法和數(shù)據(jù)。
  2. created(創(chuàng)建后):實例創(chuàng)建完成,實例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此時渲染得節(jié)點還未掛載到 DOM,所以不能訪問到 ?$el? 屬性。
  3. beforeMount(掛載前):在掛載開始之前被調(diào)用,相關(guān)的render函數(shù)首次被調(diào)用。實例已完成以下的配置:編譯模板,把data里面的數(shù)據(jù)和模板生成html。此時還沒有掛載html到頁面上。
  4. mounted(掛載后):在el被新創(chuàng)建的 vm.$el 替換,并掛載到實例上去之后調(diào)用。實例已完成以下的配置:用上面編譯好的html內(nèi)容替換el屬性指向的DOM對象。完成模板中的html渲染到html 頁面中。此過程中進行ajax交互。
  5. beforeUpdate(更新前):響應(yīng)式數(shù)據(jù)更新時調(diào)用,此時雖然響應(yīng)式數(shù)據(jù)更新了,但是對應(yīng)的真實 DOM 還沒有被渲染。
  6. updated(更新后) :在由于數(shù)據(jù)更改導(dǎo)致的虛擬DOM重新渲染和打補丁之后調(diào)用。此時 DOM 已經(jīng)根據(jù)響應(yīng)式數(shù)據(jù)的變化更新了。調(diào)用時,組件 DOM已經(jīng)更新,所以可以執(zhí)行依賴于DOM的操作。然而在大多數(shù)情況下,應(yīng)該避免在此期間更改狀態(tài),因為這可能會導(dǎo)致更新無限循環(huán)。該鉤子在服務(wù)器端渲染期間不被調(diào)用。
  7. beforeDestroy(銷毀前):實例銷毀之前調(diào)用。這一步,實例仍然完全可用,?this ?仍能獲取到實例。
  8. destroyed(銷毀后):實例銷毀后調(diào)用,調(diào)用后,Vue 實例指示的所有東西都會解綁定,所有的事件監(jiān)聽器會被移除,所有的子實例也會被銷毀。該鉤子在服務(wù)端渲染期間不被調(diào)用。

另外還有 keep-alive 獨有的生命周期,分別為 activated 和 deactivated 。用 keep-alive 包裹的組件在切換時不會進行銷毀,而是緩存到內(nèi)存中并執(zhí)行 deactivated 鉤子函數(shù),命中緩存渲染后會執(zhí)行 activated 鉤子函數(shù)。

2. Vue 子組件和父組件執(zhí)行順序

加載渲染過程:

1.父組件 beforeCreate

2.父組件 created

3.父組件 beforeMount

4.子組件 beforeCreate

5.子組件 created

6.子組件 beforeMount

7.子組件 mounted

8.父組件 mounted

更新過程:

1. 父組件 beforeUpdate

2.子組件 beforeUpdate

3.子組件 updated

4.父組件 updated

銷毀過程:

1. 父組件 beforeDestroy

2.子組件 beforeDestroy

3.子組件 destroyed

4.父組件 destoryed

3. created和mounted的區(qū)別

  • created:在模板渲染成html前調(diào)用,即通常初始化某些屬性值,然后再渲染成視圖。
  • mounted:在模板渲染成html后調(diào)用,通常是初始化頁面完成后,再對html的dom節(jié)點進行一些需要的操作。

4. 一般在哪個生命周期請求異步數(shù)據(jù)

我們可以在鉤子函數(shù) created、beforeMount、mounted 中進行調(diào)用,因為在這三個鉤子函數(shù)中,data 已經(jīng)創(chuàng)建,可以將服務(wù)端端返回的數(shù)據(jù)進行賦值。

推薦在 created 鉤子函數(shù)中調(diào)用異步請求,因為在 created 鉤子函數(shù)中調(diào)用異步請求有以下優(yōu)點:

  • 能更快獲取到服務(wù)端數(shù)據(jù),減少頁面加載時間,用戶體驗更好;
  • SSR不支持 beforeMount 、mounted 鉤子函數(shù),放在 created 中有助于一致性。

5. keep-alive 中的生命周期哪些

keep-alive是 Vue 提供的一個內(nèi)置組件,用來對組件進行緩存——在組件切換過程中將狀態(tài)保留在內(nèi)存中,防止重復(fù)渲染DOM。

如果為一個組件包裹了 keep-alive,那么它會多出兩個生命周期:deactivated、activated。同時,beforeDestroy 和 destroyed 就不會再被觸發(fā)了,因為組件不會被真正銷毀。

當(dāng)組件被換掉時,會被緩存到內(nèi)存中、觸發(fā) deactivated 生命周期;當(dāng)組件被切回來時,再去緩存里找這個組件、觸發(fā) activated鉤子函數(shù)。

三、組件通信


組件通信的方式如下:

(1) props / $emit

父組件通過 props向子組件傳遞數(shù)據(jù),子組件通過 $emit和父組件通信

1. 父組件向子組件傳值
  • ?props?只能是父組件向子組件進行傳值,?props?使得父子組件之間形成了一個單向下行綁定。子組件的數(shù)據(jù)會隨著父組件不斷更新。
  • ?props ?可以顯示定義一個或一個以上的數(shù)據(jù),對于接收的數(shù)據(jù),可以是各種數(shù)據(jù)類型,同樣也可以傳遞一個函數(shù)。
  • ?props?屬性名規(guī)則:若在 ?props?中使用駝峰形式,模板中需要使用短橫線的形式
// 父組件
<template>
    <div id="father">
        <son :msg="msgData" :fn="myFunction"></son>
    </div>
</template>

<script>
import son from "./son.vue";
export default {
    name: father,
    data() {
        msgData: "父組件數(shù)據(jù)";
    },
    methods: {
        myFunction() {
            console.log("vue");
        }
    },
    components: {
        son
    }
};
</script>
// 子組件
<template>
    <div id="son">
        <p>{{msg}}</p>
        <button @click="fn">按鈕</button>
    </div>
</template>
<script>
export default {
    name: "son",
    props: ["msg", "fn"]
};
</script>
2. 子組件向父組件傳值
  • ?$emit?綁定一個自定義事件,當(dāng)這個事件被執(zhí)行的時就會將參數(shù)傳遞給父組件,而父組件通過 v-on監(jiān)聽并接收參數(shù)。
// 父組件
<template>
  <div class="section">
    <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {
  name: 'comArticle',
  components: { comArticle },
  data() {
    return {
      currentIndex: -1,
      articleList: ['紅樓夢', '西游記', '三國演義']
    }
  },
  methods: {
    onEmitIndex(idx) {
      this.currentIndex = idx
    }
  }
}
</script>
//子組件
<template>
  <div>
    <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
  </div>
</template>

<script>
export default {
  props: ['articles'],
  methods: {
    emitIndex(index) {
      this.$emit('onEmitIndex', index) // 觸發(fā)父組件的方法,并傳遞參數(shù)index
    }
  }
}
</script>

(2)eventBus事件總線($emit / $on)

eventBus事件總線適用于父子組件、非父子組件等之間的通信,使用步驟如下:

(1)創(chuàng)建事件中心管理組件之間的通信

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

(2)發(fā)送事件

假設(shè)有兩個兄弟組件 firstCom和 secondCom

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
  components: { firstCom, secondCom }
}
</script>

在 firstCom組件中發(fā)送事件:

<template>
  <div>
    <button @click="add">加法</button>  
  </div>
</template>

<script>
import {EventBus} from './event-bus.js' // 引入事件中心

export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>

(3)接收事件

在 secondCom組件中發(fā)送事件:

<template>
  <div>求和: {{count}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>

在上述代碼中,這就相當(dāng)于將 num值存貯在了事件總線中,在其他組件中可以直接訪問。事件總線就相當(dāng)于一個橋梁,不用組件通過它來通信。

雖然看起來比較簡單,但是這種方法也有不變之處,如果項目過大,使用這種方式進行通信,后期維護起來會很困難。

(3)依賴注入(project / inject)

這種方式就是Vue中的依賴注入,該方法用于父子組件之間的通信。當(dāng)然這里所說的父子不一定是真正的父子,也可以是祖孫組件,在層數(shù)很深的情況下,可以使用這種方法來進行傳值。就不用一層一層的傳遞了。

project / inject是Vue提供的兩個鉤子,和 datamethods是同級的。并且 project的書寫形式和 data一樣。

  • ?project ?鉤子用來發(fā)送數(shù)據(jù)或方法
  • ?inject?鉤子用來接收數(shù)據(jù)或方法

在父組件中:

provide() {
 return {
    num: this.num
  };
}

在子組件中:

inject: ['num']

還可以這樣寫,這樣寫就可以訪問父組件中的所有屬性:

provide() {
 return {
    app: this
  };
}
data() {
 return {
    num: 1
  };
}

inject: ['app']
console.log(this.app.num)

注意: 依賴注入所提供的屬性是非響應(yīng)式的。

(3)ref / $refs

這種方式也是實現(xiàn)父子組件之間的通信。

ref: 這個屬性用在子組件上,它的引用就指向了子組件的實例??梢酝ㄟ^實例來訪問組件的數(shù)據(jù)和方法。

在子組件中:

export default {
  data () {
    return {
      name: 'JavaScript'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}

在父組件中:

<template>
  <child ref="child"></component-a>
</template>
<script>
  import child from './child.vue'
  export default {
    components: { child },
    mounted () {
      console.log(this.$refs.child.name);  // JavaScript
      this.$refs.child.sayHello();  // hello
    }
  }
</script>

(4)$parent / $children

  • 使用 ?$parent?可以讓組件訪問父組件的實例(訪問的是上一級父組件的屬性和方法)
  • 使用 ?$children?可以讓組件訪問子組件的實例,但是,?$children?并不能保證順序,并且訪問的數(shù)據(jù)也不是響應(yīng)式的。

在子組件中:

<template>
  <div>
    <span>{{message}}</span>
    <p>獲取父組件的值為:  {{parentVal}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Vue'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}
</script>

在父組件中:

// 父組件中
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <child></child>
    <button @click="change">點擊改變子組件值</button>
  </div>
</template>

<script>
import child from './child.vue'
export default {
  components: { child },
  data() {
    return {
      msg: 'Welcome'
    }
  },
  methods: {
    change() {
      // 獲取到子組件
      this.$children[0].message = 'JavaScript'
    }
  }
}
</script>

在上面的代碼中,子組件獲取到了父組件的 parentVal值,父組件改變了子組件中 message的值。

需要注意:

  • 通過 ?$parent?訪問到的是上一級父組件的實例,可以使用 ?$root?來訪問根組件的實例
  • 在組件中使用 ?$children?拿到的是所有的子組件的實例,它是一個數(shù)組,并且是無序的
  • 在根組件 ?#app?上拿 ?$parent?得到的是 ?new Vue()?的實例,在這實例上再拿 ?$parent?得到的是 ?undefined?,而在最底層的子組件拿 ?$children?是個空數(shù)組
  • ?$children? 的值是數(shù)組,而 ?$parent?是個對象

(5)$attrs / $listeners

考慮一種場景,如果A是B組件的父組件,B是C組件的父組件。如果想要組件A給組件C傳遞數(shù)據(jù),這種隔代的數(shù)據(jù),該使用哪種方式呢?

如果是用 props/$emit來一級一級的傳遞,確實可以完成,但是比較復(fù)雜;如果使用事件總線,在多人開發(fā)或者項目較大的時候,維護起來很麻煩;如果使用Vuex,的確也可以,但是如果僅僅是傳遞數(shù)據(jù),那可能就有點浪費了。

針對上述情況,Vue引入了 $attrs / $listeners,實現(xiàn)組件之間的跨代通信。

先來看一下 inheritAttrs,它的默認(rèn)值true,繼承所有的父組件屬性除 props之外的所有屬性;inheritAttrs:false 只繼承class屬性 。

  • ?$attrs?:繼承所有的父組件屬性(除了prop傳遞的屬性、class 和 style ),一般用在子組件的子元素上
  • ?$listeners?:該屬性是一個對象,里面包含了作用在這個組件上的所有監(jiān)聽器,可以配合 ?v-on="$listeners"? 將所有的事件監(jiān)聽器指向這個組件的某個特定的子元素。(相當(dāng)于子組件繼承父組件的事件)

A組件(APP.vue):

<template>
    <div id="app">
        //此處監(jiān)聽了兩個事件,可以在B組件或者C組件中直接觸發(fā) 
        <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
    </div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
    components: { Child1 },
    methods: {
        onTest1() {
            console.log('test1 running');
        },
        onTest2() {
            console.log('test2 running');
        }
    }
};
</script>

B組件(Child1.vue):

<template>
    <div class="child-1">
        <p>props: {{pChild1}}</p>
        <p>$attrs: {{$attrs}}</p>
        <child2 v-bind="$attrs" v-on="$listeners"></child2>
    </div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
    props: ['pChild1'],
    components: { Child2 },
    inheritAttrs: false,
    mounted() {
        this.$emit('test1'); // 觸發(fā)APP.vue中的test1方法
    }
};
</script>

C 組件 (Child2.vue):

<template>
    <div class="child-2">
        <p>props: {{pChild2}}</p>
        <p>$attrs: {{$attrs}}</p>
    </div>
</template>
<script>
export default {
    props: ['pChild2'],
    inheritAttrs: false,
    mounted() {
        this.$emit('test2');// 觸發(fā)APP.vue中的test2方法
    }
};
</script>

在上述代碼中:

  • C組件中能直接觸發(fā)test的原因在于 B組件調(diào)用C組件時 使用 v-on 綁定了 ?$listeners? 屬性
  • 在B組件中通過v-bind 綁定 ?$attrs?屬性,C組件可以直接獲取到A組件中傳遞下來的props(除了B組件中props聲明的)

(6)總結(jié)

(1)父子組件間通信

  • 子組件通過 props 屬性來接受父組件的數(shù)據(jù),然后父組件在子組件上注冊監(jiān)聽事件,子組件通過 emit 觸發(fā)事件來向父組件發(fā)送數(shù)據(jù)。
  • 通過 ref 屬性給子組件設(shè)置一個名字。父組件通過 $refs 組件名來獲得子組件,子組件通過 $parent 獲得父組件,這樣也可以實現(xiàn)通信。
  • 使用 provide/inject,在父組件中通過 provide提供變量,在子組件中通過 inject 來將變量注入到組件中。不論子組件有多深,只要調(diào)用了 inject 那么就可以注入 provide中的數(shù)據(jù)。

(2)兄弟組件間通信

  • 使用 eventBus 的方法,它的本質(zhì)是通過創(chuàng)建一個空的 Vue 實例來作為消息傳遞的對象,通信的組件引入這個實例,通信的組件通過在這個實例上監(jiān)聽和觸發(fā)事件,來實現(xiàn)消息的傳遞。
  • 通過 $parent/$refs 來獲取到兄弟組件,也可以進行通信。

(3)任意組件之間

  • 使用 eventBus ,其實就是創(chuàng)建一個事件中心,相當(dāng)于中轉(zhuǎn)站,可以用它來傳遞事件和接收事件。

如果業(yè)務(wù)邏輯復(fù)雜,很多組件之間需要同時處理一些公共的數(shù)據(jù),這個時候采用上面這一些方法可能不利于項目的維護。這個時候可以使用 vuex ,vuex 的思想就是將這一些公共的數(shù)據(jù)抽離出來,將它作為一個全局的變量來管理,然后其他組件就可以對這個公共數(shù)據(jù)進行讀寫操作,這樣達到了解耦的目的。

四、路由


1. Vue-Router 的懶加載如何實現(xiàn)

非懶加載:

import List from '@/components/list.vue'
const router = new VueRouter({
  routes: [
    { path: '/list', component: List }
  ]
})

(1)方案一(常用):使用箭頭函數(shù)+import動態(tài)加載

const List = () => import('@/components/list.vue')
const router = new VueRouter({
  routes: [
    { path: '/list', component: List }
  ]
})

(2)方案二:使用箭頭函數(shù)+require動態(tài)加載

const router = new Router({
  routes: [
   {
     path: '/list',
     component: resolve => require(['@/components/list'], resolve)
   }
  ]
})

(3)方案三:使用webpack的require.ensure技術(shù),也可以實現(xiàn)按需加載。 這種情況下,多個路由指定相同的chunkName,會合并打包成一個js文件。

// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的寫法  這種是官方推薦的寫的 按模塊劃分懶加載 
const router = new Router({
  routes: [
  {
    path: '/list',
    component: List,
    name: 'list'
  }
 ]
}))

2. 路由的hash和history模式的區(qū)別

Vue-Router有兩種模式:hash模式history模式。默認(rèn)的路由模式是hash模式。

1. hash模式

簡介: hash模式是開發(fā)中默認(rèn)的模式,它的URL帶著一個#,例如:http://www.abc.com/#/vue,它的hash值就是 #/vue。

特點:hash值會出現(xiàn)在URL里面,但是不會出現(xiàn)在HTTP請求中,對后端完全沒有影響。所以改變hash值,不會重新加載頁面。這種模式的瀏覽器支持度很好,低版本的IE瀏覽器也支持這種模式。hash路由被稱為是前端路由,已經(jīng)成為SPA(單頁面應(yīng)用)的標(biāo)配。

原理: hash模式的主要原理就是onhashchange()事件

window.onhashchange = function(event){
    console.log(event.oldURL, event.newURL);
    let hash = location.hash.slice(1);
}

使用onhashchange()事件的好處就是,在頁面的hash值發(fā)生變化時,無需向后端發(fā)起請求,window就可以監(jiān)聽事件的改變,并按規(guī)則加載相應(yīng)的代碼。除此之外,hash值變化對應(yīng)的URL都會被瀏覽器記錄下來,這樣瀏覽器就能實現(xiàn)頁面的前進和后退。雖然是沒有請求后端服務(wù)器,但是頁面的hash值和對應(yīng)的URL關(guān)聯(lián)起來了。

2. history模式

簡介: history模式的URL中沒有#,它使用的是傳統(tǒng)的路由分發(fā)模式,即用戶在輸入一個URL時,服務(wù)器會接收這個請求,并解析這個URL,然后做出相應(yīng)的邏輯處理。

特點: 當(dāng)使用history模式時,URL就像這樣:http://abc.com/user/id。相比hash模式更加好看。但是,history模式需要后臺配置支持。如果后臺沒有正確配置,訪問時會返回404。

API: history api可以分為兩大部分,切換歷史狀態(tài)和修改歷史狀態(tài):

  • 修改歷史狀態(tài):包括了 HTML5 History Interface 中新增的 ?pushState()? 和 ?replaceState()? 方法,這兩個方法應(yīng)用于瀏覽器的歷史記錄棧,提供了對歷史記錄進行修改的功能。只是當(dāng)他們進行修改時,雖然修改了url,但瀏覽器不會立即向后端發(fā)送請求。如果要做到改變url但又不刷新頁面的效果,就需要前端用上這兩個API。
  • 切換歷史狀態(tài): 包括 ?forward()?、?back()?、?go()?三個方法,對應(yīng)瀏覽器的前進,后退,跳轉(zhuǎn)操作。

雖然history模式丟棄了丑陋的#。但是,它也有自己的缺點,就是在刷新頁面的時候,如果沒有相應(yīng)的路由或資源,就會刷出404來。

如果想要切換到history模式,就要進行以下配置(后端也要進行配置):

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

3. 兩種模式對比

調(diào)用 history.pushState() 相比于直接修改 hash,存在以下優(yōu)勢:

  • pushState() 設(shè)置的新 URL 可以是與當(dāng)前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能設(shè)置與當(dāng)前 URL 同文檔的 URL;
  • pushState() 設(shè)置的新 URL 可以與當(dāng)前 URL 一模一樣,這樣也會把記錄添加到棧中;而 hash 設(shè)置的新值必須與原來不一樣才會觸發(fā)動作將記錄添加到棧中;
  • pushState() 通過 stateObject 參數(shù)可以添加任意類型的數(shù)據(jù)到記錄中;而 hash 只可添加短字符串;
  • pushState() 可額外設(shè)置 title 屬性供后續(xù)使用。
  • hash模式下,僅hash符號之前的url會被包含在請求中,后端如果沒有做到對路由的全覆蓋,也不會返回404錯誤;history模式下,前端的url必須和實際向后端發(fā)起請求的url一致,如果沒有對用的路由處理,將返回404錯誤。

hash模式和history模式都有各自的優(yōu)勢和缺陷,還是要根據(jù)實際情況選擇性的使用。

3. 如何獲取頁面的hash變化

(1)監(jiān)聽$route的變化

// 監(jiān)聽,當(dāng)路由發(fā)生變化的時候執(zhí)行
watch: {
  $route: {
    handler: function(val, oldVal){
      console.log(val);
    },
    // 深度觀察監(jiān)聽
    deep: true
  }
},

(2)window.location.hash讀取#值

window.location.hash 的值可讀可寫,讀取來判斷狀態(tài)是否改變,寫入時可以在不重載網(wǎng)頁的前提下,添加一條歷史訪問記錄。

4. $route 和$router 的區(qū)別

  • $route 是“路由信息對象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息參數(shù)
  • $router 是“路由實例”對象包括了路由的跳轉(zhuǎn)方法,鉤子函數(shù)等。

5. 如何定義動態(tài)路由?如何獲取傳過來的動態(tài)參數(shù)?

(1)param方式

  • 配置路由格式:?/router/:id?
  • 傳遞的方式:在path后面跟上對應(yīng)的值
  • 傳遞后形成的路徑:?/router/123?

1)路由定義

//在APP.vue中
<router-link :to="'/user/'+userId" replace>用戶</router-link>  

//在index.js
{
   path: '/user/:userid',
   component: User,
},

2)路由跳轉(zhuǎn)

// 方法1:
<router-link :to="{ name: 'users', params: { uname: wade }}">按鈕</router-link

// 方法2:
this.$router.push({name:'users',params:{uname:wade}})

// 方法3:
this.$router.push('/user/' + wade)

3)參數(shù)獲取

通過 $route.params.userid 獲取傳遞的值

(2)query方式

  • 配置路由格式:?/router?,也就是普通配置
  • 傳遞的方式:對象中使用query的key作為傳遞方式
  • 傳遞后形成的路徑:?/route?id=123?

1)路由定義

//方式1:直接在router-link 標(biāo)簽上以對象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">檔案</router-link>

// 方式2:寫成按鈕以點擊事件形式
<button @click='profileClick'>我的</button>  

profileClick(){
  this.$router.push({
    path: "/profile",
    query: {
        name: "kobi",
        age: "28",
        height: 198
    }
  });
}

2)跳轉(zhuǎn)方法

// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按鈕</router-link>

// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})

// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按鈕</router-link>

// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})

// 方法5:
this.$router.push('/user?uname=' + jsmes)

3)獲取參數(shù)

通過$route.query 獲取傳遞的值

6. Vue-router 路由鉤子在生命周期的體現(xiàn)

一、Vue-Router導(dǎo)航守衛(wèi)

有的時候,需要通過路由來進行一些操作,比如最常見的登錄權(quán)限驗證,當(dāng)用戶滿足條件時,才讓其進入導(dǎo)航,否則就取消跳轉(zhuǎn),并跳到登錄頁面讓其登錄。

為此有很多種方法可以植入路由的導(dǎo)航過程:全局的,單個路由獨享的,或者組件級的

  1. 全局路由鉤子

vue-router全局有三個路由鉤子;

  • router.beforeEach 全局前置守衛(wèi) 進入路由之前
  • router.beforeResolve 全局解析守衛(wèi)(2.5.0+)在 beforeRouteEnter 調(diào)用之后調(diào)用
  • router.afterEach 全局后置鉤子 進入路由之后

具體使用∶

  • beforeEach(判斷是否登錄了,沒登錄就跳轉(zhuǎn)到登錄頁)
router.beforeEach((to, from, next) => {  
    let ifInfo = Vue.prototype.$common.getSession('userData');  // 判斷是否登錄的存儲信息
    if (!ifInfo) { 
        // sessionStorage里沒有儲存user信息  
        if (to.path == '/') { 
            //如果是登錄頁面路徑,就直接next()    
            next();  
        } else { 
            //不然就跳轉(zhuǎn)到登錄    
            Message.warning("請重新登錄!");   
            window.location.href = Vue.prototype.$loginUrl;  
        }  
    } else {  
        return next();  
    }
})
  • afterEach (跳轉(zhuǎn)之后滾動條回到頂部)
router.afterEach((to, from) => {  
    // 跳轉(zhuǎn)之后滾動條回到頂部  
    window.scrollTo(0,0);
});
  1. 單個路由獨享鉤子

beforeEnter

如果不想全局配置守衛(wèi)的話,可以為某些路由單獨配置守衛(wèi),有三個參數(shù)∶ to、from、next

export default [  
    {      
        path: '/',      
        name: 'login',      
        component: login,      
        beforeEnter: (to, from, next) => {        
            console.log('即將進入登錄頁面')        
            next()      
        }  
    }
]
  1. 組件內(nèi)鉤子

beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave

這三個鉤子都有三個參數(shù)∶to、from、next

  • beforeRouteEnter∶ 進入組件前觸發(fā)
  • beforeRouteUpdate∶ 當(dāng)前地址改變并且改組件被復(fù)用時觸發(fā),舉例來說,帶有動態(tài)參數(shù)的路徑foo/∶id,在 /foo/1 和 /foo/2 之間跳轉(zhuǎn)的時候,由于會渲染同樣的foa組件,這個鉤子在這種情況下就會被調(diào)用
  • beforeRouteLeave∶ 離開組件被調(diào)用

注意點,beforeRouteEnter組件內(nèi)還訪問不到this,因為該守衛(wèi)執(zhí)行前組件實例還沒有被創(chuàng)建,需要傳一個回調(diào)給 next來訪問,例如:

beforeRouteEnter(to, from, next) {    
    next(target => {      
        if (from.path == '/classProcess') {        
            target.isFromProcess = true      
        }    
    })  
}

二、Vue路由鉤子在生命周期函數(shù)的體現(xiàn)

  1. 完整的路由導(dǎo)航解析流程(不包括其他生命周期)
  • 觸發(fā)進入其他路由。
  • 調(diào)用要離開路由的組件守衛(wèi)beforeRouteLeave
  • 調(diào)用局前置守衛(wèi)∶ beforeEach
  • 在重用的組件里調(diào)用 beforeRouteUpdate
  • 調(diào)用路由獨享守衛(wèi) beforeEnter。
  • 解析異步路由組件。
  • 在將要進入的路由組件中調(diào)用 beforeRouteEnter
  • 調(diào)用全局解析守衛(wèi) beforeResolve
  • 導(dǎo)航被確認(rèn)。
  • 調(diào)用全局后置鉤子的 afterEach 鉤子。
  • 觸發(fā)DOM更新(mounted)。
  • 執(zhí)行beforeRouteEnter 守衛(wèi)中傳給 next 的回調(diào)函數(shù)
  1. 觸發(fā)鉤子的完整順序

路由導(dǎo)航、keep-alive、和組件生命周期鉤子結(jié)合起來的,觸發(fā)順序,假設(shè)是從a組件離開,第一次進入b組件∶

  • beforeRouteLeave:路由組件的組件離開路由前鉤子,可取消路由離開。
  • beforeEach:路由全局前置守衛(wèi),可用于登錄驗證、全局路由loading等。
  • beforeEnter:路由獨享守衛(wèi)
  • beforeRouteEnter:路由組件的組件進入路由前鉤子。
  • beforeResolve:路由全局解析守衛(wèi)
  • afterEach:路由全局后置鉤子
  • beforeCreate:組件生命周期,不能訪問tAis。
  • created;組件生命周期,可以訪問tAis,不能訪問dom。
  • beforeMount:組件生命周期
  • deactivated:離開緩存組件a,或者觸發(fā)a的beforeDestroy和destroyed組件銷毀鉤子。
  • mounted:訪問/操作dom。
  • activated:進入緩存組件,進入a的嵌套子組件(如果有的話)。
  • 執(zhí)行beforeRouteEnter回調(diào)函數(shù)next。
  1. 導(dǎo)航行為被觸發(fā)到導(dǎo)航完成的整個過程
  • 導(dǎo)航行為被觸發(fā),此時導(dǎo)航未被確認(rèn)。
  • 在失活的組件里調(diào)用離開守衛(wèi) beforeRouteLeave。
  • 調(diào)用全局的 beforeEach守衛(wèi)。
  • 在重用的組件里調(diào)用 beforeRouteUpdate 守衛(wèi)(2.2+)。
  • 在路由配置里調(diào)用 beforeEnteY。
  • 解析異步路由組件(如果有)。
  • 在被激活的組件里調(diào)用 beforeRouteEnter。
  • 調(diào)用全局的 beforeResolve 守衛(wèi)(2.5+),標(biāo)示解析階段完成。
  • 導(dǎo)航被確認(rèn)。
  • 調(diào)用全局的 afterEach 鉤子。
  • 非重用組件,開始組件實例的生命周期:beforeCreate&created、beforeMount&mounted
  • 觸發(fā) DOM 更新。
  • 用創(chuàng)建好的實例調(diào)用 beforeRouteEnter守衛(wèi)中傳給 next 的回調(diào)函數(shù)。
  • 導(dǎo)航完成

7. Vue-router跳轉(zhuǎn)和location.href有什么區(qū)別

  • 使用 ?location.href= /url? 來跳轉(zhuǎn),簡單方便,但是刷新了頁面;
  • 使用 ?history.pushState( /url )? ,無刷新頁面,靜態(tài)跳轉(zhuǎn);
  • 引進 router ,然后使用 ?router.push( /url )? 來跳轉(zhuǎn),使用了 ?diff ?算法,實現(xiàn)了按需加載,減少了 dom 的消耗。其實使用 router 跳轉(zhuǎn)和使用 ?history.pushState()? 沒什么差別的,因為vue-router就是用了 ?history.pushState()? ,尤其是在history模式下。

8. params和query的區(qū)別

用法:query要用path來引入,params要用name來引入,接收參數(shù)都是類似的,分別是 this.$route.query.name 和 this.$route.params.name 。

url地址顯示:query更加類似于ajax中g(shù)et傳參,params則類似于post,說的再簡單一點,前者在瀏覽器地址欄中顯示參數(shù),后者則不顯示

注意:query刷新不會丟失query里面的數(shù)據(jù) params刷新會丟失 params里面的數(shù)據(jù)。

9. Vue-router 導(dǎo)航守衛(wèi)有哪些

  • 全局前置/鉤子:beforeEach、beforeResolve、afterEach
  • 路由獨享的守衛(wèi):beforeEnter
  • 組件內(nèi)的守衛(wèi):beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

10. 對前端路由的理解

在前端技術(shù)早期,一個 url 對應(yīng)一個頁面,如果要從 A 頁面切換到 B 頁面,那么必然伴隨著頁面的刷新。這個體驗并不好,不過在最初也是無奈之舉——用戶只有在刷新頁面的情況下,才可以重新去請求數(shù)據(jù)。

后來,改變發(fā)生了——Ajax 出現(xiàn)了,它允許人們在不刷新頁面的情況下發(fā)起請求;與之共生的,還有“不刷新頁面即可更新頁面內(nèi)容”這種需求。在這樣的背景下,出現(xiàn)了 SPA(單頁面應(yīng)用)。

SPA極大地提升了用戶體驗,它允許頁面在不刷新的情況下更新頁面內(nèi)容,使內(nèi)容的切換更加流暢。但是在 SPA 誕生之初,人們并沒有考慮到“定位”這個問題——在內(nèi)容切換前后,頁面的 URL 都是一樣的,這就帶來了兩個問題:

  • SPA 其實并不知道當(dāng)前的頁面“進展到了哪一步”??赡茉谝粋€站點下經(jīng)過了反復(fù)的“前進”才終于喚出了某一塊內(nèi)容,但是此時只要刷新一下頁面,一切就會被清零,必須重復(fù)之前的操作、才可以重新對內(nèi)容進行定位——SPA 并不會“記住”你的操作。
  • 由于有且僅有一個 URL 給頁面做映射,這對 SEO 也不夠友好,搜索引擎無法收集全面的信息

為了解決這個問題,前端路由出現(xiàn)了。

前端路由可以幫助我們在僅有一個頁面的情況下,“記住”用戶當(dāng)前走到了哪一步——為 SPA 中的各個視圖匹配一個唯一標(biāo)識。這意味著用戶前進、后退觸發(fā)的新內(nèi)容,都會映射到不同的 URL 上去。此時即便他刷新頁面,因為當(dāng)前的 URL 可以標(biāo)識出他所處的位置,因此內(nèi)容也不會丟失。

那么如何實現(xiàn)這個目的呢?首先要解決兩個問題:

  • 當(dāng)用戶刷新頁面時,瀏覽器會默認(rèn)根據(jù)當(dāng)前 URL 對資源進行重新定位(發(fā)送請求)。這個動作對 SPA 是不必要的,因為我們的 SPA 作為單頁面,無論如何也只會有一個資源與之對應(yīng)。此時若走正常的請求-刷新流程,反而會使用戶的前進后退操作無法被記錄。
  • 單頁面應(yīng)用對服務(wù)端來說,就是一個URL、一套資源,那么如何做到用“不同的URL”來映射不同的視圖內(nèi)容呢?

從這兩個問題來看,服務(wù)端已經(jīng)完全救不了這個場景了。所以要靠咱們前端自力更生,不然怎么叫“前端路由”呢?作為前端,可以提供這樣的解決思路:

  • 攔截用戶的刷新操作,避免服務(wù)端盲目響應(yīng)、返回不符合預(yù)期的資源內(nèi)容。把刷新這個動作完全放到前端邏輯里消化掉。
  • 感知 URL 的變化。這里不是說要改造 URL、憑空制造出 N 個 URL 來。而是說 URL 還是那個 URL,只不過我們可以給它做一些微小的處理——這些處理并不會影響 URL 本身的性質(zhì),不會影響服務(wù)器對它的識別,只有我們前端感知的到。一旦我們感知到了,我們就根據(jù)這些變化、用 JS 去給它生成不同的內(nèi)容。

五、Vuex


1. Vuex 的原理

Vuex 是一個專為 Vue.js 應(yīng)用程序開發(fā)的狀態(tài)管理模式。每一個 Vuex 應(yīng)用的核心就是 store(倉庫)?!皊tore” 基本上就是一個容器,它包含著你的應(yīng)用中大部分的狀態(tài) ( state )。

  • Vuex 的狀態(tài)存儲是響應(yīng)式的。當(dāng) Vue 組件從 store 中讀取狀態(tài)的時候,若 store 中的狀態(tài)發(fā)生變化,那么相應(yīng)的組件也會相應(yīng)地得到高效更新。
  • 改變 store 中的狀態(tài)的唯一途徑就是顯式地提交 (commit) mutation。這樣可以方便地跟蹤每一個狀態(tài)的變化。


Vuex為Vue Components建立起了一個完整的生態(tài)圈,包括開發(fā)中的API調(diào)用一環(huán)。

(1)核心流程中的主要功能:

  • Vue Components 是 vue 組件,組件會觸發(fā)(dispatch)一些事件或動作,也就是圖中的 Actions;
  • 在組件中發(fā)出的動作,肯定是想獲取或者改變數(shù)據(jù)的,但是在 vuex 中,數(shù)據(jù)是集中管理的,不能直接去更改數(shù)據(jù),所以會把這個動作提交(Commit)到 Mutations 中;
  • 然后 Mutations 就去改變(Mutate)State 中的數(shù)據(jù);
  • 當(dāng) State 中的數(shù)據(jù)被改變之后,就會重新渲染(Render)到 Vue Components 中去,組件展示更新后的數(shù)據(jù),完成一個流程。

(2)各模塊在核心流程中的主要功能:

  • ?Vue Components?∶ Vue組件。HTML頁面上,負(fù)責(zé)接收用戶操作等交互行為,執(zhí)行dispatch方法觸發(fā)對應(yīng)action進行回應(yīng)。
  • ?dispatch?∶操作行為觸發(fā)方法,是唯一能執(zhí)行action的方法。
  • ?actions?∶ 操作行為處理模塊。負(fù)責(zé)處理Vue Components接收到的所有交互行為。包含同步/異步操作,支持多個同名方法,按照注冊的順序依次觸發(fā)。向后臺API請求的操作就在這個模塊中進行,包括觸發(fā)其他action以及提交mutation的操作。該模塊提供了Promise的封裝,以支持action的鏈?zhǔn)接|發(fā)。
  • ?commit?∶狀態(tài)改變提交操作方法。對mutation進行提交,是唯一能執(zhí)行mutation的方法。
  • ?mutations?∶狀態(tài)改變操作方法。是Vuex修改state的唯一推薦方法,其他修改方式在嚴(yán)格模式下將會報錯。該方法只能進行同步操作,且方法名只能全局唯一。操作之中會有一些hook暴露出來,以進行state的監(jiān)控等。
  • ?state?∶ 頁面狀態(tài)管理容器對象。集中存儲Vuecomponents中data對象的零散數(shù)據(jù),全局唯一,以進行統(tǒng)一的狀態(tài)管理。頁面顯示所需的數(shù)據(jù)從該對象中進行讀取,利用Vue的細(xì)粒度數(shù)據(jù)響應(yīng)機制來進行高效的狀態(tài)更新。
  • ?getters?∶ state對象讀取方法。圖中沒有單獨列出該模塊,應(yīng)該被包含在了render中,Vue Components通過該方法讀取全局state對象。

2. Vuex中action和mutation的區(qū)別

mutation中的操作是一系列的同步函數(shù),用于修改state中的變量的的狀態(tài)。當(dāng)使用vuex時需要通過commit來提交需要操作的內(nèi)容。mutation 非常類似于事件:每個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調(diào)函數(shù) (handler)。這個回調(diào)函數(shù)就是實際進行狀態(tài)更改的地方,并且它會接受 state 作為第一個參數(shù):

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      state.count++      // 變更狀態(tài)
    }
  }
})

當(dāng)觸發(fā)一個類型為 increment 的 mutation 時,需要調(diào)用此函數(shù):

store.commit('increment')

而Action類似于mutation,不同點在于:

  • Action 可以包含任意異步操作。
  • Action 提交的是 mutation,而不是直接變更狀態(tài)。
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 函數(shù)接受一個與 store 實例具有相同方法和屬性的 context 對象,因此你可以調(diào)用 context.commit 提交一個 mutation,或者通過 context.state 和 context.getters 來獲取 state 和 getters。

所以,兩者的不同點如下:

  • Mutation專注于修改State,理論上是修改State的唯一途徑;Action業(yè)務(wù)代碼、異步請求。
  • Mutation:必須同步執(zhí)行;Action:可以異步,但不能直接操作State。
  • 在視圖更新時,先觸發(fā)actions,actions再觸發(fā)mutation
  • mutation的參數(shù)是state,它包含store中的數(shù)據(jù);store的參數(shù)是context,它是 state 的父級,包含 state、getters

3. Vuex 和 localStorage 的區(qū)別

(1)最重要的區(qū)別

  • vuex存儲在內(nèi)存中
  • localstorage 則以文件的方式存儲在本地,只能存儲字符串類型的數(shù)據(jù),存儲對象需要 JSON的stringify和parse方法進行處理。 讀取內(nèi)存比讀取硬盤速度要快

(2)應(yīng)用場景

  • Vuex 是一個專為 Vue.js 應(yīng)用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲管理應(yīng)用的所有組件的狀態(tài),并以相應(yīng)的規(guī)則保證狀態(tài)以一種可預(yù)測的方式發(fā)生變化。vuex用于組件之間的傳值。
  • localstorage是本地存儲,是將數(shù)據(jù)存儲到瀏覽器的方法,一般是在跨頁面?zhèn)鬟f數(shù)據(jù)時使用 。
  • Vuex能做到數(shù)據(jù)的響應(yīng)式,localstorage不能

(3)永久性

刷新頁面時vuex存儲的值會丟失,localstorage不會。

注意:對于不變的數(shù)據(jù)確實可以用localstorage可以代替vuex,但是當(dāng)兩個組件共用一個數(shù)據(jù)源(對象或數(shù)組)時,如果其中一個組件改變了該數(shù)據(jù)源,希望另一個組件響應(yīng)該變化時,localstorage無法做到,原因就是區(qū)別1。

4. Redux 和 Vuex 有什么區(qū)別,它們的共同思想

(1)Redux 和 Vuex區(qū)別

  • Vuex改進了Redux中的Action和Reducer函數(shù),以mutations變化函數(shù)取代Reducer,無需switch,只需在對應(yīng)的mutation函數(shù)里改變state值即可
  • Vuex由于Vue自動重新渲染的特性,無需訂閱重新渲染函數(shù),只要生成新的State即可
  • Vuex數(shù)據(jù)流的順序是∶View調(diào)用store.commit提交對應(yīng)的請求到Store中對應(yīng)的mutation函數(shù)->store改變(vue檢測到數(shù)據(jù)變化自動渲染)

通俗點理解就是,vuex 弱化 dispatch,通過commit進行 store狀態(tài)的一次更變;取消了action概念,不必傳入特定的 action形式進行指定變更;弱化reducer,基于commit參數(shù)直接對數(shù)據(jù)進行轉(zhuǎn)變,使得框架更加簡易;

(2)共同思想

  • 單—的數(shù)據(jù)源
  • 變化可以預(yù)測

本質(zhì)上:redux與vuex都是對mvvm思想的服務(wù),將數(shù)據(jù)從視圖中抽離的一種方案;

形式上:vuex借鑒了redux,將store作為全局的數(shù)據(jù)中心,進行mode管理;

5. 為什么要用 Vuex 或者 Redux

由于傳參的方法對于多層嵌套的組件將會非常繁瑣,并且對于兄弟組件間的狀態(tài)傳遞無能為力。我們經(jīng)常會采用父子組件直接引用或者通過事件來變更和同步狀態(tài)的多份拷貝。以上的這些模式非常脆弱,通常會導(dǎo)致代碼無法維護。

所以需要把組件的共享狀態(tài)抽取出來,以一個全局單例模式管理。在這種模式下,組件樹構(gòu)成了一個巨大的"視圖",不管在樹的哪個位置,任何組件都能獲取狀態(tài)或者觸發(fā)行為。

另外,通過定義和隔離狀態(tài)管理中的各種概念并強制遵守一定的規(guī)則,代碼將會變得更結(jié)構(gòu)化且易維護。

6. Vuex有哪幾種屬性?

有五種,分別是 State、 Getter、Mutation 、Action、 Module

  • state => 基本數(shù)據(jù)(數(shù)據(jù)源存放地)
  • getters => 從基本數(shù)據(jù)派生出來的數(shù)據(jù)
  • mutations => 提交更改數(shù)據(jù)的方法,同步
  • actions => 像一個裝飾器,包裹mutations,使之可以異步。
  • modules => 模塊化Vuex

7. Vuex和單純的全局對象有什么區(qū)別?

  • Vuex 的狀態(tài)存儲是響應(yīng)式的。當(dāng) Vue 組件從 store 中讀取狀態(tài)的時候,若 store 中的狀態(tài)發(fā)生變化,那么相應(yīng)的組件也會相應(yīng)地得到高效更新。
  • 不能直接改變 store 中的狀態(tài)。改變 store 中的狀態(tài)的唯一途徑就是顯式地提交 (commit) mutation。這樣可以方便地跟蹤每一個狀態(tài)的變化,從而能夠?qū)崿F(xiàn)一些工具幫助更好地了解我們的應(yīng)用。

8. 為什么 Vuex 的 mutation 中不能做異步操作?

  • Vuex中所有的狀態(tài)更新的唯一途徑都是mutation,異步操作通過 Action 來提交 mutation實現(xiàn),這樣可以方便地跟蹤每一個狀態(tài)的變化,從而能夠?qū)崿F(xiàn)一些工具幫助更好地了解我們的應(yīng)用。
  • 每個mutation執(zhí)行完成后都會對應(yīng)到一個新的狀態(tài)變更,這樣devtools就可以打個快照存下來,然后就可以實現(xiàn) time-travel 了。如果mutation支持異步操作,就沒有辦法知道狀態(tài)是何時更新的,無法很好的進行狀態(tài)的追蹤,給調(diào)試帶來困難。

9. Vuex的嚴(yán)格模式是什么,有什么作用,如何開啟?

在嚴(yán)格模式下,無論何時發(fā)生了狀態(tài)變更且不是由mutation函數(shù)引起的,將會拋出錯誤。這能保證所有的狀態(tài)變更都能被調(diào)試工具跟蹤到。

在Vuex.Store 構(gòu)造器選項中開啟,如下

const store = new Vuex.Store({
    strict:true,
})

10. 如何在組件中批量使用Vuex的getter屬性

使用mapGetters輔助函數(shù), 利用對象展開運算符將getter混入computed 對象中

import {mapGetters} from 'vuex'
export default{
    computed:{
        ...mapGetters(['total','discountTotal'])
    }
}

11. 如何在組件中重復(fù)使用Vuex的mutation

使用mapMutations輔助函數(shù),在組件中這么使用

import { mapMutations } from 'vuex'
methods:{
    ...mapMutations({
        setNumber:'SET_NUMBER',
    })
}

然后調(diào)用 this.setNumber(10)相當(dāng)調(diào)用 this.$store.commit('SET_NUMBER',10)

六、Vue 3.0


1. Vue3.0有什么更新

(1)監(jiān)測機制的改變

  • 3.0 將帶來基于代理 Proxy的 observer 實現(xiàn),提供全語言覆蓋的反應(yīng)性跟蹤。
  • 消除了 Vue 2 當(dāng)中基于 Object.defineProperty 的實現(xiàn)所存在的很多限制:

(2)只能監(jiān)測屬性,不能監(jiān)測對象

  • 檢測屬性的添加和刪除;
  • 檢測數(shù)組索引和長度的變更;
  • 支持 Map、Set、WeakMap 和 WeakSet。

(3)模板

  • 作用域插槽,2.x 的機制導(dǎo)致作用域插槽變了,父組件會重新渲染,而 3.0 把作用域插槽改成了函數(shù)的方式,這樣只會影響子組件的重新渲染,提升了渲染的性能。
  • 同時,對于 render 函數(shù)的方面,vue3.0 也會進行一系列更改來方便習(xí)慣直接使用 api 來生成 vdom 。

(4)對象式的組件聲明方式

  • vue2.x 中的組件是通過聲明的方式傳入一系列 option,和 TypeScript 的結(jié)合需要通過一些裝飾器的方式來做,雖然能實現(xiàn)功能,但是比較麻煩。
  • 3.0 修改了組件的聲明方式,改成了類式的寫法,這樣使得和 TypeScript 的結(jié)合變得很容易

(5)其它方面的更改

  • 支持自定義渲染器,從而使得 weex 可以通過自定義渲染器的方式來擴展,而不是直接 fork 源碼來改的方式。
  • 支持 Fragment(多個根節(jié)點)和 Protal(在 dom 其他部分渲染組建內(nèi)容)組件,針對一些特殊的場景做了處理。
  • 基于 tree shaking 優(yōu)化,提供了更多的內(nèi)置功能。

2. defineProperty和proxy的區(qū)別

Vue 在實例初始化時遍歷 data 中的所有屬性,并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter。這樣當(dāng)追蹤數(shù)據(jù)發(fā)生變化時,setter 會被自動調(diào)用。

Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的原因。

但是這樣做有以下問題:

  1. 添加或刪除對象的屬性時,Vue 檢測不到。因為添加或刪除的對象沒有在初始化進行響應(yīng)式處理,只能通過 ?$set? 來調(diào)用 ?Object.defineProperty()?處理。
  2. 無法監(jiān)控到數(shù)組下標(biāo)和長度的變化。

Vue3 使用 Proxy 來監(jiān)控數(shù)據(jù)的變化。Proxy 是 ES6 中提供的功能,其作用為:用于定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數(shù)調(diào)用等)。相對于 ?Object.defineProperty()?,其有以下特點:

  1. Proxy 直接代理整個對象而非對象屬性,這樣只需做一層代理就可以監(jiān)聽同級結(jié)構(gòu)下的所有屬性變化,包括新增屬性和刪除屬性。
  2. Proxy 可以監(jiān)聽數(shù)組的變化。

3. Vue3.0 為什么要用 proxy?

在 Vue2 中, 0bject.defineProperty 會改變原始數(shù)據(jù),而 Proxy 是創(chuàng)建對象的虛擬表示,并提供 set 、get 和 deleteProperty 等處理器,這些處理器可在訪問或修改原始對象上的屬性時進行攔截,有以下特點∶

  • 不需用使用 ?Vue.$set? 或 ?Vue.$delete? 觸發(fā)響應(yīng)式。
  • 全方位的數(shù)組變化檢測,消除了Vue2 無效的邊界情況。
  • 支持 Map,Set,WeakMap 和 WeakSet。

Proxy 實現(xiàn)的響應(yīng)式原理與 Vue2的實現(xiàn)原理相同,實現(xiàn)方式大同小異∶

  • get 收集依賴
  • Set、delete 等觸發(fā)依賴
  • 對于集合類型,就是對集合對象的方法做一層包裝:原方法執(zhí)行后執(zhí)行依賴相關(guān)的收集或觸發(fā)邏輯。

4. Vue 3.0 中的 Vue Composition API?

在 Vue2 中,代碼是 Options API 風(fēng)格的,也就是通過填充 (option) data、methods、computed 等屬性來完成一個 Vue 組件。這種風(fēng)格使得 Vue 相對于 React極為容易上手,同時也造成了幾個問題:

  1. 由于 Options API 不夠靈活的開發(fā)方式,使得Vue開發(fā)缺乏優(yōu)雅的方法來在組件間共用代碼。
  2. Vue 組件過于依賴 ?this?上下文,Vue 背后的一些小技巧使得 Vue 組件的開發(fā)看起來與 JavaScript 的開發(fā)原則相悖,比如在 ?methods ?中的 ?this?竟然指向組件實例來不指向 ?methods?所在的對象。這也使得 TypeScript 在Vue2 中很不好用。

于是在 Vue3 中,舍棄了 Options API,轉(zhuǎn)而投向 Composition API。Composition API本質(zhì)上是將 Options API 背后的機制暴露給用戶直接使用,這樣用戶就擁有了更多的靈活性,也使得 Vue3 更適合于 TypeScript 結(jié)合。

如下,是一個使用了 Vue Composition API 的 Vue3 組件:

<template>
  <button @click="increment">
    Count: {{ count }}
  </button>
</template>
 
<script>
// Composition API 將組件屬性暴露為函數(shù),因此第一步是導(dǎo)入所需的函數(shù)
import { ref, computed, onMounted } from 'vue'
 
export default {
  setup() {
// 使用 ref 函數(shù)聲明了稱為 count 的響應(yīng)屬性,對應(yīng)于Vue2中的data函數(shù)
    const count = ref(0)
 
// Vue2中需要在methods option中聲明的函數(shù),現(xiàn)在直接聲明
    function increment() {
      count.value++
    }
 // 對應(yīng)于Vue2中的mounted聲明周期
    onMounted(() => console.log('component mounted!'))
 
    return {
      count,
      increment
    }
  }
}
</script>

顯而易見,Vue Composition API 使得 Vue3 的開發(fā)風(fēng)格更接近于原生 JavaScript,帶給開發(fā)者更多地靈活性

5. Composition API與React Hook很像,區(qū)別是什么

從React Hook的實現(xiàn)角度看,React Hook是根據(jù)useState調(diào)用的順序來確定下一次重渲染時的state是來源于哪個useState,所以出現(xiàn)了以下限制

  • 不能在循環(huán)、條件、嵌套函數(shù)中調(diào)用Hook
  • 必須確??偸窃谀愕腞eact函數(shù)的頂層調(diào)用Hook
  • useEffect、useMemo等函數(shù)必須手動確定依賴關(guān)系

而Composition API是基于Vue的響應(yīng)式系統(tǒng)實現(xiàn)的,與React Hook的相比

  • 聲明在setup函數(shù)內(nèi),一次組件實例化只調(diào)用一次setup,而React Hook每次重渲染都需要調(diào)用Hook,使得React的GC比Vue更有壓力,性能也相對于Vue來說也較慢
  • Compositon API的調(diào)用不需要顧慮調(diào)用順序,也可以在循環(huán)、條件、嵌套函數(shù)中使用
  • 響應(yīng)式系統(tǒng)自動實現(xiàn)了依賴收集,進而組件的部分的性能優(yōu)化由Vue內(nèi)部自己完成,而React Hook需要手動傳入依賴,而且必須必須保證依賴的順序,讓useEffect、useMemo等函數(shù)正確的捕獲依賴變量,否則會由于依賴不正確使得組件性能下降。

雖然Compositon API看起來比React Hook好用,但是其設(shè)計思想也是借鑒React Hook的。

七、虛擬DOM


1. 對虛擬DOM的理解?

從本質(zhì)上來說,Virtual Dom是一個JavaScript對象,通過對象的方式來表示DOM結(jié)構(gòu)。將頁面的狀態(tài)抽象為JS對象的形式,配合不同的渲染工具,使跨平臺渲染成為可能。通過事務(wù)處理機制,將多次DOM修改的結(jié)果一次性的更新到頁面上,從而有效的減少頁面渲染的次數(shù),減少修改DOM的重繪重排次數(shù),提高渲染性能。

虛擬DOM是對DOM的抽象,這個對象是更加輕量級的對 DOM的描述。它設(shè)計的最初目的,就是更好的跨平臺,比如Node.js就沒有DOM,如果想實現(xiàn)SSR,那么一個方式就是借助虛擬DOM,因為虛擬DOM本身是js對象。 在代碼渲染到頁面之前,vue會把代碼轉(zhuǎn)換成一個對象(虛擬 DOM)。以對象的形式來描述真實DOM結(jié)構(gòu),最終渲染到頁面。在每次數(shù)據(jù)發(fā)生變化前,虛擬DOM都會緩存一份,變化之時,現(xiàn)在的虛擬DOM會與緩存的虛擬DOM進行比較。在vue內(nèi)部封裝了diff算法,通過這個算法來進行比較,渲染時修改改變的變化,原先沒有發(fā)生改變的通過原先的數(shù)據(jù)進行渲染。

另外現(xiàn)代前端框架的一個基本要求就是無須手動操作DOM,一方面是因為手動操作DOM無法保證程序性能,多人協(xié)作的項目中如果review不嚴(yán)格,可能會有開發(fā)者寫出性能較低的代碼,另一方面更重要的是省略手動DOM操作可以大大提高開發(fā)效率。

2. 虛擬DOM的解析過程

虛擬DOM的解析過程:

  • 首先對將要插入到文檔中的 DOM 樹結(jié)構(gòu)進行分析,使用 js 對象將其表示出來,比如一個元素對象,包含 TagName、props 和 Children 這些屬性。然后將這個 js 對象樹給保存下來,最后再將 DOM 片段插入到文檔中。
  • 當(dāng)頁面的狀態(tài)發(fā)生改變,需要對頁面的 DOM 的結(jié)構(gòu)進行調(diào)整的時候,首先根據(jù)變更的狀態(tài),重新構(gòu)建起一棵對象樹,然后將這棵新的對象樹和舊的對象樹進行比較,記錄下兩棵樹的的差異。
  • 最后將記錄的有差異的地方應(yīng)用到真正的 DOM 樹中去,這樣視圖就更新了。

3. 為什么要用虛擬DOM

(1)保證性能下限,在不進行手動優(yōu)化的情況下,提供過得去的性能

看一下頁面渲染的流程:解析HTML -> 生成DOM -> 生成 CSSOM -> Layout -> Paint -> Compiler

下面對比一下修改DOM時真實DOM操作和Virtual DOM的過程,來看一下它們重排重繪的性能消耗∶

  • 真實DOM∶ 生成HTML字符串+重建所有的DOM元素
  • 虛擬DOM∶ 生成vNode+ DOMDiff+必要的dom更新

Virtual DOM的更新DOM的準(zhǔn)備工作耗費更多的時間,也就是JS層面,相比于更多的DOM操作它的消費是極其便宜的。尤雨溪在社區(qū)論壇中說道∶ 框架給你的保證是,你不需要手動優(yōu)化的情況下,依然可以給你提供過得去的性能。

(2)跨平臺

Virtual DOM本質(zhì)上是JavaScript的對象,它可以很方便的跨平臺操作,比如服務(wù)端渲染、uniapp等。

4. 虛擬DOM真的比真實DOM性能好嗎

  • 首次渲染大量DOM時,由于多了一層虛擬DOM的計算,會比innerHTML插入慢。
  • 正如它能保證性能下限,在真實DOM操作的時候進行針對性的優(yōu)化時,還是更快的。

5. DIFF算法的原理

在新老虛擬DOM對比時:

  • 首先,對比節(jié)點本身,判斷是否為同一節(jié)點,如果不為相同節(jié)點,則刪除該節(jié)點重新創(chuàng)建節(jié)點進行替換
  • 如果為相同節(jié)點,進行patchVnode,判斷如何對該節(jié)點的子節(jié)點進行處理,先判斷一方有子節(jié)點一方?jīng)]有子節(jié)點的情況(如果新的children沒有子節(jié)點,將舊的子節(jié)點移除)
  • 比較如果都有子節(jié)點,則進行updateChildren,判斷如何對這些新老節(jié)點的子節(jié)點進行操作(diff核心)。
  • 匹配時,找到相同的子節(jié)點,遞歸比較子節(jié)點

在diff中,只對同層的子節(jié)點進行比較,放棄跨級的節(jié)點比較,使得時間復(fù)雜從O(n3)降低值O(n),也就是說,只有當(dāng)新舊children都為多個子節(jié)點時才需要用核心的Diff算法進行同層級比較。

6. Vue中key的作用

標(biāo)識元素的身份,實現(xiàn)高效復(fù)用,在更新渲染時更加高效,準(zhǔn)確key隨元素移動不會產(chǎn)生順序錯誤。 vue 中 key 值的作用可以分為兩種情況來考慮:

  • 第一種情況是 v-if 中使用 key。由于 Vue 會盡可能高效地渲染元素,通常會復(fù)用已有元素而不是從頭開始渲染。因此當(dāng)使用 v-if 來實現(xiàn)元素切換的時候,如果切換前后含有相同類型的元素,那么這個元素就會被復(fù)用。如果是相同的 input 元素,那么切換前后用戶的輸入不會被清除掉,這樣是不符合需求的。因此可以通過使用 key 來唯一的標(biāo)識一個元素,這個情況下,使用 key 的元素不會被復(fù)用。這個時候 key 的作用是用來標(biāo)識一個獨立的元素。
  • 第二種情況是 v-for 中使用 key。用 v-for 更新已渲染過的元素列表時,它默認(rèn)使用“就地復(fù)用”的策略。如果數(shù)據(jù)項的順序發(fā)生了改變,Vue 不會移動 DOM 元素來匹配數(shù)據(jù)項的順序,而是簡單復(fù)用此處的每個元素。因此通過為每個列表項提供一個 key 值,來以便 Vue 跟蹤元素的身份,從而高效的實現(xiàn)復(fù)用。這個時候 key 的作用是為了高效的更新渲染虛擬 DOM。

key 是為 Vue 中 vnode 的唯一標(biāo)記,通過這個 key,diff 操作可以更準(zhǔn)確、更快速

  • 更準(zhǔn)確:因為帶 key 就不是就地復(fù)用了,在 sameNode 函數(shù)a.key === b.key對比中可以避免就地復(fù)用的情況。所以會更加準(zhǔn)確。
  • 更快速:利用 key 的唯一性生成 map 對象來獲取對應(yīng)節(jié)點,比遍歷方式更快

7. 為什么不建議用index作為key?

使用index 作為 key和沒寫基本上沒區(qū)別,因為不管數(shù)組的順序怎么顛倒,index 都是 0, 1, 2...這樣排列,導(dǎo)致 Vue 會復(fù)用錯誤的舊子節(jié)點,做很多額外的工作。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號