前端面試 Vue篇

2023-02-17 10:50 更新

一、Vue 基礎(chǔ)


1. Vue的基本原理

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


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

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

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


3. 使用 Object.defineProperty() 來(lái)進(jìn)行數(shù)據(jù)劫持有什么缺點(diǎn)?

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

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

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

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

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

(1)MVC

MVC 通過(guò)分離 Model、View 和 Controller 的方式來(lái)組織代碼結(jié)構(gòu)。其中 View 負(fù)責(zé)頁(yè)面的顯示邏輯,Model 負(fù)責(zé)存儲(chǔ)頁(yè)面的業(yè)務(wù)數(shù)據(jù),以及對(duì)相應(yīng)數(shù)據(jù)的操作。并且 View 和 Model 應(yīng)用了觀察者模式,當(dāng) Model 層發(fā)生改變的時(shí)候它會(huì)通知有關(guān) View 層更新頁(yè)面。Controller 層是 View 層和 Model 層的紐帶,它主要負(fù)責(zé)用戶與應(yīng)用的響應(yīng)操作,當(dāng)用戶與頁(yè)面產(chǎn)生交互的時(shí)候,Controller 中的事件觸發(fā)器就開(kāi)始工作了,通過(guò)調(diào)用 Model 層,來(lái)完成對(duì) 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)聽(tīng)Model中數(shù)據(jù)的改變并且控制視圖的更新,處理用戶交互操作;

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

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


(3)MVP

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

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

對(duì)于Computed:

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

對(duì)于Watch:

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

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

總結(jié):

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

運(yùn)用場(chǎng)景:

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

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

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

不同點(diǎn):

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

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

slot又名插槽,是Vue的內(nèi)容分發(fā)機(jī)制,組件內(nèi)部的模板引擎使用slot元素作為承載分發(fā)內(nèi)容的出口。插槽slot是子組件的一個(gè)模板標(biāo)簽元素,而這一個(gè)標(biāo)簽元素是否顯示,以及怎么顯示是由父組件決定的。slot又分三類,默認(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ù),則可稱該插槽為作用域插槽。

8. 過(guò)濾器的作用,如何實(shí)現(xiàn)一個(gè)過(guò)濾器

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

使用場(chǎng)景:

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

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

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

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

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

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

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

  • 前組件會(huì)被卸載
  • 前組件不會(huì)被卸載

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

組件會(huì)被卸載:

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

只需要在組件即將被銷毀的生命周期 componentWillUnmount (react)中在 LocalStorage / SessionStorage 中把當(dāng)前組件的 state 通過(guò) JSON.stringify() 儲(chǔ)存下來(lái)就可以了。在這里面需要注意的是組件更新?tīng)顟B(tài)的時(shí)機(jī)。

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

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

(2)路由傳值

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

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

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

組件不會(huì)被卸載:

(1)單頁(yè)面渲染

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

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

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

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

<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. 常見(jiàn)的事件修飾符及其作用

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

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

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

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

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

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

(1)作用在表單元素上

動(dòng)態(tài)綁定了 input 的 value 指向了 messgae 變量,并且在觸發(fā) input 事件的時(shí)候去動(dòng)態(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ā)的事件對(duì)象;
//$event.target 指代當(dāng)前觸發(fā)的事件對(duì)象的dom;
//$event.target.value 就是當(dāng)前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;

(2)作用在組件上

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

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

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

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

例子:

// 父組件
<aa-input v-model="aa"></aa-input>
// 等價(jià)于
<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)情況下,一個(gè)組件上的v-model 會(huì)把 value 用作 prop且把 input 用作 event。但是一些輸入類型比如單選框和復(fù)選框按鈕可能想使用 value prop 來(lái)達(dá)到不同的目的。使用 model 選項(xiàng)可以回避這些情況產(chǎn)生的沖突。js 監(jiān)聽(tīng)input 輸入框輸入數(shù)據(jù)改變,用oninput,數(shù)據(jù)改變以后就會(huì)立刻出發(fā)這個(gè)事件。通過(guò)input事件把數(shù)據(jù)$emit 出去,在父組件接受。父組件設(shè)置v-model的值為input $emit過(guò)來(lái)的值。

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

可以。v-model 實(shí)際上是一個(gè)語(yǔ)法糖,如:

<input v-model="searchText">

實(shí)際上相當(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 組件的實(shí)現(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為什么是一個(gè)函數(shù)而不是對(duì)象

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

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

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

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

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

(1)keep-alive

keep-alive有以下三個(gè)屬性:

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

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

主要流程

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

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

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

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

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

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

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

  mounted() {
    // prune 削減精簡(jiǎn)[v.]
    // 去監(jiān)控include和exclude的改變,根據(jù)最新的include和exclude的內(nèi)容,來(lái)實(shí)時(shí)削減緩存的組件的內(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. 會(huì)在 keep-alive 組件內(nèi)部去寫自己的內(nèi)容,所以可以去獲取默認(rèn) slot 的內(nèi)容,然后根據(jù)這個(gè)去獲取組件
  2. keep-alive 只對(duì)第一個(gè)組件有效,所以獲取第一個(gè)子組件。
  3. 和 keep-alive 搭配使用的一般有:動(dòng)態(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)// 獲取第一個(gè)子組件
  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
      // 那么直接返回組件的實(shí)例
      return vnode
    }

    const { cache, keys } = this

    // 獲取這個(gè)組件的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 // 組件初次渲染的時(shí)候componentInstance為undefined

      // make current key freshest
      remove(keys, key)
      keys.push(key)
      // 根據(jù)LRU緩存策略執(zhí)行,將key從原來(lái)的位置移除,然后將這個(gè)key值放到最后面
    } else {
      // 在緩存列表里面沒(méi)有的話,則加入,同時(shí)判斷當(dāng)前加入之后,是否超過(guò)了max所設(shè)定的范圍,如果是,則去除
      // 使用時(shí)間間隔最長(zhǎng)的一個(gè)
      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 具體是通過(guò) cache 數(shù)組緩存所有組件的 vnode 實(shí)例。當(dāng) cache 內(nèi)原有組件被使用時(shí)會(huì)將該組件 key 從 keys 數(shù)組中刪除,然后 push 到 keys數(shù)組最后,以便清除最不常用組件。

實(shí)現(xiàn)步驟:

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

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

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

首次渲染

  • 組件的首次渲染∶判斷組件的 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 是否存在來(lái)判斷是否要執(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í)行的是組件更新的過(guò)程
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

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

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

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

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

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

18. $nextTick 原理及作用

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

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

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

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

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

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

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

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

所以,在以下情況下,會(huì)用到nextTick:

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

因?yàn)樵赾reated()鉤子函數(shù)中,頁(yè)面的DOM還未渲染,這時(shí)候也沒(méi)辦法操作DOM,所以,此時(shí)如果想要操作DOM,必須將操作的代碼放在 nextTick()的回調(diào)函數(shù)中。

19. Vue 中給 data 中的對(duì)象屬性添加一個(gè)新的屬性時(shí)會(huì)發(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>

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

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

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

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

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


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

// 緩存數(shù)組原型
const arrayProto = Array.prototype;
// 實(shí)現(xiàn) arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要進(jìn)行功能拓展的方法
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會(huì)新增索引,所以要手動(dòng)observer
      case "push":
      case "unshift":
        inserted = args;
        break;
      // splice方法,如果傳入了第三個(gè)參數(shù),也會(huì)有索引加入,也要手動(dòng)observer。
      case "splice":
        inserted = args.slice(2);
        break;
    }
    // 
    if (inserted) ob.observeArray(inserted);// 獲取插入的值,并設(shè)置響應(yīng)式監(jiān)聽(tīng)
    // notify change
    ob.dep.notify();// 通知依賴更新
    // 返回原生數(shù)組方法的執(zhí)行結(jié)果
    return result;
  });
});

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

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

概念:

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

區(qū)別:


22. Vue template 到 render 的過(guò)程

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

vue 在模版編譯版本的碼中會(huì)執(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(抽象語(yǔ)法樹(shù))

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

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

(2)對(duì)靜態(tài)節(jié)點(diǎn)做優(yōu)化

optimize(ast,options)

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

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

(3)生成代碼

const code = generate(ast, options)

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

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

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

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

24. 簡(jiǎn)述 mixin、extends 的覆蓋邏輯

(1)mixin 和 extends

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

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

68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032312f6a7065672f313530303630342f313630393531383438303237322d38636231616630312d613461382d346435342d393162622d3535343661616661633531302e6a7065673f782d

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

  • 規(guī)范化選項(xiàng)(normalizeProps、normalizelnject、normalizeDirectives)
  • 對(duì)未合并的選項(xiàng),進(jìn)行判斷
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ù)一個(gè)通用 Vue 實(shí)例所包含的選項(xiàng)進(jìn)行分類逐一判斷合并,如 props、data、 methods、watch、computed、生命周期等,將合并結(jié)果存儲(chǔ)在新定義的 options 對(duì)象里。
  • 返回合并結(jié)果 options。

25. 描述下Vue自定義指令

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

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

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

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

(2)使用場(chǎng)景

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

(3)使用案例

初級(jí)應(yīng)用:

  • 鼠標(biāo)聚焦
  • 下拉菜單
  • 相對(duì)時(shí)間轉(zhuǎn)換
  • 滾動(dòng)動(dòng)畫(huà)

高級(jí)應(yīng)用:

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

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

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

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

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

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

在初始化 Vue 的每個(gè)組件時(shí),會(huì)對(duì)組件的 data 進(jìn)行初始化,就會(huì)將由普通對(duì)象變成響應(yīng)式對(duì)象,在這個(gè)過(guò)程中便會(huì)進(jìn)行依賴收集的相關(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()實(shí)例化一個(gè) Dep 的實(shí)例,然后在 get 函數(shù)中通過(guò) dep.depend() 進(jìn)行依賴收集。

(1)Dep

Dep是整個(gè)依賴收集的核心,其關(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 是一個(gè) class ,其中有一個(gè)關(guān) 鍵的靜態(tài)屬性 static,它指向了一個(gè)全局唯一 Watcher,保證了同一時(shí)間全局只有一個(gè) watcher 被計(jì)算,另一個(gè)屬性 subs 則是一個(gè) Watcher 的數(shù)組,所以 Dep 實(shí)際上就是對(duì) 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 是一個(gè) class,它定義了一些方法,其中和依賴收集相關(guān)的主要有 get、addDep 等。

(3)過(guò)程

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

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

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

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

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

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

28. 對(duì) React 和 Vue 的理解,它們的異同

相似之處:

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

不同之處 :

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

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

2)虛擬DOM

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

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

3)組件化

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

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

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

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

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

5)高階組件

react可以通過(guò)高階組件(HOC)來(lái)擴(kuò)展,而Vue需要通過(guò)mixins來(lái)擴(kuò)展。

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

6)構(gòu)建工具

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

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

7)跨平臺(tái)

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

29. Vue的優(yōu)點(diǎn)

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

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

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

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

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

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

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

32. vue如何監(jiān)聽(tīng)對(duì)象或者數(shù)組某個(gè)屬性的變化

當(dāng)在項(xiàng)目中直接設(shè)置數(shù)組的某一項(xiàng)的值,或者直接設(shè)置對(duì)象的某個(gè)屬性值,這個(gè)時(shí)候,你會(huì)發(fā)現(xiàn)頁(yè)面并沒(méi)有更新。這是因?yàn)镺bject.defineProperty()限制,監(jiān)聽(tīng)不到變化。

解決方式:

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

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

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

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

33. 什么是 mixin ?

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

34. 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。

  • 解析階段:使用大量的正則表達(dá)式對(duì)template字符串進(jìn)行解析,將標(biāo)簽、指令、屬性等轉(zhuǎn)化為抽象語(yǔ)法樹(shù)AST。
  • 優(yōu)化階段:遍歷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的性能。
  • 生成階段:將最終的AST轉(zhuǎn)化為render函數(shù)字符串。

35. 對(duì)SSR的理解

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

SSR的優(yōu)勢(shì):

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

SSR的缺點(diǎn):

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

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

(1)編碼階段

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

(2)SEO優(yōu)化

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

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

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

(4)用戶體驗(yàn)

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

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

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

優(yōu)點(diǎn):

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

缺點(diǎn):

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

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

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

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

39. vue初始化頁(yè)面閃動(dòng)問(wèn)題

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

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

[v-cloak] {
    display: none;
}

如果沒(méi)有徹底解決問(wèn)題,則在根元素加上 style="display: none;" :style="{display: 'block'}"

40. extend 有什么作用

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

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

41. mixin 和 mixins 區(qū)別

mixin 用于全局混入,會(huì)影響到每個(gè)組件實(shí)例,通常插件都是這樣做初始化的。

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

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

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

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

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

優(yōu)點(diǎn):

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

缺點(diǎn):

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

二、生命周期


1. 說(shuō)一下Vue的生命周期

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

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

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

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

加載渲染過(guò)程:

1.父組件 beforeCreate

2.父組件 created

3.父組件 beforeMount

4.子組件 beforeCreate

5.子組件 created

6.子組件 beforeMount

7.子組件 mounted

8.父組件 mounted

更新過(guò)程:

1. 父組件 beforeUpdate

2.子組件 beforeUpdate

3.子組件 updated

4.父組件 updated

銷毀過(guò)程:

1. 父組件 beforeDestroy

2.子組件 beforeDestroy

3.子組件 destroyed

4.父組件 destoryed

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

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

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

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

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

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

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

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

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

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

三、組件通信


組件通信的方式如下:

(1) props / $emit

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

1. 父組件向子組件傳值
  • ?props?只能是父組件向子組件進(jìn)行傳值,?props?使得父子組件之間形成了一個(gè)單向下行綁定。子組件的數(shù)據(jù)會(huì)隨著父組件不斷更新。
  • ?props ?可以顯示定義一個(gè)或一個(gè)以上的數(shù)據(jù),對(duì)于接收的數(shù)據(jù),可以是各種數(shù)據(jù)類型,同樣也可以傳遞一個(gè)函數(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?綁定一個(gè)自定義事件,當(dāng)這個(gè)事件被執(zhí)行的時(shí)就會(huì)將參數(shù)傳遞給父組件,而父組件通過(guò) v-on監(jiān)聽(tīng)并接收參數(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: ['紅樓夢(mèng)', '西游記', '三國(guó)演義']
    }
  },
  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è)有兩個(gè)兄弟組件 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值存貯在了事件總線中,在其他組件中可以直接訪問(wèn)。事件總線就相當(dāng)于一個(gè)橋梁,不用組件通過(guò)它來(lái)通信。

雖然看起來(lái)比較簡(jiǎn)單,但是這種方法也有不變之處,如果項(xiàng)目過(guò)大,使用這種方式進(jìn)行通信,后期維護(hù)起來(lái)會(huì)很困難。

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

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

project / inject是Vue提供的兩個(gè)鉤子,和 datamethods是同級(jí)的。并且 project的書(shū)寫形式和 data一樣。

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

在父組件中:

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

在子組件中:

inject: ['num']

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

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

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

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

(3)ref / $refs

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

