當(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)的組件得以更新。
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è)步驟:
在對(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ǔ)法。
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和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)邏輯。
對(duì)于Computed:
對(duì)于Watch:
當(dāng)想要執(zhí)行異步或者昂貴的操作以響應(yīng)不斷的變化時(shí),就需要使用watch。
總結(jié):
運(yùn)用場(chǎng)景:
可以將同一函數(shù)定義為一個(gè) method 或者一個(gè)計(jì)算屬性。對(duì)于最終的結(jié)果,兩種方式是相同的
不同點(diǎn):
slot又名插槽,是Vue的內(nèi)容分發(fā)機(jī)制,組件內(nèi)部的模板引擎使用slot元素作為承載分發(fā)內(nèi)容的出口。插槽slot是子組件的一個(gè)模板標(biāo)簽元素,而這一個(gè)標(biāo)簽元素是否顯示,以及怎么顯示是由父組件決定的。slot又分三類,默認(rèn)插槽,具名插槽和作用域插槽。
實(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ù),則可稱該插槽為作用域插槽。
根據(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)景:
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) : '--'
}
}
既然是要保持頁(yè)面的狀態(tài)(其實(shí)也就是組件的狀態(tài)),那么會(huì)出現(xiàn)以下兩種情況:
那么可以按照這兩種情況分別得到以下方法:
組件會(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)。
(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)保持的效果。
組件不會(huì)被卸載:
(1)單頁(yè)面渲染
要切換的組件作為子組件全屏渲染,父組件中正常儲(chǔ)存頁(yè)面狀態(tài)。
除此之外,在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 // 需要被緩存
}
},
.stop
?:等同于 JavaScript 中的 ?event.stopPropagation()
? ,防止事件冒泡;.prevent
? :等同于 JavaScript 中的 ?event.preventDefault()
? ,防止執(zhí)行預(yù)設(shè)的行為(如果事件可取消,則取消該事件,而不停止事件的進(jìn)一步傳播);.capture
? :與事件冒泡的方向相反,事件捕獲由外到內(nèi);.self
? :只會(huì)觸發(fā)自己范圍內(nèi)的事件,不包含子元素;.once
? :只會(huì)觸發(fā)一次。(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)的值。
可以。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 與父組件的交互如下:
searchText
?變量傳入custom-input 組件,使用的 prop 名為 ?value
?;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)"
>
`
})
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)行。
如果需要在組件切換的時(shí)候,保存一些組件的狀態(tài)防止多次渲染,就可以使用 keep-alive 組件包裹需要保存的組件。
(1)keep-alive
keep-alive有以下三個(gè)屬性:
注意:keep-alive 包裹動(dòng)態(tài)組件時(shí),會(huì)緩存不活動(dòng)的組件實(shí)例。
主要流程
(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ù):
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)步驟:
(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ò)程。
首次渲染
// 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
}
// 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)如下∶
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ī)制的原因∶
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:
nextTick()
?的回調(diào)函數(shù)中。nextTick()
?的回調(diào)函數(shù)中。因?yàn)樵赾reated()鉤子函數(shù)中,頁(yè)面的DOM還未渲染,這時(shí)候也沒(méi)辦法操作DOM,所以,此時(shí)如果想要操作DOM,必須將操作的代碼放在 nextTick()
的回調(diào)函數(shù)中。
<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ì)跟著改變了。
在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。
概念:
區(qū)別:
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)
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ù)。
不會(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í)際(已去重的)工作。
(1)mixin 和 extends
mixin 和 extends均是用于合并、拓展組件的,兩者均通過(guò) mergeOptions 方法實(shí)現(xiàn)合并。
(2)mergeOptions 的執(zhí)行過(guò)程
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)
}
}
}
在 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:{}}
?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)用。
el:綁定元素
bing: 指令核心對(duì)象,描述指令全部信息屬性
name
value
oldValue
expression
arg
modifers
vnode 虛擬節(jié)點(diǎn)
oldVnode:上一個(gè)虛擬節(jié)點(diǎn)(更新鉤子函數(shù)中才有用)
(2)使用場(chǎng)景
(3)使用案例
初級(jí)應(yīng)用:
高級(jí)應(yīng)用:
子組件不可以直接改變父組件的數(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è)自定義事件,父組件接收到后,由父組件修改。
在初始化 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ò)程。
相似之處:
不同之處 :
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)。
3)組件化
React與Vue最大的不同是模板的編寫。
具體來(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)原理不同
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)建工具:
7)跨平臺(tái)
kb
?;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ì);dom
? 操作是非常耗費(fèi)性能的,不再使用原生的 ?dom
?操作節(jié)點(diǎn),極大解放 ?dom
?操作,但具體操作的還是 ?dom
?不過(guò)是換了另一種方式;react
?而言,同樣是操作虛擬 ?dom
?,就性能而言, ?vue
?存在很大的優(yōu)勢(shì)。相同點(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ò)處理,不再需要處理,直接上傳。
delete
?只是被刪除的元素變成了 ?empty/undefined
? 其他的元素的鍵值還是不變。Vue.delete
? 直接刪除了數(shù)組 改變了數(shù)組的鍵值。當(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(this.arr, 0, "OBKoro1"); // 改變數(shù)組
this.$set(this.obj, "c", "OBKoro1"); // 改變對(duì)象
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)原理是:
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。
SSR也就是服務(wù)端渲染,也就是將Vue在客戶端把標(biāo)簽渲染成HTML的工作放在服務(wù)端完成,然后再把html直接返回給客戶端
SSR的優(yōu)勢(shì):
SSR的缺點(diǎn):
(1)編碼階段
(2)SEO優(yōu)化
(3)打包優(yōu)化
(4)用戶體驗(yà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):
缺點(diǎn):
對(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ù)。
使用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'}"
這個(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')
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)行合并。
優(yōu)點(diǎn):
缺點(diǎn):
Vue 實(shí)例有?個(gè)完整的?命周期,也就是從開(kāi)始創(chuàng)建、初始化數(shù)據(jù)、編譯模版、掛載Dom -> 渲染、更新 -> 渲染、卸載 等?系列過(guò)程,稱這是Vue的?命周期。
$el
? 屬性。this
?仍能獲取到實(shí)例。另外還有 keep-alive
獨(dú)有的生命周期,分別為 activated
和 deactivated
。用 keep-alive
包裹的組件在切換時(shí)不會(huì)進(jìn)行銷毀,而是緩存到內(nèi)存中并執(zhí)行 deactivated
鉤子函數(shù),命中緩存渲染后會(huì)執(zhí)行 activated
鉤子函數(shù)。
加載渲染過(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
我們可以在鉤子函數(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):
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ù)。
組件通信的方式如下:
父組件通過(guò) props
向子組件傳遞數(shù)據(jù),子組件通過(guò) $emit
和父組件通信
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>
$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>
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ì)很困難。
這種方式就是Vue中的依賴注入,該方法用于父子組件之間的通信。當(dāng)然這里所說(shuō)的父子不一定是真正的父子,也可以是祖孫組件,在層數(shù)很深的情況下,可以使用這種方法來(lái)進(jìn)行傳值。就不用一層一層的傳遞了。
project / inject
是Vue提供的兩個(gè)鉤子,和 data
、methods
是同級(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)式的。
這種方式也是實(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>
$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
的值。
需要注意:
$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ì)象
考慮一種場(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>
在上述代碼中:
$listeners
? 屬性$attrs
?屬性,C組件可以直接獲取到A組件中傳遞下來(lái)的props(除了B組件中props聲明的)(1)父子組件間通信
(2)兄弟組件間通信
(3)任意組件之間
如果業(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á)到了解耦的目的。
非懶加載:
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'
}
]
}))
Vue-Router有兩種模式:hash模式和history模式。默認(rèn)的路由模式是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)了。
簡(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):
pushState()
? 和 ?replaceState()
? 方法,這兩個(gè)方法應(yīng)用于瀏覽器的歷史記錄棧,提供了對(duì)歷史記錄進(jìn)行修改的功能。只是當(dāng)他們進(jìn)行修改時(shí),雖然修改了url,但瀏覽器不會(huì)立即向后端發(fā)送請(qǐng)求。如果要做到改變url但又不刷新頁(yè)面的效果,就需要前端用上這兩個(gè)API。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: [...]
})
調(diào)用 history.pushState() 相比于直接修改 hash,存在以下優(yōu)勢(shì):
hash模式和history模式都有各自的優(yōu)勢(shì)和缺陷,還是要根據(jù)實(shí)際情況選擇性的使用。
(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)記錄。
(1)param方式
/router/:id
?/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
?,也就是普通配置/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 獲取傳遞的值
一、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í)的
vue-router全局有三個(gè)路由鉤子;
具體使用∶
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();
}
})
router.afterEach((to, from) => {
// 跳轉(zhuǎn)之后滾動(dòng)條回到頂部
window.scrollTo(0,0);
});
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()
}
}
]
beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
這三個(gè)鉤子都有三個(gè)參數(shù)∶to、from、next
注意點(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)
路由導(dǎo)航、keep-alive、和組件生命周期鉤子結(jié)合起來(lái)的,觸發(fā)順序,假設(shè)是從a組件離開(kāi),第一次進(jìn)入b組件∶
location.href= /url
? 來(lái)跳轉(zhuǎn),簡(jiǎn)單方便,但是刷新了頁(yè)面;history.pushState( /url )
? ,無(wú)刷新頁(yè)面,靜態(tài)跳轉(zhuǎn);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模式下。用法: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ù)。
在前端技術(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)題:
為了解決這個(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)題:
從這兩個(gè)問(wèn)題來(lái)看,服務(wù)端已經(jīng)完全救不了這個(gè)場(chǎng)景了。所以要靠咱們前端自力更生,不然怎么叫“前端路由”呢?作為前端,可以提供這樣的解決思路:
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為Vue Components建立起了一個(gè)完整的生態(tài)圈,包括開(kāi)發(fā)中的API調(diào)用一環(huán)。
(1)核心流程中的主要功能:
(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ì)象。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)在于:
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)如下:
(1)最重要的區(qū)別
(2)應(yīng)用場(chǎng)景
(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。
(1)Redux 和 Vuex區(qū)別
通俗點(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)共同思想
本質(zhì)上:redux與vuex都是對(duì)mvvm思想的服務(wù),將數(shù)據(jù)從視圖中抽離的一種方案;
形式上:vuex借鑒了redux,將store作為全局的數(shù)據(jù)中心,進(jìn)行mode管理;
由于傳參的方法對(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ù)。
有五種,分別是 State、 Getter、Mutation 、Action、 Module
在嚴(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,
})
使用mapGetters輔助函數(shù), 利用對(duì)象展開(kāi)運(yùn)算符將getter混入computed 對(duì)象中
import {mapGetters} from 'vuex'
export default{
computed:{
...mapGetters(['total','discountTotal'])
}
}
使用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)
(1)監(jiān)測(cè)機(jī)制的改變
(2)只能監(jiān)測(cè)屬性,不能監(jiān)測(cè)對(duì)象
(3)模板
(4)對(duì)象式的組件聲明方式
(5)其它方面的更改
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)題:
$set
? 來(lái)調(diào)用 ?Object.defineProperty()
?處理。Vue3 使用 Proxy 來(lái)監(jiān)控?cái)?shù)據(jù)的變化。Proxy 是 ES6 中提供的功能,其作用為:用于定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數(shù)調(diào)用等)。相對(duì)于 ?Object.defineProperty()
?,其有以下特點(diǎn):
在 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)式。Proxy 實(shí)現(xiàn)的響應(yīng)式原理與 Vue2的實(shí)現(xiàn)原理相同,實(shí)現(xiàn)方式大同小異∶
在 Vue2 中,代碼是 Options API 風(fēng)格的,也就是通過(guò)填充 (option) data、methods、computed 等屬性來(lái)完成一個(gè) Vue 組件。這種風(fēng)格使得 Vue 相對(duì)于 React極為容易上手,同時(shí)也造成了幾個(gè)問(wèn)題:
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ā)者更多地靈活性
從React Hook的實(shí)現(xiàn)角度看,React Hook是根據(jù)useState調(diào)用的順序來(lái)確定下一次重渲染時(shí)的state是來(lái)源于哪個(gè)useState,所以出現(xiàn)了以下限制
而Composition API是基于Vue的響應(yīng)式系統(tǒng)實(shí)現(xiàn)的,與React Hook的相比
雖然Compositon API看起來(lái)比React Hook好用,但是其設(shè)計(jì)思想也是借鑒React Hook的。
從本質(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ā)效率。
虛擬DOM的解析過(guò)程:
(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)看一下它們重排重繪的性能消耗∶
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等。
在新老虛擬DOM對(duì)比時(shí):
在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í)比較。
標(biāo)識(shí)元素的身份,實(shí)現(xiàn)高效復(fù)用,在更新渲染時(shí)更加高效,準(zhǔn)確key隨元素移動(dòng)不會(huì)產(chǎn)生順序錯(cuò)誤。 vue 中 key 值的作用可以分為兩種情況來(lái)考慮:
key 是為 Vue 中 vnode 的唯一標(biāo)記,通過(guò)這個(gè) key,diff 操作可以更準(zhǔn)確、更快速
使用index 作為 key和沒(méi)寫基本上沒(méi)區(qū)別,因?yàn)椴还軘?shù)組的順序怎么顛倒,index 都是 0, 1, 2...這樣排列,導(dǎo)致 Vue 會(huì)復(fù)用錯(cuò)誤的舊子節(jié)點(diǎn),做很多額外的工作。
更多建議: