1. 簡單說下vue
- vue是漸變式框架,根據自己的需求添加功能
- vue資料驅動采用mvvm模式,m是資料層,v是視圖層,vm是調度者
- SPA單頁面應用,只有一個頁面,加載速率快
- 組件化,復用性強
那么,它有什么缺點?
- vue2底層基于Object.defineProperty實作回應式,這個api本身不支持IE8及以下瀏覽器
- csr的先天不足,首屏性能問題(白屏)
- 由于百度等搜索引擎爬蟲無法爬取js中的內容,故spa先天就對seo優化心有余力不足(谷歌的puppeteer就挺牛逼的,實作預渲染底層也是用到了這個工具)
2. vue和react的區別
首先你得說說相同點,兩個都是MVVM框架,資料驅動視圖,無爭議,如果說不同,那可能分為以下這么幾點:
-
vue是完整一套由官方維護的框架,核心庫主要有由尤雨溪大神獨自維護,而react是不要臉的書維護(很多庫由社區維護),曾經一段時間很多人質疑vue的后續維護性,似乎這并不是問題,
-
vue上手簡單,進階式框架,白話說你可以學一點,就可以在你專案中去用一點,你不一定需要一次性學習整個vue才能去使用它,而react,恐怕如果你這樣會面對專案束手無策,
-
語法上vue并不限制你必須es6+完全js形式撰寫頁面,可以視圖和js邏輯盡可能分離,減少很多人看不慣react-jsx的惡心嵌套,畢竟都是作為前端開發者,還是更習慣于html干凈,
-
很多人說react適合大型專案,適合什么什么,vue輕量級,適合移動端中小型專案,其實我想說,說這話的人是心里根本沒點逼數,vue完全可以應對復雜的大型應用,甚至于說如果你react學的不是很好,寫出來的東西或根本不如vue寫的,畢竟vue跟著官方檔案擼就行,自有人幫你規范,而react比較懶散自由,可以自由發揮
-
vue在國內人氣明顯勝過react,這很大程度上得益于它的很多語法包括編程思維更符合國人思想
1. 什么是mvvm
MVVM的核心是資料驅動即ViewModel,ViewModel是View和Model的關系映射,
MVVM本質就是基于操作資料來操作視圖進而操作DOM,借助于MVVM無需直接操作DOM,開發者只需撰寫ViewModel中有業務,使得View完全實作自動化,
2. 什么是 SPA 單頁面,它的優缺點分別是什么
SPA( single-page application )即一個web專案就只有一個頁面(即一個HTML檔案,HTML 內容的變換是利用路由機制實作的,
僅在 Web 頁面初始化時加載相應的 HTML、JavaScript 和 CSS,一旦頁面加載完成,SPA 不會因為用戶的操作而進行頁面的重新加載或跳轉;取而代之的是利用路由機制實作 HTML 內容的變換,UI 與用戶的互動,避免頁面的重新加載,
優點:
用戶體驗好、快,內容的改變不需要重新加載整個頁面,避免了不必要的跳轉和重復渲染;- 基于上面一點,SPA 相對對
服務器壓力小; - 前后端
職責分離,架構清晰,前端進行互動邏輯,后端負責資料處理;
缺點:
初次加載耗時多:為實作單頁 Web 應用功能及顯示效果,需要在加載頁面的時候將 JavaScript、CSS 統一加載,部分頁面按需加載;
前進后退路由管理:由于單頁應用在一個頁面中顯示所有的內容,所以不能使用瀏覽器的前進后退功能,所有的頁面切換需要自己建立堆疊管理;
SEO 難度較大:由于所有的內容都在一個頁面中動態替換顯示,所以在 SEO 上其有著天然的弱勢,
3. 生命周期
3-1 基本概念
什么是vue生命周期?
Vue 實體從創建到銷毀的程序,就是生命周期,
注意:瀏覽器有8個鉤子,但是node中做服務端渲染的時候只有beforeCreate和created
-
beforeCreate是
new Vue()之后觸發的第一個鉤子,在當前階段data、methods、computed以及watch上的資料和方法都不能被訪問, 可以做頁面攔截,當進一個路由的時候我們可以判斷是否有權限進去,是否安全進去,攜帶引數是否完整,引數是否安全,使用這個鉤子好函式的時候就避免了讓頁面去判斷,省掉了創建一個組建Vue實體, -
created 發生在
實體創建完成后,當前階段已經完成了資料觀測,也就是可以使用資料,更改資料,在這里更改資料不會觸發updated函式,可以做一些初始資料的獲取,在當前階段無法與Dom進行互動(因為Dom還沒有創建),如果非要想,可以通過vm.$nextTick來訪問Dom, -
beforeMount發生在
掛載之前,在這之前template模板已匯入渲染函式編譯,而當前階段虛擬Dom已經創建完成,即將開始渲染,在此時也可以對資料進行更改,不會觸發updated, -
mounted發生在
掛載完成后,在當前階段,真實的Dom掛載完畢,資料完成雙向系結,可以訪問到Dom節點,使用$refs屬性對Dom進行操作, -
beforeUpdate發生在
更新之前,也就是回應式資料發生更新,虛擬dom重新渲染之前被觸發,你可以在當前階段進行更改資料,不會造成重渲染, -
updated發生在
更新完成之后,當前階段組件Dom已完成更新,要注意的是避免在此期間更改資料,因為這可能會導致無限回圈的更新, -
beforeDestroy發生在
實體銷毀之前,在當前階段實體完全可以被使用,我們可以在這時進行善后收尾作業,比如清除計時器,銷毀父組件對子組件的重復監聽,beforeDestroy(){Bus.$off("saveTheme")} -
destroyed發生在實體銷毀之后,這個時候只剩下了dom空殼,組件已被拆解,資料系結被卸除,監聽被移出,子實體也統統被銷毀,
3-2 生命周期呼叫順序
- 組件的呼叫順序都是先父后子
- 渲染完成的順序是先子后父
- 組件的銷毀操作是先父后子
- 銷毀完成的順序是先子后父
加載渲染程序 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
子組件更新程序 父beforeUpdate->子beforeUpdate->子updated->父updated
父組件更新程序 父 beforeUpdate -> 父 updated
銷毀程序 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
3-3 vue生命周期的作用是什么
它的生命周期中有多個事件鉤子,讓我們控制Vue實體程序更加清晰,
3-4 第一次頁面加載會觸發哪幾個鉤子
第一次頁面加載時會觸發 beforeCreate, created, beforeMount, mounted 這幾個鉤子
3-5 每個周期具體適合哪些場景
- beforecreate : 可以在這加個loading事件,在加載實體時觸發
- created : 初始化完成時的事件寫在這里,如在這結束loading事件,異步請求也適宜在這里呼叫
- mounted : 掛載元素,獲取到DOM節點
- updated : 如果對資料統一處理,在這里寫上相應函式
- beforeDestroy : 可以清除定時器
- nextTick : 更新資料后立即操作dom
4.v-show 與 v-if 的區別
v-if
- 是真正的條件渲染,因為它會確保在切換程序中條件塊內的
事件監聽器和子組件適當地被銷毀和重建; - 也是
惰性的:如果在初始渲染時條件為假,則什么也不做——直到條件第一次變為真時,才會開始渲染條件塊,
v-show
不管初始條件是什么,元素總是會被渲染,并且只是簡單地基于 CSS 的 “display” 屬性進行切換,
所以:
- v-if 適用于在運行時
很少改變條件,不需要頻繁切換條件的場景; - v-show 則適用于需要
非常頻繁切換條件的場景,
5. Vue 的單向資料流
背景:
所有的 prop 都使得其父子 prop 之間形成了一個單向下行系結:父級 prop 的更新會向下流動到子組件中,但是反過來則不行,這樣會防止從子組件意外改變父級組件的狀態,從而導致你的應用的資料流向變的混亂,
每次父級組件發生更新時,子組件中所有的 prop 都將會重繪為最新的值,這意味著你不應該在一個子組件內部改變 prop,如果你這樣做了,Vue 會在瀏覽器的控制臺中發出警告,子組件想修改時,只能通過 $emit 派發一個自定義事件,父組件接收到后,由父組件修改,
有兩種常見的試圖改變一個 prop 的情形 :
- 這個 prop 用來傳遞一個初始值;
- 這個子組件接下來希望將其作為一個本地的 prop 資料來使用,
在第2情況下,最好定義一個本地的 data屬性并將這個 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter//定義本地的data屬性接收prop初始值
}
}
這個 prop 以一種原始的值傳入且需要進行轉換,
在這種情況下,最好使用這個 prop 的值來定義一個計算屬性
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
6.異步請求適合在哪個生命周期呼叫?
官方實體的異步請求是在mounted生命周期中呼叫的,而實際上也可以在created生命周期中呼叫,
本人推薦在 created 鉤子函式中呼叫異步請求,有以下優點:
- 能
更快獲取到服務端資料,減少頁面 loading 時間; ssr不支持 beforeMount 、mounted 鉤子函式,所以放在 created 中有助于一致性;
7.Vue2.x組件通信有哪些方式?
7-1 父子組件通信
- 父->子props;子(emit)->父(on)
- 獲取父子組件實體 $parent / c h i l d r e n 如 : 直 接 在 子 組 件 的 m e t h o d s 方 法 里 面 寫 : t h i s . children 如:直接在子組件的methods方法里面寫:this. children如:直接在子組件的methods方法里面寫:this.parent.show()//show為父組件中定義的方法
- Ref (如果在普通的 DOM 元素上使用,參考指向的就是 DOM 元素;如果用在子組件上,參考就指向組件實體),如在引入的子組件的標簽上掛載一個: ref=“comA”,然后在方法中或者子組件的資料,this.$refs.comA.titles
- Provide、inject 官方不推薦使用,但是寫組件庫時很常用,祖先組件中通過provider來提供變數,然后在子孫組件中通過inject來注入變數
7-2 兄弟組件通信
- Event Bus 實作跨組件通信: Vue.prototype.$bus = new Vue
- Vuex
7-3 跨級組件通信
- Vuex
- attrs,listeners
- Provide、inject
7-4 使用
1. 父子props,on
// 子組件
<template>
<header>
<h1 @click="changeTitle">{{title}}</h1>//系結一個點擊事件
</header>
</template>
<script>
export default {
data() {
return {
title:"Vue.js Demo"
}
},
methods:{
changeTitle() {
this.$emit("titleChanged","子向父組件傳值");//自定義事件 傳遞值“子向父組件傳值”
}
}
}
</script>
// 父組件
<template>
<div id="app">
<Header @titleChanged="updateTitle" ></Header>//與子組件titleChanged自定義事件保持一致
<h2>{{title}}</h2>
</div>
</template>
<script>
import Header from "./Header"
export default {
data(){
return{
title:"傳遞的是一個值"
}
},
methods:{
updateTitle(e){ //宣告這個函式
this.title = e;
}
},
components:{
Header
}
}
</script>
2. parent / $children與 ref
// A 子組件
export default {
data () {
return {
title: 'a組件'
}
},
methods: {
sayHello () {
alert('Hello');
}
}
}
// 父組件
<template>
<A ref="comA"></A>
</template>
<script>
export default {
mounted () {
const comA = this.$refs.comA;
console.log(comA.title); // a組件
comA.sayHello(); // 彈窗
}
}
</script>
3.attrs,listeners
attrs: 包含了父作用域中不被 prop 所識別 (且獲取) 的特性系結 ( class 和 style 除外 ),當一個組件沒有宣告任何 prop 時,這里會包含所有父作用域的系結 ( class 和 style 除外 ),并且可以通過 v-bind="$attrs" 傳入內部組件,通常配合 inheritAttrs 選項一起使用,
listeners: :包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監聽器,它可以通過 v-on="$listeners" 傳入內部組件
// index.vue
<template>
<div>
<h2>浪里行舟</h2>
<child-com1 :foo="foo" :boo="boo" :coo="coo" :doo="doo" title="前端工匠"></child-com1>
</div>
</template>
<script>
const childCom1 = () => import("./childCom1.vue");
export default {
components: { childCom1 },
data() {
return {
foo: "Javascript",
boo: "Html",
coo: "CSS",
doo: "Vue"
};
}
};
</script>
// childCom1.vue
<template class="border">
<div>
<p>foo: {{ foo }}</p>
<p>childCom1的$attrs: {{ $attrs }}</p>
<child-com2 v-bind="$attrs"></child-com2>
</div>
</template>
<script>
const childCom2 = () => import("./childCom2.vue");
export default {
components: {
childCom2
},
inheritAttrs: false, // 可以關閉自動掛載到組件根元素上的沒有在props宣告的屬性
props: {
foo: String // foo作為props屬性系結
},
created() {
console.log(this.$attrs); // 父組件中的屬性,且不在當前組件props中的屬性,{ "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" }
}
};
</script>
// childCom2.vue
<template>
<div class="border">
<p>boo: {{ boo }}</p>
<p>childCom2: {{ $attrs }}</p>
<child-com3 v-bind="$attrs"></child-com3>
</div>
</template>
<script>
const childCom3 = () => import("./childCom3.vue");
export default {
components: {
childCom3
},
inheritAttrs: false,
props: {
boo: String
},
created() {
console.log(this.$attrs); // / 父組件中的屬性,且不在當前組件props中的屬性,{"coo": "CSS", "doo": "Vue", "title": "前端工匠" }
}
};
</script>
// childCom3.vue
<template>
<div class="border">
<p>childCom3: {{ $attrs }}</p>
</div>
</template>
<script>
export default {
props: {
coo: String,
title: String
}
};
</script>
4. Provide、inject的使用:
父組件
<template>
<div id="app">
</div>
</template>
<script>
export default {
data () {
return {
datas: [
{
id: 1,
label: '產品一'
}
]
}
},
provide {
return {
datas: this.datas
}
}
}
</script>
子組件
<template>
<div>
<ul>
<li v-for="(item, index) in datas" :key="index">
{{ item.label }}
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['datas']
}
</script>
8. 什么是SSR
SSR也就是
服務端渲染,也就是將Vue在客戶端把標簽渲染成HTML的作業放在服務端完成,然后再把html直接回傳給客戶端,
服務端渲染 SSR 的優缺點如下:
(1)服務端渲染的優點:
-
更好的 SEO: 因為 SPA 頁面的內容是通過 Ajax 獲取,而搜索引擎爬取工具并不會等待 Ajax 異步完成后再抓取頁面內容,所以在
SPA中是抓取不到頁面通過Ajax獲取到的內容;而SSR是直接由服務端回傳已經渲染好的頁面(資料已經包含在頁面中),所以搜索引擎爬取工具可以抓取渲染好的頁面; -
更快的內容到達時間(首屏加載更快):
SPA會等待所有 Vue 編譯后的js檔案都下載完成后,才開始進行頁面的渲染,檔案下載等需要一定的時間等,所以首屏渲染需要一定的時間;SSR 直接由服務端渲染好頁面直接回傳顯示,無需等待下載 js 檔案及再去渲染等,所以 SSR 有更快的內容到達時間;
(2) 服務端渲染的缺點:
-
更多的開發條件限制: 例如服務端渲染只支持
beforCreate和created兩個鉤子函式,這會導致一些外部擴展庫需要特殊處理,才能在服務端渲染應用程式中運行;并且與可以部署在任何靜態檔案服務器上的完全靜態單頁面應用程式 SPA 不同,服務端渲染應用程式,需要處于 Node.js server 運行環境; -
更多的服務器負載: 在 Node.js 中渲染完整的應用程式,顯然會比僅僅提供靜態檔案的 server 更加大量占用CPU 資源 (CPU-intensive - CPU 密集),因此如果你預料在高流量環境 ( high traffic ) 下使用,請準備相應的服務器負載,并明智地采用快取策略,
9.Vue路由
9-1 vue-router 路由模式有幾種?
vue-router 有 3 種路由模式:hash、history、abstract,對應的原始碼如下所示:
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
路由模式的說明如下:
-
hash: 使用 URL hash 值來作路由,支持
所有瀏覽器,包括不支持 HTML5 History Api 的瀏覽器; -
history : 依賴 HTML5 History API 和服務器配置,具體可以查看 HTML5 History 模式;
-
abstract : 支持所有
JavaScript運行環境,如 Node.js 服務器端,如果發現沒有瀏覽器的 API,路由會自動強制進入這個模式.
9-2 hash路由和history路由實作原理
(1)hash 模式的實作原理
早期的前端路由的實作就是基于 location.hash 來實作的,其實作原理很簡單,location.hash 的值就是 URL 中 # 后面的內容,
比如下面這個網站,它的 location.hash 的值為 '#search':
https://www.word.com#search
hash 路由模式的實作主要是基于下面幾個特性:
- URL 中 hash 值只是
客戶端的一種狀態,也就是說當向服務器端發出請求時,hash 部分不會被發送; - hash 值的
改變,都會在瀏覽器的訪問歷史中增加一個記錄,因此我們能通過瀏覽器的回退、前進按鈕控制hash 的切換; - 可以通過 a 標簽,并設定 href 屬性,當用戶點擊這個標簽后,URL 的 hash 值會發生改變;或者使用JavaScript 來對 loaction.hash 進行賦值,改變 URL 的 hash 值;
- 我們可以使用
hashchange事件來監聽 hash 值的變化,從而對頁面進行跳轉(渲染),
(2)history 模式的實作原理
HTML5 提供了 History API 來實作 URL 的變化,其中做最主要的 API 有以下兩個:
- history.pushState() //新曾歷史記錄
- history.repalceState(), //替換歷史記錄
這兩個 API 可以在不進行重繪的情況下,操作瀏覽器的歷史紀錄,唯一不同的是,前者是新增一個歷史記錄,后者是直接替換當前的歷史記錄,如下所示:
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
history 路由模式的實作主要基于存在下面幾個特性:
- pushState 和 repalceState 兩個 API 來操作實作 URL 的變化;
- 我們可以使用
popstate事件來監聽 url 的變化,從而對頁面進行跳轉(渲染); - history.pushState() 或 history.replaceState() 不會觸發 popstate 事件,這時我們需要手動觸發頁面跳轉(渲染),
10.Vue 中的 key 有什么作用?
key 是為 Vue 中 vnode 的唯一標記,通過這個 key,我們的 diff 操作可以更準確、更快速,
Vue 的 diff 程序可以概括為:
oldCh 和 newCh 各有兩個頭尾的變數 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它們會新節點和舊節點會進行兩兩對比,即一共有4種比較方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 種比較都沒匹配,如果設定了key,就會用 key 再進行比較,在比較的程序中,遍歷會往中間靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一個已經遍歷完了,就會結束比較,
所以 Vue 中 key 的作用是:key 是為 Vue 中 vnode 的唯一標記,通過這個 key,我們的 diff 操作可以更準確、更快速
-
更準確因為帶 key 就不是就地復用了,在 sameNode 函式 a.key === b.key對比中可以避免就地復用的情況,所以會更加準確,
-
更快速:利用 key 的唯一性生成 map 物件來獲取對應節點,比遍歷方式更快,原始碼如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
參考1:Vue2.0 v-for 中 :key 到底有什么用?
11.虛擬 DOM 實作原理
虛擬 DOM 的實作原理主要包括以下 3 部分:
- 用 JavaScript 物件模擬真實 DOM 樹,對真實 DOM 進行抽象;
- diff 演算法 — 比較兩棵虛擬 DOM 樹的差異;
- pach 演算法 — 將兩個虛擬 DOM 物件的差異應用到真正的 DOM 樹,
詳情點擊這里
13.虛擬 DOM 的優缺點
優點:
-
保證性能下限: 框架的虛擬 DOM 需要適配任何上層 API 可能產生的操作,它的一些
DOM 操作的實作必須是普適的,所以它的性能并不是最優的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虛擬 DOM 至少可以保證在你不需要手動優化的情況下,依然可以提供還不錯的性能,即保證性能的下限; -
無需手動操作 DOM: 我們不再需要手動去操作 DOM,只需要寫好 View-Model 的代碼邏輯,框架會根據虛擬 DOM 和 資料雙向系結,幫我們以可預期的方式更新視圖,極大提高我們的開發效率;
-
跨平臺: 虛擬 DOM 本質上是 JavaScript 物件,而 DOM 與平臺強相關,相比之下虛擬 DOM 可以進行更方便地跨平臺操作,例如服務器渲染、weex 開發等等,
缺點:
- 無法進行極致優化: 雖然虛擬 DOM + 合理的優化,足以應對絕大部分應用的性能需求,但在一些性能要求極高的應用中虛擬 DOM 無法進行針對性的極致優化,
14.Proxy 與 Object.defineProperty 優劣對比
Proxy 的優勢如下:
- Proxy 可以直接監聽物件而非屬性;
- Proxy 可以直接監聽陣列的變化;
- Proxy 有多達 13 種攔截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具備的;
- Proxy 回傳的是一個新物件,我們可以只操作新的物件達到目的,而 Object.defineProperty 只能遍歷物件屬性直接修改;
- Proxy 作為新標準將受到瀏覽器廠商重點持續的性能優化,也就是傳說中的新標準的性能紅利;
Object.defineProperty 的優勢如下:
- 兼容性好,支持 IE9,而 Proxy 的存在瀏覽器兼容性問題,而且無法用 polyfill 磨平
14-1 那么,Proxy 可以實作什么功能?
Proxy 是 ES6 中新增的功能,它可以用來自定義物件中的操作,
let p = new Proxy(target, handler)
- target 代表需要
添加代理的物件 - handler 用來自定義
物件中的操作,比如可以用來自定義 set 或者 get 函式,
下面來通過 Proxy 來實作一個資料回應式:
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
setBind(value, property)
return Reflect.set(target, property, value)
}
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`監聽到屬性${property}改變為${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 監聽到屬性a改變為2
p.a // 'a' = 2
在上述代碼中,通過自定義 set 和 get 函式的方式,在原本的邏輯中插入了我們的函式邏輯,實作了在對物件任何屬性進行讀寫時發出通知,
當然這是簡單版的回應式實作,如果需要實作一個 Vue 中的回應式,需要在 get 中收集依賴,在 set 派發更新,之所以 Vue3.0 要使用 Proxy 替換原本的 API 原因在于 Proxy 無需一層層遞回為每個屬性添加代理,一次即可完成以上操作,性能上更好,并且原本的實作有一些資料更新不能監聽到,但是 Proxy 可以完美監聽到任何方式的資料改變,唯一缺陷就是瀏覽器的兼容性不好,
15.Vue 框架怎么實作物件和陣列的監聽?
Vue 框架是通過遍歷陣列 和遞回遍歷物件,從而達到利用 Object.defineProperty() 也能對物件和陣列(部分方法的操作)進行監聽,
vue2:
陣列就是使用 object.defineProperty 重新定義陣列的每一項,能引起陣列變化的方法為 pop 、 push 、 shift 、 unshift 、 splice 、 sort 、 reverse 這七種,只要這些方法執行改了陣列內容,就更新內容
-
是用來
函式劫持的方式,重寫了陣列方法,具體就是更改了陣列的原型,更改成自己的,用戶調陣列的一些方法的時候,走的就是自己的方法,然后通知視圖去更新(本質就是在原有的方法上又呼叫了更新資料的方法), -
陣列項可能是
物件,那么就對陣列的每一項進行觀測
vue3:
改用 proxy ,可直接監聽物件陣列的變化,
參考
16.Vue 是如何實作資料雙向系結的
Vue 資料雙向系結主要是指:資料變化更新視圖,視圖變化更新資料
輸入框內容變化時,Data 中的資料同步變化,即 View => Data 的變化, Data 中的資料變化時,文本節點的內容同步變化,即 Data => View 的變化,
其中,View 變化更新 Data ,可以通過事件監聽的方式來實作,所以 Vue 的資料雙向系結的作業主要是如何根據 Data 變化更新 View,
Vue 主要通過以下 4 個步驟來實作資料雙向系結的:
-
實作一個監聽器 Observer 對資料物件進行遍歷,包括子屬性物件的屬性,利用
Object.defineProperty()對屬性都加上setter和getter,這樣的話,給這個物件的某個值賦值,就會觸發 setter,那么就能監聽到了資料變化, -
實作一個決議器 Compile 決議 Vue 模板指令,將模板中的
變數都替換成資料,然后初始化渲染頁面視圖,并將每個指令對應的節點系結更新函式,添加監聽資料的訂閱者,一旦資料有變動,收到通知,呼叫更新函式進行資料更新, -
實作一個訂閱者 Watcher 訂閱者是 Observer 和 Compile 之間通信的橋梁 ,主要的任務是訂閱 Observer 中的屬性值變化的訊息,當收到屬性值變化的訊息時,觸發決議器 Compile 中對應的更新函式,
-
實作一個訂閱器 Dep 訂閱器采用 發布-訂閱 設計模式,用來收集訂閱者 Watcher,對監聽器 Observer 和 訂閱者 Watcher 進行統一管理,
17.v-model 的原理
v-model 指令在表單 input、textarea、select 等元素上創建雙向資料系結,v-model 本質上是語法糖,會在內部為不同的輸入元素使用不同的屬性并拋出不同的事件:
- text 和 textarea 元素使用
value屬性和input事件; - checkbox 和 radio 使用
checked屬性和change事件; - select 欄位將 value 作為
prop并將change作為事件,
以 input 表單元素為例:
<input v-model='something'>
相當于
<input :value="something" @input="something = $event.target.value">
18.組件中 data 為什么是一個函式?
為什么組件中的 data 必須是一個函式,然后 return 一個物件,而 new Vue 實體里,data 可以直接是一個物件?
// data
data() {
return {
message: "子組件",
childName:this.name
}
}
// new Vue
new Vue({
el: '#app',
router,
template: '<App/>',
components: {App}
})
一個組件被復用多次的話,也就會創建多個實體,本質上,這些實體用的都是同一個建構式,
如果data是物件的話,物件屬于參考型別,會影響到所有的實體,所以為了保證組件不同的實體之間data不沖突,data必須是一個函式,
而 new Vue 的實體,是不會被復用的,因此不存在參考物件的問題,
19.談談你對 keep-alive
keep-alive 是 Vue 內置的一個組件,可以使被包含的組件保留狀態,避免重新渲染 ,其有以下特性:
-
一般結合路由和動態組件一起使用,用于快取組件;
-
提供
include和exclude屬性,兩者都支持字串或正則運算式, include 表示只有名稱匹配的組件會被快取,exclude 表示任何名稱匹配的組件都不會被快取,其中 exclude 的優先級比 include高; -
對應兩個鉤子函式
activated和deactivated,當組件被激活時,觸發鉤子函式 activated,當組件被移除時,觸發鉤子函式 deactivated,
keep-alive的生命周期
- activated: 頁面第一次進入的時候,鉤子觸發的順序是created->mounted->activated
- deactivated: 頁面退出的時候會觸發deactivated,當再次前進或者后退的時候只觸發activated
20.父組件可以監聽到子組件的生命周期嗎?
比如有父組件 Parent 和子組件 Child,如果父組件監聽到子組件掛載 mounted 就做一些邏輯處理,可以通過以下寫法實作:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
以上需要手動通過 $emit 觸發父組件的事件,更簡單的方式可以在父組件參考子組件時通過 @hook 來監聽即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父組件監聽到 mounted 鉤子函式 ...');
},
// Child.vue
mounted(){
console.log('子組件觸發 mounted 鉤子函式 ...');
},
// 以上輸出順序為:
// 子組件觸發 mounted 鉤子函式 …
// 父組件監聽到 mounted 鉤子函式 …
當然 @hook 方法不僅僅是可以監聽 mounted,其它的生命周期事件,例如:created,updated 等都可以監聽,
21.直接給一個陣列項賦值,Vue 能檢測到變化嗎?
由于 JavaScript 的限制,Vue 不能檢測到以下陣列的變動:
- 當你利用索引直接設定一個陣列項時,例如:
vm.items[indexOfItem] = newValue - 當你修改陣列的長度時,例如:
vm.items.length = newLength
為了解決第一個問題,Vue 提供了以下操作方法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set(Vue.set的一個別名)
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
為了解決第二個問題,Vue 提供了以下操作方法:
// Array.prototype.splice
vm.items.splice(newLength)
22.vue2.x中如何監測陣列變化
使用了函式劫持的方式,重寫了陣列的方法,Vue將data中的陣列進行了原型鏈重寫,指向了自己定義的陣列原型方法,這樣當呼叫陣列api時,可以通知依賴更新,如果陣列中包含著參考型別,會對陣列中的參考型別再次遞回遍歷進行監控,這樣就實作了監測陣列變化,
22.Vue2.x和Vue3.x渲染器的diff演算法分別說一下
簡單來說,diff演算法有以下程序
同級比較, 再比較子節點,先判斷一方有子節點一方沒有子節點的情況(如果新的children沒有子節點,將舊的子節點移除)
比較都有子節點的情況(核心diff)遞回比較子節點
正常Diff兩個樹的時間復雜度是O(n3),但實際情況下我們很少會進行跨層級的移動DOM,所以Vue將Diff進行了優化,從O(n3) -> O(n),只有當新舊children都為多個子節點時才需要用核心的Diff演算法進行同層級比較,
Vue2的核心Diff演算法采用了雙端比較的演算法,同時從新舊children的兩端開始進行比較,借助key值找到可復用的節點,再進行相關操作,相比React的Diff演算法,同樣情況下可以減少移動節點次數,減少不必要的性能損耗,更加的優雅,
Vue3.x借鑒了 ivi演算法和 inferno演算法 在創建VNode時就確定其型別,以及在mount/patch的程序中采用位運算來判斷一個VNode的型別,在這個基礎之上再配合核心的Diff演算法,使得性能上較Vue2.x有了提升, 該演算法中還運用了動態規劃的思想求解最長遞回子序列,
23.Vue模版編譯原理
簡單說,Vue的編譯程序就是將template轉化為render函式的程序,會經歷以下階段:
- 生成AST
- 樹優化
- codegen
首先決議模版,生成AST語法樹(一種用JavaScript物件的形式來描述整個模板),
使用大量的正則運算式對模板進行決議,遇到標簽、文本的時候都會執行對應的鉤子進行相關處理,
Vue的資料是回應式的,但其實模板中并不是所有的資料都是回應式的,有一些資料首次渲染后就不會再變化,對應的DOM也不會變化,那么優化程序就是深度遍歷AST樹,按照相關條件對樹節點進行標記,這些被標記的節點(靜態節點)我們就可以跳過對它們的比對,對運行時的模板起到很大的優化作用,
編譯的最后一步是將優化后的AST樹轉換為可執行的代碼,
24.Computed和Watch
computed:
- computed是
計算屬性,也就是計算值,它更多用于計算值的場景 - computed具有
快取性,computed的值在getter執行后是會快取的,只有在它依賴的屬性值改變之后,下一次獲取computed的值時才會重新呼叫對應的getter來計算 - computed適用于計算比較消耗性能的計算場景
watch:
- 更多的是「觀察」的作用,類似于某些資料的監聽回呼,用于觀察
props$emit或者本組件的值,當資料變化時來執行回呼進行后續操作 - 無快取性,頁面重新渲染時值
不變化也會執行
小結:
- 當我們要進行數值計算,而且依賴于其他資料,那么把這個資料設計為computed
- 如果你需要在某個資料變化時做一些事情,使用watch來觀察這個資料變化
25.nextTick
在下次 DOM 更新回圈結束之后執行延遲回呼,在這里面的代碼會等到dom更新以后再執行,
<template>
<section>
<div ref="hello">
<h1>Hello World ~</h1>
</div>
<el-button type="danger" @click="get">點擊</el-button>
</section>
</template>
<script>
export default {
methods: {
get() {
}
},
mounted() {
console.log(333);
console.log(this.$refs['hello']);
this.$nextTick(() => {
console.log(444);
console.log(this.$refs['hello']);
});
},
created() {
console.log(111);
console.log(this.$refs['hello']);
this.$nextTick(() => {
console.log(222);
console.log(this.$refs['hello']);
});
}
}
</script>

詳細點擊這路
26.vue回應式原理
26-1 Vue2.x回應式資料原理
Vue在初始化資料時,會使用Object.defineProperty重新定義data中的所有屬性,當頁面使用對應屬性時,首先會進行依賴收集(收集當前組件的watcher),如果屬性發生變化會通知相關依賴進行更新操作(發布訂閱)
具體的程序:
- 首先Vue使用
initData初始化用戶傳入的引數 - 然后使用
new Observer對資料進行觀測 - 如果資料是一個
物件型別就會呼叫this.walk(value)對物件進行處理,內部使用defineeReactive回圈物件屬性定義回應式變化,核心就是使用Object.defineProperty重新定義資料,
26-2 Vue3.x回應式資料原理
Vue3.x改用Proxy替代Object.defineProperty,因為Proxy可以直接監聽物件和陣列的變化,并且有多達13種攔截方法,并且作為新標準將受到瀏覽器廠商重點持續的性能優化,
Proxy只會代理物件的第一層,那么Vue3又是怎樣處理這個問題的呢?
判斷當前Reflect.get的回傳值是否為Object,如果是則再通過reactive方法做代理, 這樣就實作了深度觀測,
監測陣列的時候可能觸發多次get/set,那么如何防止觸發多次呢?
我們可以判斷key是否為當前被代理物件target自身屬性,也可以判斷舊值與新值是否相等,只有滿足以上兩個條件之一時,才有可能執行trigger,
27.在使用計算屬性的時,函式名和data資料源中的資料可以同名嗎?
不能同名 因為不管是計算屬性還是data還是props 都會被掛載在vm實體上,因此 這三個都不能同名
28.怎么解決vue打包后靜態資源圖片失效的問題
找到config/index.js 組態檔,找build打包物件里的assetsPublicPath屬性 默認值為/,更改為./就好了
29. 怎么解決vue動態設定img的src不生效的問題?
因為動態添加src被當做靜態資源處理了,沒有進行編譯,所以要加上require,
<img :src="require('../../../assets/images/xxx.png')" />
30. 使用vue渲染大量資料時應該怎么優化?說下你的思路!
Object.freeze
適合一些 big data的業務場景,尤其是做管理后臺的時候,經常會有一些超大資料量的 table,或者一個含有 n 多資料的圖表,這種資料量很大的東西使用起來最明顯的感受就是卡,但其實很多時候其實這些資料其實并不需要回應式變化,這時候你就可以使用 Object.freeze 方法了,它可以凍結一個物件(注意它不并是 vue 特有的 api),
當你把一個普通的 JavaScript 物件傳給 Vue 實體的 data 選項,Vue 將遍歷此物件所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉為 getter/setter,它們讓 Vue 能進行追蹤依賴,在屬性被訪問和修改時通知變化,
使用了 Object.freeze 之后,不僅可以減少 observer 的開銷,還能減少不少記憶體開銷,
使用方式:
this.item = Object.freeze(Object.assign({}, this.item))
30. vue 自定義指令
先了解一下,在 vue 中,有很多內置的指令.
比如:
- v-bind: 屬性系結,把資料系結在HTML元素的屬性上.
- v-html & v-text 把資料系結在HTML元素的屬性上,作用同 innerHTML & innerText
- v-on: 系結HTML元素事件
所以,關于指令,我們可以總結下面幾點:
- 指令是寫在
HTML 屬性地方的,<input v-model='name' type='text' /> - 指令都是以
v-開頭的. - 指令運算式的右邊一般也可以跟值 v-if = false
Vue自定義指令案例1
例如:我們需要一個指令,寫在某個HTML表單元素上,然后讓它在被加載到DOM中時,自動獲取焦點.
// 和自定義過濾器一樣,我們這里定義的是全域指令
Vue.directive('focus',{
inserted(el) {
el.focus()
}
})
<div id='app'>
<input type="text">
<input type="text" v-focus placeholder="我有v-focus,所以,我獲取了焦點">
</div>

先總結幾個點:
- 使用
Vue.directive()來新建一個全域指令,(指令使用在HTML元素屬性上的) - Vue.directive(‘focus’)
第一個引數focus是指令名,指令名在宣告的時候,不需要加 v- - 在使用指令的HTML元素上,
<input type="text" v-focus placeholder="我有v-focus,所以,我獲取了焦點"/>我們需要加上 v-. - Vue.directive(‘focus’,{})
第二個引數是一個物件,物件內部有個inserted()的函式,函式有el這個引數. - el 這個引數表示了系結這個指令的
DOM元素,在這里就是后面那個有placeholder的input,el 就等價于document.getElementById('el.id') - 可以利用
$(el)無縫連接 jQuery
指令的生命周期
用指令我們需要:
- 新增一個指令
- 定義指令的第二個引數里的 inserted 函式
- 在需要獲取焦點的元素上,使用這個指令.
當一個指令系結到一個元素上時,其實指令的內部會有五個生命周期事件函式.
bind(){}當指令系結到 HTML元素上時觸發.只呼叫一次.inserted()當系結了指令的這個HTML元素插入到父元素上時觸發(在這里父元素是div#app).但不保證,父元素已經插入了 DOM 檔案.updated()所在組件的VNode更新時呼叫.componentUpdate指令所在的組件的VNode以及其子VNode全部更新后呼叫.unbind: 指令和元素解綁的時候呼叫,只呼叫一次
Vue 指令的宣告周期函式
Vue.directive('gqs',{
bind() {
// 當指令系結到 HTML 元素上時觸發.**只呼叫一次**
console.log('bind triggerd')
},
inserted() {
// 當系結了指令的這個HTML元素插入到父元素上時觸發(在這里父元素是 `div#app`)**.但不保證,父元素已經插入了 DOM 檔案.**
console.log('inserted triggerd')
},
updated() {
// 所在組件的`VNode`更新時呼叫.
console.log('updated triggerd')
},
componentUpdated() {
// 指令所在組件的 VNode 及其子 VNode 全部更新后呼叫,
console.log('componentUpdated triggerd')
},
unbind() {
// 只呼叫一次,指令與元素解綁時呼叫.
console.log('unbind triggerd')
}
})
HTML
<div id='app' v-gqs></div>
結果:
bind triggerd
inserted triggerd
發現默認情況下只有 bind 和 inserted 宣告周期函式觸發了.
那么剩下的三個什么時候觸發呢?
<div id='app' >
<p v-gqs v-if="show">v-if是洗掉或者新建dom元素,它會觸發unbind指令宣告周期嗎?</p>
<button @click="show=!show">toggle</button>
</div>
當指令系結的元素被銷毀時,會觸發指令的 unbind 事件.
(新建并顯示,仍然是觸發 bind & inserted)
unbind觸發.gif
<p v-gqs v-show="show2">v-show設定元素的display:block|none,會觸發componentUpdated事件</p>
<button @click="show2=!show2">toggle-v-show</button>
- 一個把元素從DOM洗掉觸發unbind().—> 僅僅是洗掉.
- 一個顯示設定元素的隱藏和顯示的時候觸發 componentUpdated() —> block | none 都觸發.
31. vue實體掛載的程序是什么
32. 組件和插件有什么區別
-
組件 (Component) 是用來構成你的 App 的業務模塊,它的目標是 App.vue,
-
插件 (Plugin) 是用來增強你的技術堆疊的功能模塊,它的目標是 Vue 本身,
33.使用vue程序中可能會遇到的問題(坑)有哪些
34. 動態給vue的data添加一個新的屬性時會發生什么
根據官方檔案定義:
如果在實體創建之后添加新的屬性到實體上,它不會觸發視圖更新,
Vue 不允許在已經創建的實體上動態添加新的根級回應式屬性 (root-level reactive property),
然而它可以使用 Vue.set(object, key, value) 方法將回應屬性添加到嵌套的物件上,
35. SPA首屏加載速度慢的怎么解決
- 通過Gzip壓縮
- 使用路由懶加載
- 利用webpack中的externals這個屬性把不需要打包的庫檔案都分離出去,減小專案打包后的大小
- 使用SSR渲染
36. mixin
多個實體參考了相同或相似的方法或屬性等,可將這些重復的內容抽取出來作為mixins的js,export出去,在需要參考的vue檔案通過mixins屬性注入,與當前實體的其他內容進行merge,
一個混入物件可以包含任意組件選項,同一個生命周期,混入物件會比組件的先執行,
//暴露兩個mixins物件
export const mixinsTest1 = {
methods: {
hello1() {
console.log("hello1");
}
},
created() {
this.hello1();
},
}
export const mixinsTest2 = {
methods:{
hello2(){
console.log("hello2");
}
},
created() {
this.hello2();
},
}
<template>
<div>
home
</div>
</template>
<script>
import {mixinsTest1,mixinsTest2} from '../util/test.js'
export default {
name: "Home",
data () {
return {
};
},
created(){
console.log("1212");
},
mixins:[mixinsTest2,mixinsTest1] // 先呼叫哪個mixins物件,就先執行哪個
}
</script>
hello2
hello1
1212
37. vue的核心是什么
- 資料驅動 專注于View 層,它讓開發者省去了操作DOM的程序,只需要改變資料,
- 組件回應原理 資料(model)改變驅動視圖(view)自動更新
- 組件化 擴展HTML元素,封裝可重用的代碼,
38. vue常用的修飾符有哪些
- v-model: .trim .number
- v-on: .stop .prevent
39. v-on可以系結多個方法嗎?
<input v-model="msg" type="text" v-on="{input:a, focus:b}"/>
40. template編譯的理解
41.axios是什么,如何中斷axios的請求
42. 如何引入scss?
安裝scss依賴包:
npm install sass-loader --save-dev npm install node-sass --save-dev
在build檔案夾下修改 webpack.base.conf.js 檔案,在 module 下的 rules 里添加配置,如下:
{ test: /\.scss$/, loaders: ['style', 'css', 'sass'] }
應用:
在vue檔案中應用scss時,需要在style樣式標簽上添加lang="scss",即<style lang="scss">,
43. 在vue中watch和created哪個先執行
watch 中的 immediate 會讓監聽在初始值宣告的時候去執行監聽計算,否則就是 created 先執行
44. 在vue中created與activated有什么區別
created():在實體創建完成后被立即呼叫,在這一步,實體已完成以下的配置:資料觀測 (data observer),property 和方法的運算,watch/event 事件回呼,然而,掛載階段還沒開始,$el property 目前尚不可用,
activated():是在路由設定<keep-alive></keep-alive>時,才會有這個生命周期,在被 keep-alive 快取的組件激活時呼叫,
45. 為什么在v-for中的key不推薦使用亂數或者index呢
因為在插入資料或者洗掉資料的時候,會導致后面的資料的key系結的index變化,進而導致從新渲染,效率會降低
46. 如何批量引入組件
動態組件使用方法
<keep-alive>
<component :is="isWhich"></component>
</keep-alive>
使用標簽保存狀態,即切換組件再次回來依然是原來的樣子,頁面不會重繪,若不需要可以去掉,
通過事件改變is系結的isWhich值即可切換成不同的組件,isWhich的值為組件名稱,
47. vue中怎么重置data
使用場景:
比如,有一個表單,表單提交成功后,希望組件恢復到初始狀態,重置data資料,
使用Object.assign(),vm.$data可以獲取當前狀態下的data,vm.$options.data可以獲取到組件初始化狀態下的data
初始狀態下設定data資料的默認值,重置時直接bject.assign(this.$data, this.$options.data())
說明:
- this.$data獲取當前狀態下的data
- this.$options.data()獲取該組件初始狀態下的data(即初始默認值)
- 如果只想修改data的某個屬性值,可以
this[屬性名] = this.$options.data()[屬性名],如this.message = this.$options.data().message
48. vue渲染模板時怎么保留模板中的HTML注釋呢
<template comments>
...
</template>
49. style加scoped屬性的用途和原理
- 用途:防止全域同名CSS污染
- 原理:在標簽加上
v-data-something屬性,再在選擇器時加上對應[v-data-something],即CSS屬性選擇器,以此完成類似作用域的選擇方式
50.在vue專案中如何配置favicon
-
將 favicon 圖片放到 static 檔案夾下
-
然后在 index.html 中添加:
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico">
51. babel-polyfill模塊
Babel默認只轉換新的JavaScript句法(syntax),而不轉換新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全域物件,以及一些定義在全域物件上的方法(比如Object.assign)都不會轉碼,
舉例來說,ES6在Array物件上新增了Array.from方法,Babel就不會轉碼這個方法,如果想讓這個方法運行,必須使用babel-polyfill,為當前環境提供一個墊片,
52. 在vue事件中傳入$event,使用e.target和e.currentTarget有什么區別
- currentTarget: 事件系結的元素
- target: 滑鼠觸發的元素
53. vue怎么實作強制重繪組件
強制重新渲染
this.$forceUpdate()
強制重新重繪某組件
//模版上系結key
<SomeComponent :key="theKey"/>
//選項里系結data
data(){
return{
theKey:0
}
}
//重繪key達到重繪組件的目的
theKey++;
54. vue給組件系結自定義事件無效怎么解決
加入.native修飾符
55. vue的屬性名與method的方法名一樣時會發生什么問題
報錯 “Method ‘xxx’ has already been defined as a data property”
鍵名優先級:props > data > methods
56. vue變數名如果以_、$開頭的屬性會發生什么問題
實體創建之后,可以通過 vm.$data 訪問原始資料物件,Vue 實體也代理了 data 物件上所有的屬性,因此訪問 vm.a 等價于訪問 vm.$data.a,
以 _ 或 $ 開頭的屬性 不會 被 Vue 實體代理,因為它們可能和 Vue 內置的屬性、API 方法沖突,可以使用 vm.$data._property 的方式訪問這些屬性,
57. vue專案本地開發完成后部署到服務器后報404
使用了history模式,而后端又沒有進行相關資源配置,
58. vue的表單修飾符.lazy
v-model默認的觸發條件是input事件,加了.lazy修飾符之后,v-model會在change事件觸發的時候去監聽
59. vue為什么要求組件模板只能有一個根元素
diff演算法要求,原始碼中patch.js中的patchVnode也是根據樹狀結構進行遍歷
60. 在vue中使用this應該注意哪些問題
生命周期的鉤子函式不能使用箭頭函式,否者this不能指向vue實體
61. <template></template>有什么用
包裹嵌套其它元素,使元素具有區域性,自身具有三個特點:
- 隱藏性:不會顯示在頁面中
- 任意性:可以寫在頁面的任意地方
- 無效性: 沒有一個根元素包裹,任何HTML內容都是無效的
62. 組件中寫name選項有什么作用
- 專案使用keep-alive時,可搭配組件name進行
快取過濾 - DOM做遞回組件時需要呼叫自身name
- vue-devtools除錯工具里顯示的
組見名稱是由vue中組件name決定的
63. prop是怎么做驗證的
- 單個型別就用Number等基礎型別
- 多個型別用陣列
- 必填的話設定require為true
- 默認值的話設定default
- 物件和陣列設定默認用工廠函式
- 自定義驗證函式validator,
64. vue權限管理,按鈕和路由
65. 大型專案你該怎么劃分結構和劃分組件
- views目錄存放一級路由的組件,即視圖組件
- Components目錄存放組件
- Store存放vuex相關檔案
- Router目錄存放路由相關檔案
- Untils目錄存放工具js檔案
- API目錄存放封裝好的與后端互動的邏輯
- Assets存放靜態檔案
66. vue3.0的新特性
67. 高階組件
68. vue-loader是什么
決議和轉換 .vue 檔案,提取出其中的邏輯代碼 script、樣式代碼 style、以及 HTML 模版 template,再分別把它們交給對應的 Loader 去處理,
69. 怎么捕獲組件vue的錯誤資訊
70.怎么配置跨域
71. vue-router怎么配置404頁面
設定 path: '*' , 并且放在最后一個
72. vue-router如何回應路由引數的變化
為什么要回應引數變化?
- 切換路由,
路由引數發生了變化,但是頁面資料并未及時更新,需要強制重繪后才會變化, - 不同路由渲染相同的組件時(組件復用比銷毀重新創建效率要高),在切換路由后,當前組件下的生命周期函式不會再被呼叫,
解決方案:
使用 watch 監聽
watch: {
$route(to, from){
if(to != from) {
console.log("監聽到路由變化,做出相應的處理");
}
}
}
向 router-view 組件中添加 key
<router-view :key="$route.fullPath"></router-view>
$route.fullPath 是完成后決議的URL,包含其查詢引數資訊和hash完整路徑
73. 切換到新路由時,面要滾動到頂部或保持原先的滾動位置
在路由實體中配置
scrollBehavior(ro,form,savedPosition){
//滾動到頂部
return {x:0,y:0}
//保持原先的滾動位置
return {selector:falsy}
}
74. 路由懶加載
75. MVVM
全稱: Model-View-ViewModel , Model 表示資料模型層, view 表示視圖層, ViewModel 是 View 和 Model 層的橋梁,資料系結到 viewModel 層并自動渲染到頁面中,視圖變化通知 viewModel 層更新資料,
76. vue中的事件系結原理
事件系結有幾種?
-
原生的事件系結,原生 dom 事件的系結,采用的是
addEventListener實作, -
組件的事件系結,組件系結事件采用的是
$on方法 ,
-
普通元素的原生事件系結在上是通過@click進行系結的 -
組件的原生事件系結是通過@click.native進行系結的,組件中的nativeOn是等價于on的, -
組件的自定義事件是通過@click系結的,是通過$on方法來實作的,必須有$emit才可以觸發,
解釋下這2種的區別:
-
原生:比如我們在原生便簽上寫一個事件
<div @click="getData"></div>,直接觸發的就是原生的點擊事件 -
組件: 現在我們自定義了個組件,想要組件上面寫事件,
<BtnGroup @click="getName" @click.native="getData"></BtnGroup>,這時候,要觸發原生的點擊事件getData,就需要使用修飾符.native,因為直接使用@click是接收來自子組件emit過來的事件getName,這樣才不會沖突,
let compiler = require('vue-template-compiler'); // vue loader中的包
let r1 = compiler.compile('<div @click="fn()"></div>'); // 給普通標簽系結click事件
// 給組件系結一個事件,有兩種系結方法
// 一種@click.native,這個系結的就是原生事件
// 另一種@click,這個系結的就是組件自定義事件
let r2 = compiler.compile('<my-component @click.native="fn" @click="fn1"></mycomponent>');
console.log(r1.render); // {on:{click}}
console.log(r2.render); // {nativeOn:{click},on:{click}}
// 為什么組件要加native?因為組件最侄訓把nativeOn屬性放到on的屬性中去,這個on會單獨處理
// 組件中的nativeOn 等價于 普通元素on,組件on會單獨處理
詳細連接
77. Vue為何采用異步渲染
Vue在更新DOM時是異步執行的,只要偵聽到資料變化,將開啟一個佇列,并緩沖在同一事件回圈中發生的所有資料變更,如果同一個watcher被多次觸發,只會被推入到佇列中一次,這種在緩沖時去除重復資料對于減少不必要的計算和DOM操作是非常重要的.
然后,在下一個的事件回圈tick中,Vue重繪佇列并執行實際(已去重的)作業,Vue在內部對異步佇列嘗試使用原生的Promise.then、MutationObserver和setImmediate,如果執行環境不支持,則會采用setTimeout(fn, 0)代替,
描述
對于Vue為何采用異步渲染,簡單來說就是為了提升性能,因為不采用異步更新,在每次更新資料都會對當前組件進行重新渲染,為了性能考慮,Vue會在本輪資料更新后,再去異步更新視圖,舉個例子,讓我們在一個方法內重復更新一個值,
this.msg = 1;
this.msg = 2;
this.msg = 3;
事實上,我們真正想要的其實只是最后一次更新而已,也就是說前三次DOM更新都是可以省略的,我們只需要等所有狀態都修改好了之后再進行渲染就可以減少一些性能損耗,
對于渲染方面的問題是很明確的,最終只渲染一次肯定比修改之后即渲染所耗費的性能少,在這里我們還需要考慮一下異步更新佇列的相關問題,假設我們現在是進行了相關處理使得每次更新資料只進行一次真實DOM渲染,來讓我們考慮異步更新佇列的性能優化,
假設這里是同步更新佇列,this.msg=1,大致會發生這些事:
msg值更新 -> 觸發setter -> 觸發Watcher的update -> 重新呼叫 render -> 生成新的vdom -> dom-diff -> dom更新
這里的dom更新并不是渲染(即布局、繪制、合成等一系列步驟),而是更新記憶體中的DOM樹結構,之后再運行this.msg=2,再重復上述步驟,之后的第3次更新同樣會觸發相同的流程,等開始渲染的時候,最新的DOM樹中確實只會存在更新完成3,從這里來看,前2次對msg的操作以及Vue內部對它的處理都是無用的操作,可以進行優化處理,
如果是異步更新佇列,會是下面的情況:
運行this.msg=1,并不是立即進行上面的流程,而是將對msg有依賴的Watcher都保存在佇列中,該佇列可能這樣[Watcher1, Watcher2...],當運行this.msg=2后,同樣是將對msg有依賴的Watcher保存到佇列中,Vue內部會做去重判斷,這次操作后,可以認為佇列資料沒有發生變化,第3次更新也是上面的程序,
當然,你不可能只對msg有操作,你可能對該組件中的另一個屬性也有操作,比如this.otherMsg=othermessage,同樣會把對otherMsg有依賴的Watcher添加到異步更新佇列中,因為有重復判斷操作,這個Watcher也只會在佇列中存在一次,本次異步任務執行結束后,會進入下一個任務執行流程,其實就是遍歷異步更新佇列中的每一個Watcher,觸發其update,然后進行重新呼叫render -> new vdom -> dom-diff -> dom更新等流程,但是這種方式和同步更新佇列相比,不管操作多少次msg,Vue在內部只會進行一次重新呼叫真實更新流程,
所以,對于異步更新佇列不是節省了
渲染成本,而是節省了Vue內部計算及DOM樹操作的成本,不管采用哪種方式,渲染確實只有一次,
此外,組件內部實際使用VirtualDOM進行渲染,也就是說,組件內部其實是不關心哪個狀態發生了變化,它只需要計算一次就可以得知哪些節點需要更新,也就是說,如果更改了N個狀態,其實只需要發送一個信號就可以將DOM更新到最新,如果我們更新多個值,
this.msg = 1;
this.age = 2;
this.name = 3;
此處我們分三次修改了三種狀態,但其實Vue只會渲染一次,因為VIrtualDOM只需要一次就可以將整個組件的DOM更新到最新,它根本不會關心這個更新的信號到底是從哪個具體的狀態發出來的,
而為了達到這個目的,我們需要將渲染操作推遲到所有的狀態都修改完成,為了做到這一點只需要將渲染操作推遲到本輪事件回圈的最后或者下一輪事件回圈,也就是說,只需要在本輪事件回圈的最后,等前面更新狀態的陳述句都執行完之后,執行一次渲染操作,它就可以無視前面各種更新狀態的語法,無論前面寫了多少條更新狀態的陳述句,只在最后渲染一次就可以了,
將渲染推遲到本輪事件回圈的最后執行渲染的時機會比推遲到下一輪快很多,所以Vue優先將渲染操作推遲到本輪事件回圈的最后,如果執行環境不支持會降級到下一輪,Vue的變化偵測機制(setter)決定了它必然會在每次狀態發生變化時都會發出渲染的信號,但Vue會在收到信號之后檢查佇列中是否已經存在這個任務,保證佇列中不會有重復,如果佇列中不存在則將渲染操作添加到佇列中,之后通過異步的方式延遲執行佇列中的所有渲染的操作并清空佇列,當同一輪事件回圈中反復修改狀態時,并不會反復向佇列中添加相同的渲染操作,所以我們在使用Vue時,修改狀態后更新DOM都是異步的,
當資料變化后會呼叫notify方法,將watcher遍歷,呼叫update方法通知watcher進行更新,這時候watcher并不會立即去執行,在update中會呼叫queueWatcher方法將watcher放到了一個佇列里,在queueWatcher會根據watcher的進行去重,若多個屬性依賴一個watcher,則如果佇列中沒有該watcher就會將該watcher添加到佇列中,然后便會在$nextTick方法的執行佇列中加入一個flushSchedulerQueue方法(這個方法將會觸發在緩沖佇列的所有回呼的執行),然后將$nextTick方法的回呼加入$nextTick方法中維護的執行佇列,flushSchedulerQueue中開始會觸發一個before的方法,其實就是beforeUpdate,然后watcher.run()才開始真正執行watcher,執行完頁面就渲染完成,更新完成后會呼叫updated鉤子,
$nextTick
在上文中談到了對于Vue為何采用異步渲染,假如此時我們有一個需求,需要在頁面渲染完成后取得頁面的DOM元素,而由于渲染是異步的,我們不能直接在定義的方法中同步取得這個值的,于是就有了vm.$nextTick方法,Vue中$nextTick方法將回呼延遲到下次DOM更新回圈之后執行,也就是在下次DOM更新回圈結束之后執行延遲回呼,在修改資料之后立即使用這個方法,能夠獲取更新后的DOM,簡單來說就是當資料更新時,在DOM中渲染完成后,執行回呼函式,
通過一個簡單的例子來演示$nextTick方法的作用,首先需要知道Vue在更新DOM時是異步執行的,也就是說在更新資料時其不會阻塞代碼的執行,直到執行堆疊中代碼執行結束之后,才開始執行異步任務佇列的代碼,所以在資料更新時,組件不會立即渲染,此時在獲取到DOM結構后取得的值依然是舊的值,而在$nextTick方法中設定的回呼函式會在組件渲染完成之后執行,取得DOM結構后取得的值便是新的值,
<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
msg: 'Vue'
},
template:`
<div>
<div ref="msgElement">{{msg}}</div>
<button @click="updateMsg">updateMsg</button>
</div>
`,
methods:{
updateMsg: function(){
this.msg = "Update";
console.log("DOM未更新:", this.$refs.msgElement.innerHTML)
this.$nextTick(() => {
console.log("DOM已更新:", this.$refs.msgElement.innerHTML)
})
}
},
})
</script>
</html>
異步機制#
Js是單執行緒的,其引入了同步阻塞與異步非阻塞的執行模式,在Js異步模式中維護了一個Event Loop,Event Loop是一個執行模型,在不同的地方有不同的實作,瀏覽器和NodeJS基于不同的技術實作了各自的Event Loop,瀏覽器的Event Loop是在HTML5的規范中明確定義,NodeJS的Event Loop是基于libuv實作的,
在瀏覽器中的Event Loop由執行堆疊Execution Stack、后臺執行緒Background Threads、宏佇列Macrotask Queue、微佇列Microtask Queue組成,
- 執行堆疊就是在
主執行緒執行同步任務的資料結構,函式呼叫形成了一個由若干幀組成的堆疊, - 后臺執行緒就是瀏覽器實作對于
setTimeout、setInterval、XMLHttpRequest等等的執行執行緒, - 宏佇列,一些
異步任務的回呼會依次進入宏佇列,等待后續被呼叫,包括setTimeout、setInterval、setImmediate(Node)、requestAnimationFrame、UI rendering、I/O等操作, - 微佇列,另一些
異步任務的回呼會依次進入微佇列,等待后續呼叫,包括Promise、process.nextTick(Node)、Object.observe、MutationObserver等操作,
當Js執行時,進行如下流程:
- 首先將執行堆疊中代碼同步執行,將這些代碼中異步任務加入后臺執行緒中,
- 執行堆疊中的同步代碼執行完畢后,執行堆疊清空,并開始掃描微佇列,
- 取出微佇列隊首任務,放入執行堆疊中執行,此時微佇列是進行了出隊操作,
- 當執行堆疊執行完成后,繼續出隊微佇列任務并執行,直到微佇列任務全部執行完畢,
- 最后一個微佇列任務出隊并進入執行堆疊后微佇列中任務為空,當執行堆疊任務完成后,開始掃面微佇列為空,繼續掃描宏佇列任務,宏佇列出隊,放入執行堆疊中執行,執行完畢后繼續掃描微佇列為空則掃描宏佇列,出隊執行,不斷往復…,
實體#
// Step 1
console.log(1);
// Step 2
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
}, 0);
// Step 3
new Promise((resolve, reject) => {
console.log(4);
resolve();
}).then(() => {
console.log(5);
})
// Step 4
setTimeout(() => {
console.log(6);
}, 0);
// Step 5
console.log(7);
// Step N
// ...
// Result
/*
1
4
7
5
2
3
6
*/
分析#
在了解異步任務的執行佇列后,回到中$nextTick方法,當用戶資料更新時,Vue將會維護一個緩沖佇列,對于所有的更新資料將要進行的組件渲染與DOM操作進行一定的策略處理后加入緩沖佇列,然后便會在$nextTick方法的執行佇列中加入一個flushSchedulerQueue方法(這個方法將會觸發在緩沖佇列的所有回呼的執行),然后將$nextTick方法的回呼加入$nextTick方法中維護的執行佇列,在異步掛載的執行佇列觸發時就會首先會首先執行flushSchedulerQueue方法來處理DOM渲染的任務,然后再去執行$nextTick方法構建的任務,這樣就可以實作在$nextTick方法中取得已渲染完成的DOM結構,
在測驗的程序中發現了一個很有意思的現象,在上述例子中的加入兩個按鈕,在點擊updateMsg按鈕的結果是3 2 1,點擊updateMsgTest按鈕的運行結果是2 3 1,
<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
msg: 'Vue'
},
template:`
<div>
<div ref="msgElement">{{msg}}</div>
<button @click="updateMsg">updateMsg</button>
<button @click="updateMsgTest">updateMsgTest</button>
</div>
`,
methods:{
updateMsg: function(){
this.msg = "Update";
setTimeout(() => console.log(1))
Promise.resolve().then(() => console.log(2))
this.$nextTick(() => {
console.log(3)
})
},
updateMsgTest: function(){
setTimeout(() => console.log(1))
Promise.resolve().then(() => console.log(2))
this.$nextTick(() => {
console.log(3)
})
}
},
})
</script>
</html>
這里假設運行環境中Promise物件是完全支持的,那么使用setTimeout是宏佇列在最后執行這個是沒有異議的,但是使用$nextTick方法以及自行定義的Promise實體是有執行順序的問題的,雖然都是微佇列任務,但是在Vue中具體實作的原因導致了執行順序可能會有所不同,首先直接看一下$nextTick方法的原始碼,關鍵地方添加了注釋,請注意這是Vue2.4.2版本的原始碼,在后期$nextTick方法可能有所變更,
/**
* Defer a task to execute it asynchronously.
*/
var nextTick = (function () {
// 閉包 內部變數
var callbacks = []; // 執行佇列
var pending = false; // 標識,用以判斷在某個事件回圈中是否為第一次加入,第一次加入的時候才觸發異步執行的佇列掛載
var timerFunc; // 以何種方法執行掛載異步執行佇列,這里假設Promise是完全支持的
function nextTickHandler () { // 異步掛載的執行任務,觸發時就已經正式準備開始執行異步任務了
pending = false; // 標識置false
var copies = callbacks.slice(0); // 創建副本
callbacks.length = 0; // 執行佇列置空
for (var i = 0; i < copies.length; i++) {
copies[i](); // 執行
}
}
// 如果支持promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
var logError = function (err) { console.error(err); };
timerFunc = function () {
p.then(nextTickHandler).catch(logError); // 掛載異步任務佇列
if (isIOS) { setTimeout(noop); }
};
} else if (typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1;
var observer = new MutationObserver(nextTickHandler);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else {
// fallback to setTimeout
/* istanbul ignore next */
timerFunc = function () {
setTimeout(nextTickHandler, 0);
};
}
return function queueNextTick (cb, ctx) { // nextTick方法真正匯出的方法
var _resolve;
callbacks.push(function () { // 添加到執行佇列中 并加入例外處理
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
//判斷在當前事件回圈中是否為第一次加入,若是第一次加入則置標識為true并執行timerFunc函式用以掛載執行佇列到Promise
// 這個標識在執行佇列中的任務將要執行時便置為false并創建執行佇列的副本去運行執行佇列中的任務,參見nextTickHandler函式的實作
// 在當前事件回圈中置標識true并掛載,然后再次呼叫nextTick方法時只是將任務加入到執行佇列中,直到掛載的異步任務觸發,便置標識為false然后執行任務,再次呼叫nextTick方法時就是同樣的執行方式然后不斷如此往復
if (!pending) {
pending = true;
timerFunc();
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve, reject) {
_resolve = resolve;
})
}
}
})();
回到剛才提出的問題上,在更新DOM操作時會先觸發$nextTick方法的回呼,解決這個問題的關鍵在于誰先將異步任務掛載到Promise物件上,
首先對有資料更新的updateMsg按鈕觸發的方法進行debug,斷點設定在Vue.js的715行,版本為2.4.2,在查看呼叫堆疊以及傳入的引數時可以觀察到第一次執行$nextTick方法的其實是由于資料更新而呼叫的nextTick(flushSchedulerQueue)陳述句,也就是說在執行this.msg = “Update”;的時候就已經觸發了第一次的$nextTick方法,此時在$nextTick方法中的任務佇列會首先將flushSchedulerQueue方法加入佇列并掛載$nextTick方法的執行佇列到Promise物件上,然后才是自行自定義的Promise.resolve().then(() => console.log(2))陳述句的掛載,當執行微任務佇列中的任務時,首先會執行第一個掛載到Promise的任務,此時這個任務是運行執行佇列,這個佇列中有兩個方法,首先會運行flushSchedulerQueue方法去觸發組件的DOM渲染操作,然后再執行console.log(3),然后執行第二個微佇列的任務也就是() => console.log(2),此時微任務佇列清空,然后再去宏任務佇列執行console.log(1),
接下來對于沒有資料更新的updateMsgTest按鈕觸發的方法進行debug,斷點設定在同樣的位置,此時沒有資料更新,那么第一次觸發$nextTick方法的是自行定義的回呼函式,那么此時$nextTick方法的執行佇列才會被掛載到Promise物件上,很顯然在此之前自行定義的輸出2的Promise回呼已經被掛載,那么對于這個按鈕系結的方法的執行流程便是首先執行console.log(2),然后執行$nextTick方法閉包的執行佇列,此時執行佇列中只有一個回呼函式console.log(3),此時微任務佇列清空,然后再去宏任務佇列執行console.log(1),
簡單來說就是誰先掛載Promise物件的問題,在呼叫$nextTick方法時就會將其閉包內部維護的執行佇列掛載到Promise物件,在資料更新時Vue內部首先就會執行$nextTick方法,之后便將執行佇列掛載到了Promise物件上,其實在明白Js的Event Loop模型后,將資料更新也看做一個$nextTick方法的呼叫,并且明白$nextTick方法會一次性執行所有推入的回呼,就可以明白其執行順序的問題了,下面是一個關于$nextTick方法的最小化的DEMO,
var nextTick = (function(){
var pending = false;
const callback = [];
var p = Promise.resolve();
var handler = function(){
pending = true;
callback.forEach(fn => fn());
}
var timerFunc = function(){
p.then(handler);
}
return function queueNextTick(fn){
callback.push(() => fn());
if(!pending){
pending = true;
timerFunc();
}
}
})();
(function(){
nextTick(() => console.log("觸發DOM渲染佇列的方法")); // 注釋 / 取消注釋 來查看效果
setTimeout(() => console.log(1))
Promise.resolve().then(() => console.log(2))
nextTick(() => {
console.log(3)
})
})();
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/286963.html
標籤:其他
上一篇:【前后端分離】Springboot+Vue實作Kaptcha生成驗證碼、Graphics 2D隨機驗證碼(兩種樣式) | 通過Vue顯示到前端頁面