ref: 這個(gè)屬性用在子組件上,它的引用就指向了子組件的實(shí)例??梢酝ㄟ^(guò)實(shí)例來(lái)訪問(wèn)組件的數(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?可以讓組件訪問(wèn)父組件的實(shí)例(訪問(wèn)的是上一級(jí)父組件的屬性和方法)
  • 使用 ?$children?可以讓組件訪問(wèn)子組件的實(shí)例,但是,?$children?并不能保證順序,并且訪問(wèn)的數(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">點(diǎn)擊改變子組件值</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的值。

需要注意:

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

(5)$attrs / $listeners

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

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

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

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

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

A組件(APP.vue):

<template>
    <div id="app">
        //此處監(jiān)聽(tīng)了兩個(gè)事件,可以在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組件時(shí) 使用 v-on 綁定了 ?$listeners? 屬性
  • 在B組件中通過(guò)v-bind 綁定 ?$attrs?屬性,C組件可以直接獲取到A組件中傳遞下來(lái)的props(除了B組件中props聲明的)

(6)總結(jié)

(1)父子組件間通信

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

(2)兄弟組件間通信

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

(3)任意組件之間

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

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

四、路由


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

非懶加載:

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

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

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

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

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

(3)方案三:使用webpack的require.ensure技術(shù),也可以實(shí)現(xiàn)按需加載。 這種情況下,多個(gè)路由指定相同的chunkName,會(huì)合并打包成一個(gè)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模式

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

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

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

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

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

2. history模式

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

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

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

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

雖然history模式丟棄了丑陋的#。但是,它也有自己的缺點(diǎn),就是在刷新頁(yè)面的時(shí)候,如果沒(méi)有相應(yīng)的路由或資源,就會(huì)刷出404來(lái)。

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

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

3. 兩種模式對(duì)比

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

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

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

3. 如何獲取頁(yè)面的hash變化

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

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

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

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

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

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

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

(1)param方式

  • 配置路由格式:?/router/:id?
  • 傳遞的方式:在path后面跟上對(duì)應(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ù)獲取

通過(guò) $route.params.userid 獲取傳遞的值

(2)query方式

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

1)路由定義

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

// 方式2:寫成按鈕以點(diǎn)擊事件形式
<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ù)

通過(guò)$route.query 獲取傳遞的值

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

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

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

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

  1. 全局路由鉤子

vue-router全局有三個(gè)路由鉤子;

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

具體使用∶

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

beforeEnter

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

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

beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave

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

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

注意點(diǎn),beforeRouteEnter組件內(nèi)還訪問(wèn)不到this,因?yàn)樵撌匦l(wèi)執(zhí)行前組件實(shí)例還沒(méi)有被創(chuàng)建,需要傳一個(gè)回調(diào)給 next來(lái)訪問(wèn),例如:

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

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

  1. 完整的路由導(dǎo)航解析流程(不包括其他生命周期)
  • 觸發(fā)進(jìn)入其他路由。
  • 調(diào)用要離開(kāi)路由的組件守衛(wèi)beforeRouteLeave
  • 調(diào)用局前置守衛(wèi)∶ beforeEach
  • 在重用的組件里調(diào)用 beforeRouteUpdate
  • 調(diào)用路由獨(dú)享守衛(wèi) beforeEnter。
  • 解析異步路由組件。
  • 在將要進(jìn)入的路由組件中調(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é)合起來(lái)的,觸發(fā)順序,假設(shè)是從a組件離開(kāi),第一次進(jìn)入b組件∶

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

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

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

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

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

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

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

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

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

10. 對(duì)前端路由的理解

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

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

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

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

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

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

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

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

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

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

五、Vuex


1. Vuex 的原理

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

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


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

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

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

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

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

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

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

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

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

store.commit('increment')

