Vue共識:
在 Vue 中我們習慣把虛擬DOM稱為 VNode,它既可以代表一個 VNode 節點,也可以代表一顆 VNode 樹。
組件的核心是它能夠產出一堆VNode。
對于 Vue 來說一個組件的核心就是它的渲染函數,組件的掛載本質就是執行渲染函數并得到要渲染的VNode,至于data/props/computed 這都是為渲染函數產出 VNode 過程中提供數據來源服務的,最關鍵的就是組件最終產出的VNode,因為這個才是要渲染的內容。
一、Vue基礎
1. Vue的基本原理
當一個Vue實例創建時,vue會遍歷data選項的屬性,用 Object.defineProperty(vue3.0使用proxy )將它們轉為 getter/setter 并且在內部追蹤相關依賴,在屬性被訪問和修改時通知變化。 每個組件實例都有相應的 watcher程序實例,它會在組件渲染的過程中把屬性記錄為依賴,之后當依賴項的setter被調用時,會通知watcher重新計算,從而致使它關聯的組件得以更新。
2. 雙向數據綁定的原理
vue.js 是采用數據劫持結合發布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數據變動時發布消息給訂閱者,觸發相應的監聽回調。主要分為以下幾個步驟:
1、需要observe的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上setter和getter這樣的話,給這個對象的某個值賦值,就會觸發setter,那么就能監聽到了數據變化
2、compile解析模板指令,將模板中的變量替換成數據,然后初始化渲染頁面視圖,并將每個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖
3、Watcher訂閱者是Observer和Compile之間通信的橋梁,主要做的事情是:
①在自身實例化時往屬性訂閱器(dep)里面添加自己
②自身必須有一個update()方法
③待屬性變動dep.notice()通知時,能調用自身的update()方法,并觸發Compile中綁定的回調,則功成身退。
4、MVVM作為數據綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監聽自己的model數據變化,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據model變更的雙向綁定效果。
3. 使用 Object.defineProperty() 來進行數據劫持有什么缺點?
有一些對屬性的操作,使用這種方法無法攔截,比如說通過下標方式修改數組數據或者給對象新增屬性,vue 內部通過重寫函數解決了這個問題。在 Vue3.0 中已經不使用這種方式了,而是通過使用 Proxy 對對象進行代理,從而實現數據劫持。使用Proxy 的好處是它可以完美的監聽到任何方式的數據改變,唯一的缺點是兼容性的問題,因為這是 ES6 的語法。
4. MVVM和MVC的區別
MVC、MVP 和 MVVM 是三種常見的軟件架構設計模式,主要通過分離關注點的方式來組織代碼結構,優化我們的開發效率。
在開發單頁面應用時,往往一個路由頁面對應了一個腳本文件,所有的頁面邏輯都在一個腳本文件里。頁面的渲染、數據的獲取,對用戶事件的響應所有的應用邏輯都混合在一起,這樣在開發簡單項目時,可能看不出什么問題,當時一旦項目變得復雜,那么整個文件就會變得冗長,混亂,這樣對我們的項目開發和后期的項目維護是非常不利的。
(1)MVC
MVC 通過分離 Model、View 和 Controller 的方式來組織代碼結構。其中 View 負責頁面的顯示邏輯,Model 負責存儲頁面的業務數據,以及對相應數據的操作。并且 View 和 Model 應用了觀察者模式,當 Model 層發生改變的時候它會通知有關 View 層更新頁面。Controller 層是 View 層和 Model 層的紐帶,它主要負責用戶與應用的響應操作,當用戶與頁面產生交互的時候,Controller 中的事件觸發器就開始工作了,通過調用 Model 層,來完成對 Model 的修改,然后 Model 層再去通知 View 層更新。
(2)MVVM
MVVM 分為 Model、View、ViewModel 三者。
Model代表數據模型,數據和業務邏輯都在Model層中定義;
View代表UI視圖,負責數據的展示;
ViewModel負責監聽Model中數據的改變并且控制視圖的更新,處理用 戶交互操作;
Model和View并無直接關聯,而是通過ViewModel來進行聯系的,Model和ViewModel之間有著雙向數據綁定的聯系。因此當Model中 的數據改變時會觸發View層的刷新,View中由于用戶交互操作而改變的 數據也會在Model中同步。
這種模式實現了 Model和View的數據自動同步,因此開發者只需要專注 對數據的維護操作即可,而不需要自己操作DOM。
(2)MVP
MVP 模式與 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中我們使用觀察者模式,來實現當 Model 層數據發生變化的時候,通知 View 層的更新。這樣 View 層和 Model 層耦合在一起,當項目邏輯變得復雜的時候,可能會造成代碼的混亂,并且可能會對代碼的復用性造成一些問題。MVP 的模式通過使用 Presenter 來實現對 View 層和 Model 層的解耦。MVC 中的Controller 只知道 Model 的接口,因此它沒有辦法控制 View 層的更新,MVP 模式中,View 層的接口暴露給了 Presenter 因此我們可以在 Presenter 中將 Model 的變化和 View 的變化綁定在一起,以此來實現 View 和 Model 的同步更新。這樣就實現了對 View 和 Model 的解耦,Presenter 還包含了其他的響應邏輯。
5. Computed和Watch的區別
對于Computed:
它支持緩存,只有依賴的數據發生了變化,才會重新計算
不支持異步,當Computed中有異步操作時,無法監聽數據的變化
computed的值會默認走緩存,計算屬性是基于它們的響應式依賴進行緩存的,也就是基于data聲明過,或者父組件傳遞過來的props中的數據進行計算的。
如果一個屬性是由其他屬性計算而來的,這個屬性依賴其他的屬性,一般會使用computed
如果computed屬性的屬性值是函數,那么默認使用get方法,函數的返回值就是屬性的屬性值;在computed中,屬性有一個get方法和一個set方法,當數據發生變化時,會調用set方法。
對于Watch:
它不支持緩存,數據變化時,它就會觸發相應的操作
支持異步監聽
監聽的函數接收兩個參數,第一個參數是最新的值,第二個是變化之前的值
當一個屬性發生變化時,就需要執行相應的操作
監聽數據必須是data中聲明的或者父組件傳遞過來的props中的數據,當發生變化時,會出大其他操作,函數有兩個的參數:
immediate:組件加載立即觸發回調函數
deep:深度監聽,發現數據內部的變化,在復雜數據類型中使用,例如數組中的對象發生變化。需要注意的是,deep無法監聽到數組和對象內部的變化。
當想要執行異步或者昂貴的操作以響應不斷的變化時,就需要使用watch。
總結:
computed 計算屬性 : 依賴其它屬性值,并且 computed 的值有緩存,只有它依賴的 屬性值發生改變,下一次獲取 computed 的值時才會重新計算 computed 的值。
watch 偵聽器 : 更多的是觀察的作用,無緩存性,類似于某些數據的監聽回調,每 當監聽的數據變化時都會執行回調進行后續操作。
運用場景:
當我們需要進行數值計算,并且依賴于其它數據時,應該使用 computed,因為可以利 用 computed 的緩存特性,避免每次獲取值時,都要重新計算。
當我們需要在數據變化時執行異步或開銷較大的操作時,應該使用 watch,使用 watch 選項允許我們執行異步操作 ( 訪問一個 API ),限制我們執行該操作的頻率, 并在我們得到最終結果前,設置中間狀態。這些都是計算屬性無法做到的。
6. Computed 和 Methods 的區別
https://segmentfault.com/a/1190000014478664
methods與computed之間的差別:
(1)methods和computed里的方法在初始化執行過后,只要任何值有更新,那么所有在computed計算屬性里和其相關的值都會更新。
methods只有在調用的時候才會執行對應的方法,不會自動同步數據。
(2) computed是屬性訪問,而methods是函數調用
computed帶有緩存功能,而methods不是
computed其實是就是屬性,之所以與data區分開,只不過為了防止文本插值中邏輯過重,會導致不易維護
(3) computed定義的方法我們是以屬性的形式訪問的,和data里的屬性訪問形式一樣,{{function}}
但是methods定義的方法,我們必須要加上()來調用,如{{function()}},否則,視圖會出現function (){[native code]}的情況
7. slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的內容分發機制,組件內部的模板引擎使用slot元素作為承載分發內容的出口。插槽slot是子組件的一個模板標簽元素,而這一個標簽元素是否顯示,以及怎么顯示是由父組件決定的。slot又分三類,默認插槽,具名插槽和作用域插槽。
(1) 默認插槽:又名匿名插槽,當slot沒有指定name屬性值的時候一個默認顯示插槽,一個組件內只有有一個匿名插槽。
(2) 具名插槽:帶有具體名字的插槽,也就是帶有name屬性的slot,一個組件可以出現多個具名插槽。
(3)作用域插槽:默認插槽、具名插槽的一個變體,可以是匿名插槽,也可以是具名插槽,該插槽的不同點是在子組件渲染作用域插槽時,可以將子組件內部的數據傳遞給父組件,讓父組件根據子組件的傳遞過來的數據決定如何渲染該插槽。
實現原理:當子組件vm實例化時,獲取到父組件傳入的slot標簽的內容,存放在vm.slot.default,具名插槽為vm.
slot中的內容進行替換,此時可以為插槽傳遞數據,若存在數據,則可稱該插槽為作用域插槽。
slot的意思是插槽,想想你的電腦主板上的各種插槽,有插CPU的,有插顯卡的,有插內存的,有插硬盤的,所以假設有個組件是computer,組件computer:
<template>
<div>
<slot name="CPU">這兒插你的CPU</slot>
<slot name="GPU">這兒插你的顯卡</slot>
<slot></slot>
<slot name="Memory">這兒插你的內存</slot>
<slot name="Hard-drive">這兒插你的硬盤</slot>
</div>
</template>
那么組裝一個電腦,就可以在調用組件的頁面這么寫:
<template>
<computer>
<div slot="CPU">Intel Core i7</div>
<div slot="GPU">GTX980Ti</div>
<div>想加內容就加內容</div>
<div slot="Memory">Kingston 32G</div>
<div slot="Hard-drive">Samsung SSD 1T</divt>
</computer>
</template>
<script>
import computerfrom "./computer";
export default {
name: "page",
components: {
computer
},
data() {
return {
};
},
computed: {},
methods: {
}
};
</script>
頁面顯示:Intel Core i7 GTX980Ti 想加內容就加內容 Kingston 32G Samsung SSD 1T
二、生命周期
使用建議:
1. beforeCreate:加載loading事件
2. created:結束loading、初始化、請求數據、實現函數自執行
3. mounted:拿回數據,配合路由鉤子做一些事
4. beforeDestory:destoryed:當前組件已被刪除,清空相關內容
1. created和mounted的區別
(1)created:在模板渲染成html前調用,即通常初始化某些屬性值,然后再渲染成視圖。
(2)mounted:在模板渲染成html后調用,通常是初始化頁面完成后,再對html的dom節點進行一些需要的操作。
2. 接口請求一般放在哪個生命周期中?
可以在鉤子函數 created、beforeMount、mounted 中進行調用,因為在這三個鉤子函數中,data 已經創建,可以將服務端端返回的數據進行賦值。
推薦在 created 鉤子函數中調用異步請求,因為在 created 鉤子函數中調用異步請求有以下優點:
能更快獲取到服務端數據,減少頁面loading 時間;
ssr不支持 beforeMount 、mounted 鉤子函數,所以放在 created 中有助于一致性;
三、組件通信
如圖所示:
A和B、B和C、B和D都是父子關系,C和D是兄弟關系,A和C是隔代關系(可能隔多代)。
eg:
props、on、vuex、
children、
listeners和provide/inject
方法一、props/$emit
父組件A通過props的方向子組件B傳遞,B到A通過在B組件中$emit,A組件中v-on的方式實現。
eg:
子組件
<template>
<div class="navBar">
<div class="navBarItem" v-for="item in dataList" :key="item.id" :index="item.id">
<span v-if="item.isNow" class="nowTitle" @click="navClick(item)">{{item.title}}</span>
<span v-else class="title" @click="navClick(item)">{{item.title}}</span>
<span class="separator">></span>
</div>
</div>
</template>
<script>
export default {
name: 'navBar',
data () {
return {}
},
props: {
dataList: Array
},
methods: {
navClick (item) {
this.$emit('navClick', item)
}
}
}
</script>
父組件:
<template>
<NavBar :dataList="navBarData" @navClick="navClick"></NavBar>
</template>
<script>
import NavBar from '@/components/NavBar'
export default {
name: 'detail',
data () {
return {
navBarData: [
{ title: '鯨選資源', url: '/whaleselect', id: 1, isNow: false },
{ title: '文件詳情', url: '/jx', id: 2, isNow: true }
]}
},
props: {
dataList: Array
},
methods: {
navClick ({ url, id }) {
this.$utils.link(`${url}/` + id)
}
},
components: {
NavBar,
}
}
</script>
1.父組件向子組件傳值
父組件通過props向下傳遞數據給子組件
2.子組件向父組件傳值(通過事件形式)
子組件通過this.$emit('方法名',傳遞的值),父組件定義同名方法即可
EventBus
EventBus事件總線適用于父子組件、非父子組件等之間的通信;
全局或者公共組件注冊一個vue實例,利用里面的注冊喝監聽事件。
(1)創建事件中心管理組件之間的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)發送事件
假設我們有兩個兄弟組件C和D:
<template>
<div>
<C></C>
<D></D>
</div>
</template>
<script>
import Cfrom './C.vue'
import D from './D.vue'
export default {
components: { C, D}
}
</script>
在C組件中發送事件:
<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)接收事件
在D組件中發送事件:
<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>
在上述代碼中,這就相當于將num值存貯在了事件總線中,在其他組件中可以直接訪問。事件總線就相當于一個橋梁,不用組件通過它來通信。
雖然看起來比較簡單,但是這種方法也有不變之處,如果項目過大,使用這種方式進行通信,后期維護起來會很困難。
方法三 Vuex
1.原理
Vuex實現了一個單向數據流,在全局擁有一個State存放數據,當組件要更改State中的數據時,必須通過Mutation進行,Mutation同時提供了訂閱者模式供外部插件調用獲取State數據的更新。當所有異步操作(常見于調用后端接口異步獲取更新數據)或批量的同步操作需要走Action,但是Action也是無法直接修改State的,還是需要通過Mutation來修改State的數據。最后,根據State的變化,渲染到視圖上。
(1)vuex下的store.js文件
import Vuex from 'vuex'
export default new Vuex.Store({
// 定義狀態 值 方法
state: {
// 獲取文件夾
getfolderscallback: function () {},
// 驗證用戶的Id的token
token: '',
// 用戶id
uid: '',
// 是否登錄
isLogin: false,
// 用戶名
username: '',
// 用戶頭像
logo: ''
},
mutations: {
// 獲取文件夾
getfolders (state) {
state.getfolderscallback()
},
// 設置上面的全局變量
setAttr (state, data) {
state[data.name] = data.val
}
}
})
(2)頁面改變值
mounted () {
// 設置全局變量-方法-獲取文件夾
this.$store.commit('setAttr', {
name: 'getfolderscallback',
val: this.getfolders
})
},}
methods: {
getuser () {
// 本地判斷Cookie,判斷用戶是否登錄
if (this.$utils.getCookie(this.$glb.fmCookieName) !== null) {
this.$api.post('/center/getuser', {}, res => {
if (!res.status) {
return
}
this.$store.commit('setAttr', {name: 'isLogin', val: true})
this.$store.commit('setAttr', {name: 'logo', val: res.data.logo})
this.$store.commit('setAttr', {name: 'uid', val: res.data.userid})
this.$store.commit('setAttr', {name: 'token', val: res.data.token})
this.$store.commit('setAttr', {name: 'username', val: res.data.username})
})
}
},
// 上傳文件框選擇文件夾
getfolders () {
this.$api.post('/file/foldertreelist', {}, (res) => {
this.folderList = res.data
})
},
}
(3)頁面調用vuex State中的值
<template>
<!-- 登錄或未登錄 --S-->
<div class="isLoginBox">
<!-- 登錄 -->
<div class="loginBox" v-if="this.$store.state.isLogin">
退出
</div>
<!-- 未登錄 -->
<div class="unLoginBox" v-else>
<div class="loginOrRegister">
<span class="login" >登錄</span>
<span class="line"></span>
<span class="register">注冊</span>
</div>
</div>
</div>
<!-- 登錄或未登錄 --E-->
</template>
<script>
export default {
name: 'SignUpBar',
data () {
return {
}
},
methods: {
}
</script>
2.各模塊在核心流程中的主要功能:
1.Vue Components∶ Vue組件。HTML頁面上,負責接收用戶操作等交互行為,執行dispatch方法觸發對應action進行回應。
- dispatch∶操作行為觸發方法,是唯一能執行action的方法。
3.actions∶ 操作行為處理模塊,由組件中的$store.dispatch('action 名稱',data1)來觸發。然后由commit()來觸發mutation的調用,間接更新state。負責處理Vue Components接收到的所有交互行為。包含同步/異步操作,支持多個同名方法,按照注冊的順序依次觸發。向后臺API請求的操作就在這個模塊中進行,包括觸發其他action以及提交mutation的操作。該模塊提供了Promise的封裝,以支持action的鏈式觸發。 - commit∶狀態改變提交操作方法。對mutation進行提交,是唯一能執行mutation的方法。
- mutations∶狀態改變操作方法,由actions中的commit('mutation 名稱')來觸發。是Vuex修改state的唯一推薦方法,其他修改方式在嚴格模式下將會報錯。該方法只能進行同步操作,且方法名只能全局唯一。操作之中會有一些hook暴露出來,以進行state的監控等。
- state∶ 頁面狀態管理容器對象。集中存儲Vuecomponents中data對象的零散數據,全局唯一,以進行統一的狀態管理。頁面顯示所需的數據從該對象中進行讀取,利用Vue的細粒度數據響應機制來進行高效的狀態更新。
7。 getters∶ state對象讀取方法。圖中沒有單獨列出該模塊,應該被包含在了render中,Vue Components通過該方法讀取全局state對象。
3.Vuex(狀態:數組)與localStorage(字符串)
vuex是vue的狀態管理器,存儲的數據是響應式的。但是并不會保存起來,刷新之后就回到初始狀態,具體做飯應該在vuex里數據改變的時候拷貝一份保存到localStorge里面,刷新之后,如果localStorge里有保存的數據,取出來再替換store里面的state
方法四、
listeners
方法五、依賴注入 provide/inject
祖先組件中通過provider來提供變量,然后在子孫組件中通過inject來注入變量。provide/inject API主要解決了跨級組件間的通信問題,不過它的使用場景,主要是子組件獲取上級組件的狀態,跨級組件間建立了一種主動提供與依賴注入的關系。
project / inject是Vue提供的兩個鉤子,和data、methods是同級的。并且project的書寫形式和data一樣。
project 鉤子用來發送數據或方法
inject鉤子用來接收數據或方法
eg:兩個組件:A.vue和B.vue,B是A的子組件
// A.vue
export default {
provide: {
name: '測試張三'
}
}
// B.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // 測試張三
}
}
核心用法:在A.vue里,設置了一個provide:name,值是測試張三,它的作用就是將name這個變量提供給它的所有子組件。
在B.vue中,通過inject注入了從A組件中提供的name變量,在B組件中,就可以直接通過this.name 訪問這個變量,值就是測試張三。
注意:**provide和inject綁定并不是可響應的。這是可以為之的。然而,如果你傳入了一個可監聽的對象,那么其對象的屬性還是可響應的。A.vue的name如果改變了,B.vue的this.name是不改變的,仍然是測試張三
provide與inject實現數據響應式
兩個辦法:
(1)provide祖先組件的實例,然后在子孫組件中注入依賴,這樣就可以在子孫組件中直接修改祖先組件的實例的屬性,不過這種方法有個缺點就是這個實例上掛載很多沒有必要的東西,eg:props,methods
(2)使用2.6最新API Vue.observable 優化響應式 provide
// A 組件
<div>
<h1>A 組件</h1>
<button @click="() => changeColor()">改變color</button>
<ChildrenB />
<ChildrenC />
</div>
......
data() {
return {
color: "blue"
};
},
// provide() {
// return {
// theme: {
// color: this.color //這種方式綁定的數據并不是可響應的
// } // 即A組件的color變化后,組件D、E、F不會跟著變
// };
// },
provide() {
return {
theme: this//方法一:提供祖先組件的實例
};
},
methods: {
changeColor(color) {
if (color) {
this.color = color;
} else {
this.color = this.color === "blue" ? "red" : "blue";
}
}
}
// 方法二:使用2.6最新API Vue.observable 優化響應式 provide
// provide() {
// this.theme = Vue.observable({
// color: "blue"
// });
// return {
// theme: this.theme
// };
// },
// methods: {
// changeColor(color) {
// if (color) {
// this.theme.color = color;
// } else {
// this.theme.color = this.theme.color === "blue" ? "red" : "blue";
// }
// }
// }
// F 組件
<template functional>
<div class="border2">
<h3 :style="{ color: injections.theme.color }">F 組件</h3>
</div>
</template>
<script>
export default {
inject: {
theme: {
//函數式組件取值不一樣
default: () => ({})
}
}
};
</script>
方法五、
children 與ref
ref:如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子組件上,引用就指向組件實例;
children:訪問父/子實例
注意:這兩種都是直接得到組件實例,使用后可以直接調用組件的方法或訪問數據。
(1)用ref來訪問組件:
// component-a 子組件
export default {
data () {
return {
title: 'Vue.js'
}
},
methods: {
sayHello () {
window.alert('Hello');
}
}
}
// 父組件(頁面)
<template>
<component-a ref="comA"></component-a>
</template>
<script>
export default {
mounted () {
const comA = this.$refs.comA;
console.log(comA.title); // Vue.js
comA.sayHello(); // 彈窗
}
}
</script>
不過,這兩種方法的弊端是:無法在跨級或者兄弟間通信
// parent.vue
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>
總結
常用使用場景可以分為三類:
(1)父子通信:
父to子傳遞數據是通過props,子to父是通過parent/
attrs/$listeners;
(2)兄弟通信:
EventBus;
Vuex;
(3跨級通信:
Event Bus;
Vuex;
provide/inject API;
listeners;