而Action類似于mutation,不同點(diǎn)在于:

  • 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ù)接受一個(gè)與 store 實(shí)例具有相同方法和屬性的 context 對(duì)象,因此你可以調(diào)用 context.commit 提交一個(gè) mutation,或者通過(guò) context.state 和 context.getters 來(lái)獲取 state 和 getters。

所以,兩者的不同點(diǎn)如下:

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

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

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

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

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

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

(3)永久性

刷新頁(yè)面時(shí)vuex存儲(chǔ)的值會(huì)丟失,localstorage不會(huì)。

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

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

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

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

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

(2)共同思想

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

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

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

5. 為什么要用 Vuex 或者 Redux

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

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

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

6. Vuex有哪幾種屬性?

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

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

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

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

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

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

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

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

在Vuex.Store 構(gòu)造器選項(xiàng)中開(kāi)啟,如下

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

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

使用mapGetters輔助函數(shù), 利用對(duì)象展開(kāi)運(yùn)算符將getter混入computed 對(duì)象中

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)測(cè)機(jī)制的改變

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

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

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

(3)模板

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

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

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

(5)其它方面的更改

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

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

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

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

但是這樣做有以下問(wèn)題:

  1. 添加或刪除對(duì)象的屬性時(shí),Vue 檢測(cè)不到。因?yàn)樘砑踊騽h除的對(duì)象沒(méi)有在初始化進(jìn)行響應(yīng)式處理,只能通過(guò) ?$set? 來(lái)調(diào)用 ?Object.defineProperty()?處理。
  2. 無(wú)法監(jiān)控到數(shù)組下標(biāo)和長(zhǎng)度的變化。

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

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

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

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

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

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

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

4. Vue 3.0 中的 Vue Composition API?

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

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

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

如下,是一個(gè)使用了 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)屬性,對(duì)應(yīng)于Vue2中的data函數(shù)
    const count = ref(0)
 
// Vue2中需要在methods option中聲明的函數(shù),現(xiàn)在直接聲明
    function increment() {
      count.value++
    }
 // 對(duì)應(yīng)于Vue2中的mounted聲明周期
    onMounted(() => console.log('component mounted!'))
 
    return {
      count,
      increment
    }
  }
}
</script>

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

5. Composition API與React Hook很像,區(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
  • 必須確??偸窃谀愕腞eact函數(shù)的頂層調(diào)用Hook
  • useEffect、useMemo等函數(shù)必須手動(dòng)確定依賴關(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)了依賴收集,進(jìn)而組件的部分的性能優(yōu)化由Vue內(nèi)部自己完成,而React Hook需要手動(dòng)傳入依賴,而且必須必須保證依賴的順序,讓useEffect、useMemo等函數(shù)正確的捕獲依賴變量,否則會(huì)由于依賴不正確使得組件性能下降。

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

七、虛擬DOM


1. 對(duì)虛擬DOM的理解?

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

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

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

2. 虛擬DOM的解析過(guò)程

虛擬DOM的解析過(guò)程:

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

3. 為什么要用虛擬DOM

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

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

下面對(duì)比一下修改DOM時(shí)真實(shí)DOM操作和Virtual DOM的過(guò)程,來(lái)看一下它們重排重繪的性能消耗∶

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

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

(2)跨平臺(tái)

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

4. 虛擬DOM真的比真實(shí)DOM性能好嗎

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

5. 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(n3)降低值O(n),也就是說(shuō),只有當(dāng)新舊children都為多個(gè)子節(jié)點(diǎn)時(shí)才需要用核心的Diff算法進(jìn)行同層級(jí)比較。

6. Vue中key的作用

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

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

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

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

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

使用index 作為 key和沒(méi)寫基本上沒(méi)區(qū)別,因?yàn)椴还軘?shù)組的順序怎么顛倒,index 都是 0, 1, 2...這樣排列,導(dǎo)致 Vue 會(huì)復(fù)用錯(cuò)誤的舊子節(jié)點(diǎn),做很多額外的工作。


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)