當(dāng)一個Vue實例創(chuàng)建時,Vue會遍歷data中的屬性,用 Object.defineProperty(vue3.0使用proxy )將它們轉(zhuǎn)為 getter/setter,并且在內(nèi)部追蹤相關(guān)依賴,在屬性被訪問和修改時通知變化。 每個組件實例都有相應(yīng)的 watcher 程序?qū)嵗?,它會在組件渲染的過程中把屬性記錄為依賴,之后當(dāng)依賴項的setter被調(diào)用時,會通知watcher重新計算,從而致使它關(guān)聯(lián)的組件得以更新。
Vue.js 是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數(shù)據(jù)變動時發(fā)布消息給訂閱者,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)。主要分為以下幾個步驟:
在對一些屬性進行操作時,使用這種方法無法攔截,比如通過下標(biāo)方式修改數(shù)組數(shù)據(jù)或者給對象新增屬性,這都不能觸發(fā)組件的重新渲染,因為 Object.defineProperty 不能攔截到這些操作。更精確的來說,對于數(shù)組而言,大部分操作都是攔截不到的,只是 Vue 內(nèi)部通過重寫函數(shù)的方式解決了這個問題。
在 Vue3.0 中已經(jīng)不使用這種方式了,而是通過使用 Proxy 對對象進行代理,從而實現(xiàn)數(shù)據(jù)劫持。使用Proxy 的好處是它可以完美的監(jiān)聽到任何方式的數(shù)據(jù)改變,唯一的缺點是兼容性的問題,因為 Proxy 是 ES6 的語法。
MVC、MVP 和 MVVM 是三種常見的軟件架構(gòu)設(shè)計模式,主要通過分離關(guān)注點的方式來組織代碼結(jié)構(gòu),優(yōu)化開發(fā)效率。
在開發(fā)單頁面應(yīng)用時,往往一個路由頁面對應(yīng)了一個腳本文件,所有的頁面邏輯都在一個腳本文件里。頁面的渲染、數(shù)據(jù)的獲取,對用戶事件的響應(yīng)所有的應(yīng)用邏輯都混合在一起,這樣在開發(fā)簡單項目時,可能看不出什么問題,如果項目變得復(fù)雜,那么整個文件就會變得冗長、混亂,這樣對項目開發(fā)和后期的項目維護是非常不利的。
(1)MVC
MVC 通過分離 Model、View 和 Controller 的方式來組織代碼結(jié)構(gòu)。其中 View 負(fù)責(zé)頁面的顯示邏輯,Model 負(fù)責(zé)存儲頁面的業(yè)務(wù)數(shù)據(jù),以及對相應(yīng)數(shù)據(jù)的操作。并且 View 和 Model 應(yīng)用了觀察者模式,當(dāng) Model 層發(fā)生改變的時候它會通知有關(guān) View 層更新頁面。Controller 層是 View 層和 Model 層的紐帶,它主要負(fù)責(zé)用戶與應(yīng)用的響應(yīng)操作,當(dāng)用戶與頁面產(chǎn)生交互的時候,Controller 中的事件觸發(fā)器就開始工作了,通過調(diào)用 Model 層,來完成對
Model 的修改,然后 Model 層再去通知 View 層更新。
(2)MVVM
MVVM 分為 Model、View、ViewModel:
Model和View并無直接關(guān)聯(lián),而是通過ViewModel來進行聯(lián)系的,Model和ViewModel之間有著雙向數(shù)據(jù)綁定的聯(lián)系。因此當(dāng)Model中的數(shù)據(jù)改變時會觸發(fā)View層的刷新,View中由于用戶交互操作而改變的數(shù)據(jù)也會在Model中同步。
這種模式實現(xiàn)了 Model和View的數(shù)據(jù)自動同步,因此開發(fā)者只需要專注于數(shù)據(jù)的維護操作即可,而不需要自己操作DOM。
(3)MVP
MVP 模式與 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用觀察者模式,來實現(xiàn)當(dāng) Model 層數(shù)據(jù)發(fā)生變化的時候,通知 View 層的更新。這樣 View 層和 Model 層耦合在一起,當(dāng)項目邏輯變得復(fù)雜的時候,可能會造成代碼的混亂,并且可能會對代碼的復(fù)用性造成一些問題。MVP 的模式通過使用 Presenter 來實現(xiàn)對 View 層和 Model 層的解耦。MVC 中的Controller 只知道 Model 的接口,因此它沒有辦法控制 View
層的更新,MVP 模式中,View 層的接口暴露給了 Presenter 因此可以在 Presenter 中將 Model 的變化和 View 的變化綁定在一起,以此來實現(xiàn) View 和 Model 的同步更新。這樣就實現(xiàn)了對 View 和 Model 的解耦,Presenter 還包含了其他的響應(yīng)邏輯。
對于Computed:
對于Watch:
當(dāng)想要執(zhí)行異步或者昂貴的操作以響應(yīng)不斷的變化時,就需要使用watch。
總結(jié):
運用場景:
可以將同一函數(shù)定義為一個 method 或者一個計算屬性。對于最終的結(jié)果,兩種方式是相同的
不同點:
slot又名插槽,是Vue的內(nèi)容分發(fā)機制,組件內(nèi)部的模板引擎使用slot元素作為承載分發(fā)內(nèi)容的出口。插槽slot是子組件的一個模板標(biāo)簽元素,而這一個標(biāo)簽元素是否顯示,以及怎么顯示是由父組件決定的。slot又分三類,默認(rèn)插槽,具名插槽和作用域插槽。
實現(xiàn)原理:當(dāng)子組件vm實例化時,獲取到父組件傳入的slot標(biāo)簽的內(nèi)容,存放在 vm.$slot
中,默認(rèn)插槽為 vm.$slot.default
,具名插槽為 vm.$slot.xxx
,xxx 為插槽名,當(dāng)組件執(zhí)行渲染函數(shù)時候,遇到slot標(biāo)簽,使用 $slot
中的內(nèi)容進行替換,此時可以為插槽傳遞數(shù)據(jù),若存在數(shù)據(jù),則可稱該插槽為作用域插槽。
根據(jù)過濾器的名稱,過濾器是用來過濾數(shù)據(jù)的,在Vue中使用 filters
來過濾數(shù)據(jù),filters
不會修改數(shù)據(jù),而是過濾數(shù)據(jù),改變用戶看到的輸出(計算屬性 computed
,方法 methods
都是通過修改數(shù)據(jù)來處理數(shù)據(jù)格式的輸出顯示)。
使用場景:
fliters
?過濾器來處理數(shù)據(jù)。過濾器是一個函數(shù),它會把表達式中的值始終當(dāng)作函數(shù)的第一個參數(shù)。過濾器用在插值表達式 {{ }}
和 v-bind
表達式 中,然后放在操作符“ |
”后面進行指示。
例如,在顯示金額,給商品價格添加單位:
<li>商品價格:{{item.price | filterPrice}}</li>
filters: {
filterPrice (price) {
return price ? ('¥' + price) : '--'
}
}
既然是要保持頁面的狀態(tài)(其實也就是組件的狀態(tài)),那么會出現(xiàn)以下兩種情況:
那么可以按照這兩種情況分別得到以下方法:
組件會被卸載:
(1)將狀態(tài)存儲在LocalStorage / SessionStorage
只需要在組件即將被銷毀的生命周期 componentWillUnmount
(react)中在 LocalStorage / SessionStorage 中把當(dāng)前組件的 state 通過 JSON.stringify() 儲存下來就可以了。在這里面需要注意的是組件更新狀態(tài)的時機。
比如從 B 組件跳轉(zhuǎn)到 A 組件的時候,A 組件需要更新自身的狀態(tài)。但是如果從別的組件跳轉(zhuǎn)到 B 組件的時候,實際上是希望 B 組件重新渲染的,也就是不要從 Storage 中讀取信息。所以需要在 Storage 中的狀態(tài)加入一個 flag 屬性,用來控制 A 組件是否讀取 Storage 中的狀態(tài)。
(2)路由傳值
通過 react-router 的 Link 組件的 prop —— to 可以實現(xiàn)路由間傳遞參數(shù)的效果。
在這里需要用到 state 參數(shù),在 B 組件中通過 history.location.state 就可以拿到 state 值,保存它。返回 A 組件時再次攜帶 state 達到路由狀態(tài)保持的效果。
組件不會被卸載:
(1)單頁面渲染
要切換的組件作為子組件全屏渲染,父組件中正常儲存頁面狀態(tài)。
除此之外,在Vue中,還可以是用keep-alive來緩存頁面,當(dāng)組件在keep-alive內(nèi)被切換時組件的activated、deactivated這兩個生命周期鉤子函數(shù)會被執(zhí)行
被包裹在keep-alive中的組件的狀態(tài)將會被保留:
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</kepp-alive>
router.js
{
path: '/',
name: 'xxx',
component: ()=>import('../src/views/xxx.vue'),
meta:{
keepAlive: true // 需要被緩存
}
},
.stop
?:等同于 JavaScript 中的 ?event.stopPropagation()
? ,防止事件冒泡;.prevent
? :等同于 JavaScript 中的 ?event.preventDefault()
? ,防止執(zhí)行預(yù)設(shè)的行為(如果事件可取消,則取消該事件,而不停止事件的進一步傳播);.capture
? :與事件冒泡的方向相反,事件捕獲由外到內(nèi);.self
? :只會觸發(fā)自己范圍內(nèi)的事件,不包含子元素;.once
? :只會觸發(fā)一次。(1)作用在表單元素上
動態(tài)綁定了 input 的 value 指向了 messgae 變量,并且在觸發(fā) input 事件的時候去動態(tài)把 message設(shè)置為目標(biāo)值:
<input v-model="sth" />
// 等同于
<input
v-bind:value="message"
v-on:input="message=$event.target.value"
>
//$event 指代當(dāng)前觸發(fā)的事件對象;
//$event.target 指代當(dāng)前觸發(fā)的事件對象的dom;
//$event.target.value 就是當(dāng)前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;
(2)作用在組件上
在自定義組件中,v-model 默認(rèn)會利用名為 value 的 prop和名為 input 的事件
本質(zhì)是一個父子組件通信的語法糖,通過prop和$.emit實現(xiàn)。因此父組件 v-model 語法糖本質(zhì)上可以修改為:
<child :value="message" @input="function(e){message = e}"></child>
在組件的實現(xiàn)中,可以通過 v-model屬性來配置子組件接收的prop名稱,以及派發(fā)的事件名稱。
例子:
// 父組件
<aa-input v-model="aa"></aa-input>
// 等價于
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>
// 子組件:
<input v-bind:value="aa" v-on:input="onmessage"></aa-input>
props:{value:aa,}
methods:{
onmessage(e){
$emit('input',e.target.value)
}
}
默認(rèn)情況下,一個組件上的v-model 會把 value 用作 prop且把 input 用作 event。但是一些輸入類型比如單選框和復(fù)選框按鈕可能想使用 value prop 來達到不同的目的。使用 model 選項可以回避這些情況產(chǎn)生的沖突。js 監(jiān)聽input 輸入框輸入數(shù)據(jù)改變,用oninput,數(shù)據(jù)改變以后就會立刻出發(fā)這個事件。通過input事件把數(shù)據(jù)$emit 出去,在父組件接受。父組件設(shè)置v-model的值為input $emit過來的值。
可以。v-model 實際上是一個語法糖,如:
<input v-model="searchText">
實際上相當(dāng)于:
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
用在自定義組件上也是同理:
<custom-input v-model="searchText">
相當(dāng)于:
<custom-input
v-bind:value="searchText"
v-on:input="searchText = $event"
></custom-input>
顯然,custom-input 與父組件的交互如下:
searchText
?變量傳入custom-input 組件,使用的 prop 名為 ?value
?;input
?的事件,父組件將接收到的值賦值給 ?searchText
?;所以,custom-input 組件的實現(xiàn)應(yīng)該類似于這樣:
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})
JavaScript中的對象是引用類型的數(shù)據(jù),當(dāng)多個實例引用同一個對象時,只要一個實例對這個對象進行操作,其他實例中的數(shù)據(jù)也會發(fā)生變化。
而在Vue中,更多的是想要復(fù)用組件,那就需要每個組件都有自己的數(shù)據(jù),這樣組件之間才不會相互干擾。
所以組件的數(shù)據(jù)不能寫成對象的形式,而是要寫成函數(shù)的形式。數(shù)據(jù)以函數(shù)返回值的形式定義,這樣當(dāng)每次復(fù)用組件的時候,就會返回一個新的data,也就是說每個組件都有自己的私有數(shù)據(jù)空間,它們各自維護自己的數(shù)據(jù),不會干擾其他組件的正常運行。
如果需要在組件切換的時候,保存一些組件的狀態(tài)防止多次渲染,就可以使用 keep-alive 組件包裹需要保存的組件。
(1)keep-alive
keep-alive有以下三個屬性:
注意:keep-alive 包裹動態(tài)組件時,會緩存不活動的組件實例。
主要流程
(2)keep-alive 的實現(xiàn)
const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正則,數(shù)組
export default {
name: 'keep-alive',
abstract: true, // 抽象組件,是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出現(xiàn)在父組件鏈中。
props: {
include: patternTypes, // 匹配的組件,緩存
exclude: patternTypes, // 不去匹配的組件,不緩存
max: [String, Number], // 緩存組件的最大實例數(shù)量, 由于緩存的是組件實例(vnode),數(shù)量過多的時候,會占用過多的內(nèi)存,可以用max指定上限
},
created() {
// 用于初始化緩存虛擬DOM數(shù)組和vnode的key
this.cache = Object.create(null)
this.keys = []
},
destroyed() {
// 銷毀緩存cache的組件實例
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// prune 削減精簡[v.]
// 去監(jiān)控include和exclude的改變,根據(jù)最新的include和exclude的內(nèi)容,來實時削減緩存的組件的內(nèi)容
this.$watch('include', (val) => {
pruneCache(this, (name) => matches(val, name))
})
this.$watch('exclude', (val) => {
pruneCache(this, (name) => !matches(val, name))
})
},
}
render函數(shù):
render () {
//
function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
const slot = this.$slots.default // 獲取默認(rèn)插槽
const vnode: VNode = getFirstComponentChild(slot)// 獲取第一個子組件
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 組件參數(shù)
if (componentOptions) { // 是否有組件參數(shù)
// check pattern
const name: ?string = getComponentName(componentOptions) // 獲取組件名
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
// 如果不匹配當(dāng)前組件的名字和include以及exclude
// 那么直接返回組件的實例
return vnode
}
const { cache, keys } = this
// 獲取這個組件的key
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// LRU緩存策略執(zhí)行
vnode.componentInstance = cache[key].componentInstance // 組件初次渲染的時候componentInstance為undefined
// make current key freshest
remove(keys, key)
keys.push(key)
// 根據(jù)LRU緩存策略執(zhí)行,將key從原來的位置移除,然后將這個key值放到最后面
} else {
// 在緩存列表里面沒有的話,則加入,同時判斷當(dāng)前加入之后,是否超過了max所設(shè)定的范圍,如果是,則去除
// 使用時間間隔最長的一個
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 將組件的keepAlive屬性設(shè)置為true
vnode.data.keepAlive = true // 作用:判斷是否要執(zhí)行組件的created、mounted生命周期函數(shù)
}
return vnode || (slot && slot[0])
}
keep-alive 具體是通過 cache 數(shù)組緩存所有組件的 vnode 實例。當(dāng) cache 內(nèi)原有組件被使用時會將該組件 key 從 keys 數(shù)組中刪除,然后 push 到 keys數(shù)組最后,以便清除最不常用組件。
實現(xiàn)步驟:
(3)keep-alive 本身的創(chuàng)建過程和 patch 過程
緩存渲染的時候,會根據(jù) vnode.componentInstance(首次渲染 vnode.componentInstance 為 undefined) 和 keepAlive 屬性判斷不會執(zhí)行組件的 created、mounted 等鉤子函數(shù),而是對緩存的組件執(zhí)行 patch 過程∶ 直接把緩存的 DOM 對象直接插入到目標(biāo)元素中,完成了數(shù)據(jù)更新的情況下的渲染過程。
首次渲染
// 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í)行的是組件更新的過程
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch 操作就不會在執(zhí)行組件的 mounted 和 created 生命周期函數(shù),而是直接將 DOM 插入
(4)LRU (least recently used)緩存策略
LRU 緩存策略∶ 從內(nèi)存中找出最久未使用的數(shù)據(jù)并置換新的數(shù)據(jù)。
LRU(Least rencently used)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù),其核心思想是"如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高"。 最常見的實現(xiàn)是使用一個鏈表保存緩存數(shù)據(jù),詳細(xì)算法實現(xiàn)如下∶
Vue 的 nextTick 其本質(zhì)是對 JavaScript 執(zhí)行原理 EventLoop 的一種應(yīng)用。
nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法來模擬對應(yīng)的微/宏任務(wù)的實現(xiàn),本質(zhì)是為了利用 JavaScript 的這些異步回調(diào)任務(wù)隊列來實現(xiàn) Vue 框架中自己的異步回調(diào)隊列。
nextTick 不僅是 Vue 內(nèi)部的異步隊列的調(diào)用方法,同時也允許開發(fā)者在實際項目中使用這個方法來滿足實際應(yīng)用中對 DOM 更新數(shù)據(jù)時機的后續(xù)邏輯處理
nextTick 是典型的將底層 JavaScript 執(zhí)行原理應(yīng)用到具體案例中的示例,引入異步更新隊列機制的原因∶
Vue采用了數(shù)據(jù)驅(qū)動視圖的思想,但是在一些情況下,仍然需要操作DOM。有時候,可能遇到這樣的情況,DOM1的數(shù)據(jù)發(fā)生了變化,而DOM2需要從DOM1中獲取數(shù)據(jù),那這時就會發(fā)現(xiàn)DOM2的視圖并沒有更新,這時就需要用到了 nextTick
了。
由于Vue的DOM操作是異步的,所以,在上面的情況中,就要將DOM2獲取數(shù)據(jù)的操作寫在 $nextTick
中。
this.$nextTick(() => {
// 獲取數(shù)據(jù)的操作...
})
所以,在以下情況下,會用到nextTick:
nextTick()
?的回調(diào)函數(shù)中。nextTick()
?的回調(diào)函數(shù)中。因為在created()鉤子函數(shù)中,頁面的DOM還未渲染,這時候也沒辦法操作DOM,所以,此時如果想要操作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>
點擊 button 會發(fā)現(xiàn),obj.b 已經(jīng)成功添加,但是視圖并未刷新。這是因為在Vue實例創(chuàng)建時,obj.b并未聲明,因此就沒有被Vue轉(zhuǎn)換為響應(yīng)式的屬性,自然就不會觸發(fā)視圖的更新,這時就需要使用Vue的全局 api $set():
addObjB () (
this.$set(this.obj, 'b', 'obj.b')
console.log(this.obj)
}
$set()方法相當(dāng)于手動的去把obj.b處理成一個響應(yīng)式的屬性,此時視圖也會跟著改變了。
在Vue中,對響應(yīng)式處理利用的是Object.defineProperty對數(shù)據(jù)進行攔截,而這個方法并不能監(jiān)聽到數(shù)組內(nèi)部變化,數(shù)組長度變化,數(shù)組的截取變化等,所以需要對這些操作進行hack,讓Vue能監(jiān)聽到其中的變化。
那Vue是如何實現(xiàn)讓這些數(shù)組方法實現(xiàn)元素的實時更新的呢,下面是Vue中對這些方法的封裝:
// 緩存數(shù)組原型
const arrayProto = Array.prototype;
// 實現(xiàn) arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要進行功能拓展的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
// 緩存原生數(shù)組方法
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 執(zhí)行并緩存原生數(shù)組功能
const result = original.apply(this, args);
// 響應(yīng)式處理
const ob = this.__ob__;
let inserted;
switch (method) {
// push、unshift會新增索引,所以要手動observer
case "push":
case "unshift":
inserted = args;
break;
// splice方法,如果傳入了第三個參數(shù),也會有索引加入,也要手動observer。
case "splice":
inserted = args.slice(2);
break;
}
//
if (inserted) ob.observeArray(inserted);// 獲取插入的值,并設(shè)置響應(yīng)式監(jiān)聽
// notify change
ob.dep.notify();// 通知依賴更新
// 返回原生數(shù)組方法的執(zhí)行結(jié)果
return result;
});
});
簡單來說就是,重寫了數(shù)組中的那些原生方法,首先獲取到這個數(shù)組的__ob__,也就是它的Observer對象,如果有新的值,就調(diào)用observeArray繼續(xù)對新的值觀察變化(也就是通過 target__proto__ == arrayMethods
來改變了數(shù)組實例的型),然后手動調(diào)用notify,通知渲染watcher,執(zhí)行update。
概念:
區(qū)別:
vue的模版編譯過程主要如下:template -> ast -> render函數(shù)
vue 在模版編譯版本的碼中會執(zhí)行 compileToFunctions 將template轉(zhuǎn)化為render函數(shù):
// 將模板編譯為render函數(shù)
const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
CompileToFunctions中的主要邏輯如下∶
(1)調(diào)用parse方法將template轉(zhuǎn)化為ast(抽象語法樹)
constast = parse(template.trim(), options)
AST元素節(jié)點總共三種類型:type為1表示普通元素、2為表達式、3為純文本
(2)對靜態(tài)節(jié)點做優(yōu)化
optimize(ast,options)
這個過程主要分析出哪些是靜態(tài)節(jié)點,給其打一個標(biāo)記,為后續(xù)更新渲染可以直接跳過靜態(tài)節(jié)點做優(yōu)化
深度遍歷AST,查看每個子樹的節(jié)點元素是否為靜態(tài)節(jié)點或者靜態(tài)節(jié)點根。如果為靜態(tài)節(jié)點,他們生成的DOM永遠(yuǎn)不會改變,這對運行時模板更新起到了極大的優(yōu)化作用。
(3)生成代碼
const code = generate(ast, options)
generate將ast抽象語法樹編譯成 render字符串并將靜態(tài)部分放到 staticRenderFns 中,最后通過 new Function(render)
生成render函數(shù)。
不會立即同步執(zhí)行重新渲染。Vue 實現(xiàn)響應(yīng)式并不是數(shù)據(jù)發(fā)生變化之后 DOM 立即變化,而是按一定的策略進行 DOM 的更新。Vue 在更新 DOM 時是異步執(zhí)行的。只要偵聽到數(shù)據(jù)變化, Vue 將開啟一個隊列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更。
如果同一個watcher被多次觸發(fā),只會被推入到隊列中一次。這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計算和 DOM 操作是非常重要的。然后,在下一個的事件循環(huán)tick中,Vue 刷新隊列并執(zhí)行實際(已去重的)工作。
(1)mixin 和 extends
mixin 和 extends均是用于合并、拓展組件的,兩者均通過 mergeOptions 方法實現(xiàn)合并。
(2)mergeOptions 的執(zhí)行過程
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ù)用和抽象的主要形式是組件。然而,有的情況下,你仍然需要對普通 DOM 元素進行底層操作,這時候就會用到自定義指令。
一般需要對DOM元素進行底層操作時使用,盡量只用來操作 DOM展示,不修改內(nèi)部的值。當(dāng)使用自定義指令直接修改 value 值時綁定v-model的值也不會同步更新;如必須修改可以在自定義指令中使用keydown事件,在vue組件中使用 change事件,回調(diào)中修改vue數(shù)據(jù);
(1)自定義指令基本內(nèi)容
Vue.directive("focus",{})
?directives:{focus:{}}
?bind:只調(diào)用一次,指令第一次綁定到元素時調(diào)用。在這里可以進行一次性的初始化設(shè)置。
inSerted:被綁定元素插入父節(jié)點時調(diào)用(僅保證父節(jié)點存在,但不一定已被插入文檔中)。
update:所在組件的VNode更新時調(diào)用,但是可能發(fā)生在其子VNode更新之前調(diào)用。指令的值可能發(fā)生了改變,也可能沒有。但是可以通過比較更新前后的值來忽略不必要的模板更新。
ComponentUpdate:指令所在組件的 VNode及其子VNode全部更新后調(diào)用。
unbind:只調(diào)用一次,指令與元素解綁時調(diào)用。
el:綁定元素
bing: 指令核心對象,描述指令全部信息屬性
name
value
oldValue
expression
arg
modifers
vnode 虛擬節(jié)點
oldVnode:上一個虛擬節(jié)點(更新鉤子函數(shù)中才有用)
(2)使用場景
(3)使用案例
初級應(yīng)用:
高級應(yīng)用:
子組件不可以直接改變父組件的數(shù)據(jù)。這樣做主要是為了維護父子組件的單向數(shù)據(jù)流。每次父級組件發(fā)生更新時,子組件中所有的 prop 都將會刷新為最新的值。如果這樣做了,Vue 會在瀏覽器的控制臺中發(fā)出警告。
Vue提倡單向數(shù)據(jù)流,即父級 props 的更新會流向子組件,但是反過來則不行。這是為了防止意外的改變父組件狀態(tài),使得應(yīng)用的數(shù)據(jù)流變得難以理解,導(dǎo)致數(shù)據(jù)流混亂。如果破壞了單向數(shù)據(jù)流,當(dāng)應(yīng)用復(fù)雜時,debug 的成本會非常高。
只能通過 $emit
派發(fā)一個自定義事件,父組件接收到后,由父組件修改。
在初始化 Vue 的每個組件時,會對組件的 data 進行初始化,就會將由普通對象變成響應(yīng)式對象,在這個過程中便會進行依賴收集的相關(guān)邏輯,如下所示∶
function defieneReactive (obj, key, val){
const dep = new Dep();
...
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {
if(Dep.target){
dep.depend();
...
}
return val
}
...
})
}
以上只保留了關(guān)鍵代碼,主要就是 const dep = new Dep()
實例化一個 Dep 的實例,然后在 get 函數(shù)中通過 dep.depend()
進行依賴收集。
(1)Dep
Dep是整個依賴收集的核心,其關(guān)鍵代碼如下:
class Dep {
static target;
subs;
constructor () {
...
this.subs = [];
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.sub, sub)
}
depend () {
if(Dep.target){
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subds.slice();
for(let i = 0;i < subs.length; i++){
subs[i].update()
}
}
}
Dep 是一個 class ,其中有一個關(guān) 鍵的靜態(tài)屬性 static,它指向了一個全局唯一 Watcher,保證了同一時間全局只有一個 watcher 被計算,另一個屬性 subs 則是一個 Watcher 的數(shù)組,所以 Dep 實際上就是對 Watcher 的管理,再看看 Watcher 的相關(guān)代碼∶
(2)Watcher
class Watcher {
getter;
...
constructor (vm, expression){
...
this.getter = expression;
this.get();
}
get () {
pushTarget(this);
value = this.getter.call(vm, vm)
...
return value
}
addDep (dep){
...
dep.addSub(this)
}
...
}
function pushTarget (_target) {
Dep.target = _target
}
Watcher 是一個 class,它定義了一些方法,其中和依賴收集相關(guān)的主要有 get、addDep 等。
(3)過程
在實例化 Vue 時,依賴收集的相關(guān)過程如下∶
初 始 化 狀 態(tài) initState , 這 中 間 便 會 通 過 defineReactive 將數(shù)據(jù)變成響應(yīng)式對象,其中的 getter 部分便是用來依賴收集的。
初始化最終會走 mount 過程,其中會實例化 Watcher ,進入 Watcher 中,便會執(zhí)行 this.get() 方法,
updateComponent = () => {
vm._update(vm._render())
}
new Watcher(vm, updateComponent)
get 方法中的 pushTarget 實際上就是把 Dep.target 賦值為當(dāng)前的 watcher。
this.getter.call(vm,vm),這里的 getter 會執(zhí)行 vm._render() 方法,在這個過程中便會觸發(fā)數(shù)據(jù)對象的 getter。那么每個對象值的 getter 都持有一個 dep,在觸發(fā) getter 的時候會調(diào)用 dep.depend() 方法,也就會執(zhí)行 Dep.target.addDep(this)。剛才 Dep.target 已經(jīng)被賦值為 watcher,于是便會執(zhí)行 addDep 方法,然后走到 dep.addSub() 方法,便將當(dāng)前的 watcher 訂閱到這個數(shù)據(jù)持有的 dep 的 subs 中,這個目的是為后續(xù)數(shù)據(jù)變化時候能通知到哪些 subs 做準(zhǔn)備。所以在 vm._render() 過程中,會觸發(fā)所有數(shù)據(jù)的 getter,這樣便已經(jīng)完成了一個依賴收集的過程。
相似之處:
不同之處 :
1)數(shù)據(jù)流
Vue默認(rèn)支持?jǐn)?shù)據(jù)雙向綁定,而React一直提倡單向數(shù)據(jù)流
2)虛擬DOM
Vue2.x開始引入"Virtual DOM",消除了和React在這方面的差異,但是在具體的細(xì)節(jié)還是有各自的特點。
3)組件化
React與Vue最大的不同是模板的編寫。
具體來講:React中render函數(shù)是支持閉包特性的,所以import的組件在render中可以直接調(diào)用。但是在Vue中,由于模板中使用的數(shù)據(jù)都必須掛在 this 上進行一次中轉(zhuǎn),所以 import 一個組件完了之后,還需要在 components 中再聲明下。
4)監(jiān)聽數(shù)據(jù)變化的實現(xiàn)原理不同
5)高階組件
react可以通過高階組件(HOC)來擴展,而Vue需要通過mixins來擴展。
高階組件就是高階函數(shù),而React的組件本身就是純粹的函數(shù),所以高階函數(shù)對React來說易如反掌。相反Vue.js使用HTML模板創(chuàng)建視圖組件,這時模板無法有效的編譯,因此Vue不能采用HOC來實現(xiàn)。
6)構(gòu)建工具
兩者都有自己的構(gòu)建工具:
7)跨平臺
kb
?;angular
?的特點,在數(shù)據(jù)操作方面更為簡單;react
?的優(yōu)點,實現(xiàn)了 ?html
?的封裝和重用,在構(gòu)建單頁面應(yīng)用方面有著獨特的優(yōu)勢;dom
? 操作是非常耗費性能的,不再使用原生的 ?dom
?操作節(jié)點,極大解放 ?dom
?操作,但具體操作的還是 ?dom
?不過是換了另一種方式;react
?而言,同樣是操作虛擬 ?dom
?,就性能而言, ?vue
?存在很大的優(yōu)勢。相同點: assets
和 static
兩個都是存放靜態(tài)資源文件。項目中所需要的資源文件圖片,字體圖標(biāo),樣式文件等都可以放在這兩個文件下,這是相同點
不相同點:assets
中存放的靜態(tài)資源文件在項目打包時,也就是運行 npm run build
時會將 assets
中放置的靜態(tài)資源文件進行打包上傳,所謂打包簡單點可以理解為壓縮體積,代碼格式化。而壓縮后的靜態(tài)資源文件最終也都會放置在 static
文件中跟著 index.html
一同上傳至服務(wù)器。static
中放置的靜態(tài)資源文件就不會要走打包壓縮格式化等流程,而是直接進入打包好的目錄,直接上傳至服務(wù)器。因為避免了壓縮直接進行上傳,在打包時會提高一定的效率,但是 static
中的資源文件由于沒有進行壓縮等操作,所以文件的體積也就相對于 assets
中打包后的文件提交較大點。在服務(wù)器中就會占據(jù)更大的空間。
建議: 將項目中 template
需要的樣式文件js文件等都可以放置在 assets
中,走打包這一流程。減少體積。而項目中引入的第三方的資源文件如 iconfoont.css
等文件可以放置在 static
中,因為這些引入的第三方文件已經(jīng)經(jīng)過處理,不再需要處理,直接上傳。
delete
?只是被刪除的元素變成了 ?empty/undefined
? 其他的元素的鍵值還是不變。Vue.delete
? 直接刪除了數(shù)組 改變了數(shù)組的鍵值。當(dāng)在項目中直接設(shè)置數(shù)組的某一項的值,或者直接設(shè)置對象的某個屬性值,這個時候,你會發(fā)現(xiàn)頁面并沒有更新。這是因為Object.defineProperty()限制,監(jiān)聽不到變化。
解決方式:
this.$set(this.arr, 0, "OBKoro1"); // 改變數(shù)組
this.$set(this.obj, "c", "OBKoro1"); // 改變對象
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
vue源碼里緩存了array的原型鏈,然后重寫了這幾個方法,觸發(fā)這幾個方法的時候會observer數(shù)據(jù),意思是使用這些方法不用再進行額外的操作,視圖自動進行更新。 推薦使用splice方法會比較好自定義,因為splice可以在數(shù)組的任何位置進行刪除/添加操作
vm.$set
的實現(xiàn)原理是:
vue中的模板template無法被瀏覽器解析并渲染,因為這不屬于瀏覽器的標(biāo)準(zhǔn),不是正確的HTML語法,所有需要將template轉(zhuǎn)化成一個JavaScript函數(shù),這樣瀏覽器就可以執(zhí)行這一個函數(shù)并渲染出對應(yīng)的HTML元素,就可以讓視圖跑起來了,這一個轉(zhuǎn)化的過程,就成為模板編譯。模板編譯又分三個階段,解析parse,優(yōu)化optimize,生成generate,最終生成可執(zhí)行函數(shù)render。
SSR也就是服務(wù)端渲染,也就是將Vue在客戶端把標(biāo)簽渲染成HTML的工作放在服務(wù)端完成,然后再把html直接返回給客戶端
SSR的優(yōu)勢:
SSR的缺點:
(1)編碼階段
(2)SEO優(yōu)化
(3)打包優(yōu)化
(4)用戶體驗
SPA( single-page application )僅在 Web 頁面初始化時加載相應(yīng)的 HTML、JavaScript 和 CSS。一旦頁面加載完成,SPA 不會因為用戶的操作而進行頁面的重新加載或跳轉(zhuǎn);取而代之的是利用路由機制實現(xiàn) HTML 內(nèi)容的變換,UI 與用戶的交互,避免頁面的重新加載。
優(yōu)點:
缺點:
對于 runtime 來說,只需要保證組件存在 render 函數(shù)即可,而有了預(yù)編譯之后,只需要保證構(gòu)建過程中生成 render 函數(shù)就可以。在 webpack 中,使用 vue-loader
編譯.vue文件,內(nèi)部依賴的 vue-template-compiler
模塊,在 webpack 構(gòu)建過程中,將template預(yù)編譯成 render 函數(shù)。與 react 類似,在添加了jsx的語法糖解析器 babel-plugin-transform-vue-jsx
之后,就可以直接手寫render函數(shù)。
所以,template和jsx的都是render的一種表現(xiàn)形式,不同的是:JSX相對于template而言,具有更高的靈活性,在復(fù)雜的組件中,更具有優(yōu)勢,而 template 雖然顯得有些呆滯。但是 template 在代碼結(jié)構(gòu)上更符合視圖與邏輯分離的習(xí)慣,更簡單、更直觀、更好維護。
使用vue開發(fā)時,在vue初始化之前,由于div是不歸vue管的,所以我們寫的代碼在還沒有解析的情況下會容易出現(xiàn)花屏現(xiàn)象,看到類似于{{message}}的字樣,雖然一般情況下這個時間很短暫,但是還是有必要讓解決這個問題的。
首先:在css里加上以下代碼:
[v-cloak] {
display: none;
}
如果沒有徹底解決問題,則在根元素加上 style="display: none;" :style="{display: 'block'}"
這個 API 很少用到,作用是擴展組件生成一個構(gòu)造器,通常會與 $mount
一起使用。
// 創(chuàng)建組件構(gòu)造器
let Component = Vue.extend({
template: '<div>test</div>'
})
// 掛載到 #app 上
new Component().$mount('#app')
// 除了上面的方式,還可以用來擴展已有的組件
let SuperComponent = Vue.extend(Component)
new SuperComponent({
created() {
console.log(1)
}
})
new SuperComponent().$mount('#app')
mixin
用于全局混入,會影響到每個組件實例,通常插件都是這樣做初始化的。
Vue.mixin({
beforeCreate() {
// ...邏輯
// 這種方式會影響到每個組件的 beforeCreate 鉤子函數(shù)
}
})
雖然文檔不建議在應(yīng)用中直接使用 mixin
,但是如果不濫用的話也是很有幫助的,比如可以全局混入封裝好的 ajax
或者一些工具函數(shù)等等。
mixins
應(yīng)該是最常使用的擴展組件的方式了。如果多個組件中有相同的業(yè)務(wù)邏輯,就可以將這些邏輯剝離出來,通過 mixins
混入代碼,比如上拉下拉加載數(shù)據(jù)這種邏輯等等。
另外需要注意的是 mixins
混入的鉤子函數(shù)會先于組件內(nèi)的鉤子函數(shù)執(zhí)行,并且在遇到同名選項的時候也會有選擇性的進行合并。
優(yōu)點:
缺點:
Vue 實例有?個完整的?命周期,也就是從開始創(chuàng)建、初始化數(shù)據(jù)、編譯模版、掛載Dom -> 渲染、更新 -> 渲染、卸載 等?系列過程,稱這是Vue的?命周期。
$el
? 屬性。this
?仍能獲取到實例。另外還有 keep-alive
獨有的生命周期,分別為 activated
和 deactivated
。用 keep-alive
包裹的組件在切換時不會進行銷毀,而是緩存到內(nèi)存中并執(zhí)行 deactivated
鉤子函數(shù),命中緩存渲染后會執(zhí)行 activated
鉤子函數(shù)。
加載渲染過程:
1.父組件 beforeCreate
2.父組件 created
3.父組件 beforeMount
4.子組件 beforeCreate
5.子組件 created
6.子組件 beforeMount
7.子組件 mounted
8.父組件 mounted
更新過程:
1. 父組件 beforeUpdate
2.子組件 beforeUpdate
3.子組件 updated
4.父組件 updated
銷毀過程:
1. 父組件 beforeDestroy
2.子組件 beforeDestroy
3.子組件 destroyed
4.父組件 destoryed
我們可以在鉤子函數(shù) created、beforeMount、mounted 中進行調(diào)用,因為在這三個鉤子函數(shù)中,data 已經(jīng)創(chuàng)建,可以將服務(wù)端端返回的數(shù)據(jù)進行賦值。
推薦在 created 鉤子函數(shù)中調(diào)用異步請求,因為在 created 鉤子函數(shù)中調(diào)用異步請求有以下優(yōu)點:
keep-alive是 Vue 提供的一個內(nèi)置組件,用來對組件進行緩存——在組件切換過程中將狀態(tài)保留在內(nèi)存中,防止重復(fù)渲染DOM。
如果為一個組件包裹了 keep-alive,那么它會多出兩個生命周期:deactivated、activated。同時,beforeDestroy 和 destroyed 就不會再被觸發(fā)了,因為組件不會被真正銷毀。
當(dāng)組件被換掉時,會被緩存到內(nèi)存中、觸發(fā) deactivated 生命周期;當(dāng)組件被切回來時,再去緩存里找這個組件、觸發(fā) activated鉤子函數(shù)。
組件通信的方式如下:
父組件通過 props
向子組件傳遞數(shù)據(jù),子組件通過 $emit
和父組件通信
props
?只能是父組件向子組件進行傳值,?props
?使得父子組件之間形成了一個單向下行綁定。子組件的數(shù)據(jù)會隨著父組件不斷更新。props
?可以顯示定義一個或一個以上的數(shù)據(jù),對于接收的數(shù)據(jù),可以是各種數(shù)據(jù)類型,同樣也可以傳遞一個函數(shù)。props
?屬性名規(guī)則:若在 ?props
?中使用駝峰形式,模板中需要使用短橫線的形式// 父組件
<template>
<div id="father">
<son :msg="msgData" :fn="myFunction"></son>
</div>
</template>
<script>
import son from "./son.vue";
export default {
name: father,
data() {
msgData: "父組件數(shù)據(jù)";
},
methods: {
myFunction() {
console.log("vue");
}
},
components: {
son
}
};
</script>
// 子組件
<template>
<div id="son">
<p>{{msg}}</p>
<button @click="fn">按鈕</button>
</div>
</template>
<script>
export default {
name: "son",
props: ["msg", "fn"]
};
</script>
$emit
?綁定一個自定義事件,當(dāng)這個事件被執(zhí)行的時就會將參數(shù)傳遞給父組件,而父組件通過 v-on監(jiān)聽并接收參數(shù)。// 父組件
<template>
<div class="section">
<com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
<p>{{currentIndex}}</p>
</div>
</template>
<script>
import comArticle from './test/article.vue'
export default {
name: 'comArticle',
components: { comArticle },
data() {
return {
currentIndex: -1,
articleList: ['紅樓夢', '西游記', '三國演義']
}
},
methods: {
onEmitIndex(idx) {
this.currentIndex = idx
}
}
}
</script>
//子組件
<template>
<div>
<div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
</div>
</template>
<script>
export default {
props: ['articles'],
methods: {
emitIndex(index) {
this.$emit('onEmitIndex', index) // 觸發(fā)父組件的方法,并傳遞參數(shù)index
}
}
}
</script>
eventBus
事件總線適用于父子組件、非父子組件等之間的通信,使用步驟如下:
(1)創(chuàng)建事件中心管理組件之間的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)發(fā)送事件
假設(shè)有兩個兄弟組件 firstCom
和 secondCom
:
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>
<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
components: { firstCom, secondCom }
}
</script>
在 firstCom
組件中發(fā)送事件:
<template>
<div>
<button @click="add">加法</button>
</div>
</template>
<script>
import {EventBus} from './event-bus.js' // 引入事件中心
export default {
data(){
return{
num:0
}
},
methods:{
add(){
EventBus.$emit('addition', {
num:this.num++
})
}
}
}
</script>
(3)接收事件
在 secondCom
組件中發(fā)送事件:
<template>
<div>求和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
count: 0
}
},
mounted() {
EventBus.$on('addition', param => {
this.count = this.count + param.num;
})
}
}
</script>
在上述代碼中,這就相當(dāng)于將 num
值存貯在了事件總線中,在其他組件中可以直接訪問。事件總線就相當(dāng)于一個橋梁,不用組件通過它來通信。
雖然看起來比較簡單,但是這種方法也有不變之處,如果項目過大,使用這種方式進行通信,后期維護起來會很困難。
這種方式就是Vue中的依賴注入,該方法用于父子組件之間的通信。當(dāng)然這里所說的父子不一定是真正的父子,也可以是祖孫組件,在層數(shù)很深的情況下,可以使用這種方法來進行傳值。就不用一層一層的傳遞了。
project / inject
是Vue提供的兩個鉤子,和 data
、methods
是同級的。并且 project
的書寫形式和 data
一樣。
project
?鉤子用來發(fā)送數(shù)據(jù)或方法inject
?鉤子用來接收數(shù)據(jù)或方法在父組件中:
provide() {
return {
num: this.num
};
}
在子組件中:
inject: ['num']
還可以這樣寫,這樣寫就可以訪問父組件中的所有屬性:
provide() {
return {
app: this
};
}
data() {
return {
num: 1
};
}
inject: ['app']
console.log(this.app.num)
注意: 依賴注入所提供的屬性是非響應(yīng)式的。
這種方式也是實現(xiàn)父子組件之間的通信。
ref
: 這個屬性用在子組件上,它的引用就指向了子組件的實例??梢酝ㄟ^實例來訪問組件的數(shù)據(jù)和方法。
在子組件中:
export default {
data () {
return {
name: 'JavaScript'
}
},
methods: {
sayHello () {
console.log('hello')
}
}
}
在父組件中:
<template>
<child ref="child"></component-a>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
mounted () {
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
}
}
</script>
$parent
?可以讓組件訪問父組件的實例(訪問的是上一級父組件的屬性和方法)$children
?可以讓組件訪問子組件的實例,但是,?$children
?并不能保證順序,并且訪問的數(shù)據(jù)也不是響應(yīng)式的。在子組件中:
<template>
<div>
<span>{{message}}</span>
<p>獲取父組件的值為: {{parentVal}}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Vue'
}
},
computed:{
parentVal(){
return this.$parent.msg;
}
}
}
</script>
在父組件中:
// 父組件中
<template>
<div class="hello_world">
<div>{{msg}}</div>
<child></child>
<button @click="change">點擊改變子組件值</button>
</div>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
data() {
return {
msg: 'Welcome'
}
},
methods: {
change() {
// 獲取到子組件
this.$children[0].message = 'JavaScript'
}
}
}
</script>
在上面的代碼中,子組件獲取到了父組件的 parentVal
值,父組件改變了子組件中 message
的值。
需要注意:
$parent
?訪問到的是上一級父組件的實例,可以使用 ?$root
?來訪問根組件的實例$children
?拿到的是所有的子組件的實例,它是一個數(shù)組,并且是無序的#app
?上拿 ?$parent
?得到的是 ?new Vue()
?的實例,在這實例上再拿 ?$parent
?得到的是 ?undefined
?,而在最底層的子組件拿 ?$children
?是個空數(shù)組$children
? 的值是數(shù)組,而 ?$parent
?是個對象
考慮一種場景,如果A是B組件的父組件,B是C組件的父組件。如果想要組件A給組件C傳遞數(shù)據(jù),這種隔代的數(shù)據(jù),該使用哪種方式呢?
如果是用 props/$emit
來一級一級的傳遞,確實可以完成,但是比較復(fù)雜;如果使用事件總線,在多人開發(fā)或者項目較大的時候,維護起來很麻煩;如果使用Vuex,的確也可以,但是如果僅僅是傳遞數(shù)據(jù),那可能就有點浪費了。
針對上述情況,Vue引入了 $attrs / $listeners
,實現(xiàn)組件之間的跨代通信。
先來看一下 inheritAttrs
,它的默認(rèn)值true,繼承所有的父組件屬性除 props
之外的所有屬性;inheritAttrs:false
只繼承class屬性 。
$attrs
?:繼承所有的父組件屬性(除了prop傳遞的屬性、class 和 style ),一般用在子組件的子元素上$listeners
?:該屬性是一個對象,里面包含了作用在這個組件上的所有監(jiān)聽器,可以配合 ?v-on="$listeners"
? 將所有的事件監(jiān)聽器指向這個組件的某個特定的子元素。(相當(dāng)于子組件繼承父組件的事件)A組件(APP.vue
):
<template>
<div id="app">
//此處監(jiān)聽了兩個事件,可以在B組件或者C組件中直接觸發(fā)
<child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
</div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
components: { Child1 },
methods: {
onTest1() {
console.log('test1 running');
},
onTest2() {
console.log('test2 running');
}
}
};
</script>
B組件(Child1.vue
):
<template>
<div class="child-1">
<p>props: {{pChild1}}</p>
<p>$attrs: {{$attrs}}</p>
<child2 v-bind="$attrs" v-on="$listeners"></child2>
</div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
props: ['pChild1'],
components: { Child2 },
inheritAttrs: false,
mounted() {
this.$emit('test1'); // 觸發(fā)APP.vue中的test1方法
}
};
</script>
C 組件 (Child2.vue
):
<template>
<div class="child-2">
<p>props: {{pChild2}}</p>
<p>$attrs: {{$attrs}}</p>
</div>
</template>
<script>
export default {
props: ['pChild2'],
inheritAttrs: false,
mounted() {
this.$emit('test2');// 觸發(fā)APP.vue中的test2方法
}
};
</script>
在上述代碼中:
$listeners
? 屬性$attrs
?屬性,C組件可以直接獲取到A組件中傳遞下來的props(除了B組件中props聲明的)(1)父子組件間通信
(2)兄弟組件間通信
(3)任意組件之間
如果業(yè)務(wù)邏輯復(fù)雜,很多組件之間需要同時處理一些公共的數(shù)據(jù),這個時候采用上面這一些方法可能不利于項目的維護。這個時候可以使用 vuex ,vuex 的思想就是將這一些公共的數(shù)據(jù)抽離出來,將它作為一個全局的變量來管理,然后其他組件就可以對這個公共數(shù)據(jù)進行讀寫操作,這樣達到了解耦的目的。
非懶加載:
import List from '@/components/list.vue'
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
(1)方案一(常用):使用箭頭函數(shù)+import動態(tài)加載
const List = () => import('@/components/list.vue')
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
(2)方案二:使用箭頭函數(shù)+require動態(tài)加載
const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/components/list'], resolve)
}
]
})
(3)方案三:使用webpack的require.ensure技術(shù),也可以實現(xiàn)按需加載。 這種情況下,多個路由指定相同的chunkName,會合并打包成一個js文件。
// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的寫法 這種是官方推薦的寫的 按模塊劃分懶加載
const router = new Router({
routes: [
{
path: '/list',
component: List,
name: 'list'
}
]
}))
Vue-Router有兩種模式:hash模式和history模式。默認(rèn)的路由模式是hash模式。
簡介: hash模式是開發(fā)中默認(rèn)的模式,它的URL帶著一個#,例如:http://www.abc.com/#/vue,它的hash值就是 #/vue
。
特點:hash值會出現(xiàn)在URL里面,但是不會出現(xiàn)在HTTP請求中,對后端完全沒有影響。所以改變hash值,不會重新加載頁面。這種模式的瀏覽器支持度很好,低版本的IE瀏覽器也支持這種模式。hash路由被稱為是前端路由,已經(jīng)成為SPA(單頁面應(yīng)用)的標(biāo)配。
原理: hash模式的主要原理就是onhashchange()事件:
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
}
使用onhashchange()事件的好處就是,在頁面的hash值發(fā)生變化時,無需向后端發(fā)起請求,window就可以監(jiān)聽事件的改變,并按規(guī)則加載相應(yīng)的代碼。除此之外,hash值變化對應(yīng)的URL都會被瀏覽器記錄下來,這樣瀏覽器就能實現(xiàn)頁面的前進和后退。雖然是沒有請求后端服務(wù)器,但是頁面的hash值和對應(yīng)的URL關(guān)聯(lián)起來了。
簡介: history模式的URL中沒有#,它使用的是傳統(tǒng)的路由分發(fā)模式,即用戶在輸入一個URL時,服務(wù)器會接收這個請求,并解析這個URL,然后做出相應(yīng)的邏輯處理。
特點: 當(dāng)使用history模式時,URL就像這樣:http://abc.com/user/id。相比hash模式更加好看。但是,history模式需要后臺配置支持。如果后臺沒有正確配置,訪問時會返回404。
API: history api可以分為兩大部分,切換歷史狀態(tài)和修改歷史狀態(tài):
pushState()
? 和 ?replaceState()
? 方法,這兩個方法應(yīng)用于瀏覽器的歷史記錄棧,提供了對歷史記錄進行修改的功能。只是當(dāng)他們進行修改時,雖然修改了url,但瀏覽器不會立即向后端發(fā)送請求。如果要做到改變url但又不刷新頁面的效果,就需要前端用上這兩個API。forward()
?、?back()
?、?go()
?三個方法,對應(yīng)瀏覽器的前進,后退,跳轉(zhuǎn)操作。雖然history模式丟棄了丑陋的#。但是,它也有自己的缺點,就是在刷新頁面的時候,如果沒有相應(yīng)的路由或資源,就會刷出404來。
如果想要切換到history模式,就要進行以下配置(后端也要進行配置):
const router = new VueRouter({
mode: 'history',
routes: [...]
})
調(diào)用 history.pushState() 相比于直接修改 hash,存在以下優(yōu)勢:
hash模式和history模式都有各自的優(yōu)勢和缺陷,還是要根據(jù)實際情況選擇性的使用。
(1)監(jiān)聽$route的變化
// 監(jiān)聽,當(dāng)路由發(fā)生變化的時候執(zhí)行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度觀察監(jiān)聽
deep: true
}
},
(2)window.location.hash讀取#值
window.location.hash 的值可讀可寫,讀取來判斷狀態(tài)是否改變,寫入時可以在不重載網(wǎng)頁的前提下,添加一條歷史訪問記錄。
(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ù)獲取
通過 $route.params.userid
獲取傳遞的值
(2)query方式
/router
?,也就是普通配置/route?id=123
?1)路由定義
//方式1:直接在router-link 標(biāo)簽上以對象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">檔案</router-link>
// 方式2:寫成按鈕以點擊事件形式
<button @click='profileClick'>我的</button>
profileClick(){
this.$router.push({
path: "/profile",
query: {
name: "kobi",
age: "28",
height: 198
}
});
}
2)跳轉(zhuǎn)方法
// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按鈕</router-link>
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按鈕</router-link>
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' + jsmes)
3)獲取參數(shù)
通過$route.query 獲取傳遞的值
一、Vue-Router導(dǎo)航守衛(wèi)
有的時候,需要通過路由來進行一些操作,比如最常見的登錄權(quán)限驗證,當(dāng)用戶滿足條件時,才讓其進入導(dǎo)航,否則就取消跳轉(zhuǎn),并跳到登錄頁面讓其登錄。
為此有很多種方法可以植入路由的導(dǎo)航過程:全局的,單個路由獨享的,或者組件級的
vue-router全局有三個路由鉤子;
具體使用∶
router.beforeEach((to, from, next) => {
let ifInfo = Vue.prototype.$common.getSession('userData'); // 判斷是否登錄的存儲信息
if (!ifInfo) {
// sessionStorage里沒有儲存user信息
if (to.path == '/') {
//如果是登錄頁面路徑,就直接next()
next();
} else {
//不然就跳轉(zhuǎn)到登錄
Message.warning("請重新登錄!");
window.location.href = Vue.prototype.$loginUrl;
}
} else {
return next();
}
})
router.afterEach((to, from) => {
// 跳轉(zhuǎn)之后滾動條回到頂部
window.scrollTo(0,0);
});
beforeEnter
如果不想全局配置守衛(wèi)的話,可以為某些路由單獨配置守衛(wèi),有三個參數(shù)∶ to、from、next
export default [
{
path: '/',
name: 'login',
component: login,
beforeEnter: (to, from, next) => {
console.log('即將進入登錄頁面')
next()
}
}
]
beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
這三個鉤子都有三個參數(shù)∶to、from、next
注意點,beforeRouteEnter組件內(nèi)還訪問不到this,因為該守衛(wèi)執(zhí)行前組件實例還沒有被創(chuàng)建,需要傳一個回調(diào)給 next來訪問,例如:
beforeRouteEnter(to, from, next) {
next(target => {
if (from.path == '/classProcess') {
target.isFromProcess = true
}
})
}
二、Vue路由鉤子在生命周期函數(shù)的體現(xiàn)
路由導(dǎo)航、keep-alive、和組件生命周期鉤子結(jié)合起來的,觸發(fā)順序,假設(shè)是從a組件離開,第一次進入b組件∶
location.href= /url
? 來跳轉(zhuǎn),簡單方便,但是刷新了頁面;history.pushState( /url )
? ,無刷新頁面,靜態(tài)跳轉(zhuǎn);router.push( /url )
? 來跳轉(zhuǎn),使用了 ?diff
?算法,實現(xiàn)了按需加載,減少了 dom 的消耗。其實使用 router 跳轉(zhuǎn)和使用 ?history.pushState()
? 沒什么差別的,因為vue-router就是用了 ?history.pushState()
? ,尤其是在history模式下。用法:query要用path來引入,params要用name來引入,接收參數(shù)都是類似的,分別是 this.$route.query.name
和 this.$route.params.name
。
url地址顯示:query更加類似于ajax中g(shù)et傳參,params則類似于post,說的再簡單一點,前者在瀏覽器地址欄中顯示參數(shù),后者則不顯示
注意:query刷新不會丟失query里面的數(shù)據(jù) params刷新會丟失 params里面的數(shù)據(jù)。
在前端技術(shù)早期,一個 url 對應(yīng)一個頁面,如果要從 A 頁面切換到 B 頁面,那么必然伴隨著頁面的刷新。這個體驗并不好,不過在最初也是無奈之舉——用戶只有在刷新頁面的情況下,才可以重新去請求數(shù)據(jù)。
后來,改變發(fā)生了——Ajax 出現(xiàn)了,它允許人們在不刷新頁面的情況下發(fā)起請求;與之共生的,還有“不刷新頁面即可更新頁面內(nèi)容”這種需求。在這樣的背景下,出現(xiàn)了 SPA(單頁面應(yīng)用)。
SPA極大地提升了用戶體驗,它允許頁面在不刷新的情況下更新頁面內(nèi)容,使內(nèi)容的切換更加流暢。但是在 SPA 誕生之初,人們并沒有考慮到“定位”這個問題——在內(nèi)容切換前后,頁面的 URL 都是一樣的,這就帶來了兩個問題:
為了解決這個問題,前端路由出現(xiàn)了。
前端路由可以幫助我們在僅有一個頁面的情況下,“記住”用戶當(dāng)前走到了哪一步——為 SPA 中的各個視圖匹配一個唯一標(biāo)識。這意味著用戶前進、后退觸發(fā)的新內(nèi)容,都會映射到不同的 URL 上去。此時即便他刷新頁面,因為當(dāng)前的 URL 可以標(biāo)識出他所處的位置,因此內(nèi)容也不會丟失。
那么如何實現(xiàn)這個目的呢?首先要解決兩個問題:
從這兩個問題來看,服務(wù)端已經(jīng)完全救不了這個場景了。所以要靠咱們前端自力更生,不然怎么叫“前端路由”呢?作為前端,可以提供這樣的解決思路:
Vuex 是一個專為 Vue.js 應(yīng)用程序開發(fā)的狀態(tài)管理模式。每一個 Vuex 應(yīng)用的核心就是 store(倉庫)?!皊tore” 基本上就是一個容器,它包含著你的應(yīng)用中大部分的狀態(tài) ( state )。
Vuex為Vue Components建立起了一個完整的生態(tài)圈,包括開發(fā)中的API調(diào)用一環(huán)。
(1)核心流程中的主要功能:
(2)各模塊在核心流程中的主要功能:
Vue Components
?∶ Vue組件。HTML頁面上,負(fù)責(zé)接收用戶操作等交互行為,執(zhí)行dispatch方法觸發(fā)對應(yīng)action進行回應(yīng)。dispatch
?∶操作行為觸發(fā)方法,是唯一能執(zhí)行action的方法。actions
?∶ 操作行為處理模塊。負(fù)責(zé)處理Vue Components接收到的所有交互行為。包含同步/異步操作,支持多個同名方法,按照注冊的順序依次觸發(fā)。向后臺API請求的操作就在這個模塊中進行,包括觸發(fā)其他action以及提交mutation的操作。該模塊提供了Promise的封裝,以支持action的鏈?zhǔn)接|發(fā)。commit
?∶狀態(tài)改變提交操作方法。對mutation進行提交,是唯一能執(zhí)行mutation的方法。mutations
?∶狀態(tài)改變操作方法。是Vuex修改state的唯一推薦方法,其他修改方式在嚴(yán)格模式下將會報錯。該方法只能進行同步操作,且方法名只能全局唯一。操作之中會有一些hook暴露出來,以進行state的監(jiān)控等。state
?∶ 頁面狀態(tài)管理容器對象。集中存儲Vuecomponents中data對象的零散數(shù)據(jù),全局唯一,以進行統(tǒng)一的狀態(tài)管理。頁面顯示所需的數(shù)據(jù)從該對象中進行讀取,利用Vue的細(xì)粒度數(shù)據(jù)響應(yīng)機制來進行高效的狀態(tài)更新。getters
?∶ state對象讀取方法。圖中沒有單獨列出該模塊,應(yīng)該被包含在了render中,Vue Components通過該方法讀取全局state對象。mutation中的操作是一系列的同步函數(shù),用于修改state中的變量的的狀態(tài)。當(dāng)使用vuex時需要通過commit來提交需要操作的內(nèi)容。mutation 非常類似于事件:每個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調(diào)函數(shù) (handler)。這個回調(diào)函數(shù)就是實際進行狀態(tài)更改的地方,并且它會接受 state 作為第一個參數(shù):
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
state.count++ // 變更狀態(tài)
}
}
})
當(dāng)觸發(fā)一個類型為 increment 的 mutation 時,需要調(diào)用此函數(shù):
store.commit('increment')
而Action類似于mutation,不同點在于:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Action 函數(shù)接受一個與 store 實例具有相同方法和屬性的 context 對象,因此你可以調(diào)用 context.commit 提交一個 mutation,或者通過 context.state 和 context.getters 來獲取 state 和 getters。
所以,兩者的不同點如下:
(1)最重要的區(qū)別
(2)應(yīng)用場景
(3)永久性
刷新頁面時vuex存儲的值會丟失,localstorage不會。
注意:對于不變的數(shù)據(jù)確實可以用localstorage可以代替vuex,但是當(dāng)兩個組件共用一個數(shù)據(jù)源(對象或數(shù)組)時,如果其中一個組件改變了該數(shù)據(jù)源,希望另一個組件響應(yīng)該變化時,localstorage無法做到,原因就是區(qū)別1。
(1)Redux 和 Vuex區(qū)別
通俗點理解就是,vuex 弱化 dispatch,通過commit進行 store狀態(tài)的一次更變;取消了action概念,不必傳入特定的 action形式進行指定變更;弱化reducer,基于commit參數(shù)直接對數(shù)據(jù)進行轉(zhuǎn)變,使得框架更加簡易;
(2)共同思想
本質(zhì)上:redux與vuex都是對mvvm思想的服務(wù),將數(shù)據(jù)從視圖中抽離的一種方案;
形式上:vuex借鑒了redux,將store作為全局的數(shù)據(jù)中心,進行mode管理;
由于傳參的方法對于多層嵌套的組件將會非常繁瑣,并且對于兄弟組件間的狀態(tài)傳遞無能為力。我們經(jīng)常會采用父子組件直接引用或者通過事件來變更和同步狀態(tài)的多份拷貝。以上的這些模式非常脆弱,通常會導(dǎo)致代碼無法維護。
所以需要把組件的共享狀態(tài)抽取出來,以一個全局單例模式管理。在這種模式下,組件樹構(gòu)成了一個巨大的"視圖",不管在樹的哪個位置,任何組件都能獲取狀態(tài)或者觸發(fā)行為。
另外,通過定義和隔離狀態(tài)管理中的各種概念并強制遵守一定的規(guī)則,代碼將會變得更結(jié)構(gòu)化且易維護。
有五種,分別是 State、 Getter、Mutation 、Action、 Module
在嚴(yán)格模式下,無論何時發(fā)生了狀態(tài)變更且不是由mutation函數(shù)引起的,將會拋出錯誤。這能保證所有的狀態(tài)變更都能被調(diào)試工具跟蹤到。
在Vuex.Store 構(gòu)造器選項中開啟,如下
const store = new Vuex.Store({
strict:true,
})
使用mapGetters輔助函數(shù), 利用對象展開運算符將getter混入computed 對象中
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)測機制的改變
(2)只能監(jiān)測屬性,不能監(jiān)測對象
(3)模板
(4)對象式的組件聲明方式
(5)其它方面的更改
Vue 在實例初始化時遍歷 data 中的所有屬性,并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter。這樣當(dāng)追蹤數(shù)據(jù)發(fā)生變化時,setter 會被自動調(diào)用。
Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的原因。
但是這樣做有以下問題:
$set
? 來調(diào)用 ?Object.defineProperty()
?處理。Vue3 使用 Proxy 來監(jiān)控數(shù)據(jù)的變化。Proxy 是 ES6 中提供的功能,其作用為:用于定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數(shù)調(diào)用等)。相對于 ?Object.defineProperty()
?,其有以下特點:
在 Vue2 中, 0bject.defineProperty 會改變原始數(shù)據(jù),而 Proxy 是創(chuàng)建對象的虛擬表示,并提供 set 、get 和 deleteProperty 等處理器,這些處理器可在訪問或修改原始對象上的屬性時進行攔截,有以下特點∶
Vue.$set
? 或 ?Vue.$delete
? 觸發(fā)響應(yīng)式。Proxy 實現(xiàn)的響應(yīng)式原理與 Vue2的實現(xiàn)原理相同,實現(xiàn)方式大同小異∶
在 Vue2 中,代碼是 Options API 風(fēng)格的,也就是通過填充 (option) data、methods、computed 等屬性來完成一個 Vue 組件。這種風(fēng)格使得 Vue 相對于 React極為容易上手,同時也造成了幾個問題:
this
?上下文,Vue 背后的一些小技巧使得 Vue 組件的開發(fā)看起來與 JavaScript 的開發(fā)原則相悖,比如在 ?methods
?中的 ?this
?竟然指向組件實例來不指向 ?methods
?所在的對象。這也使得 TypeScript 在Vue2 中很不好用。于是在 Vue3 中,舍棄了 Options API,轉(zhuǎn)而投向 Composition API。Composition API本質(zhì)上是將 Options API 背后的機制暴露給用戶直接使用,這樣用戶就擁有了更多的靈活性,也使得 Vue3 更適合于 TypeScript 結(jié)合。
如下,是一個使用了 Vue Composition API 的 Vue3 組件:
<template>
<button @click="increment">
Count: {{ count }}
</button>
</template>
<script>
// Composition API 將組件屬性暴露為函數(shù),因此第一步是導(dǎo)入所需的函數(shù)
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
// 使用 ref 函數(shù)聲明了稱為 count 的響應(yīng)屬性,對應(yīng)于Vue2中的data函數(shù)
const count = ref(0)
// Vue2中需要在methods option中聲明的函數(shù),現(xiàn)在直接聲明
function increment() {
count.value++
}
// 對應(yīng)于Vue2中的mounted聲明周期
onMounted(() => console.log('component mounted!'))
return {
count,
increment
}
}
}
</script>
顯而易見,Vue Composition API 使得 Vue3 的開發(fā)風(fēng)格更接近于原生 JavaScript,帶給開發(fā)者更多地靈活性
從React Hook的實現(xiàn)角度看,React Hook是根據(jù)useState調(diào)用的順序來確定下一次重渲染時的state是來源于哪個useState,所以出現(xiàn)了以下限制
而Composition API是基于Vue的響應(yīng)式系統(tǒng)實現(xiàn)的,與React Hook的相比
雖然Compositon API看起來比React Hook好用,但是其設(shè)計思想也是借鑒React Hook的。
從本質(zhì)上來說,Virtual Dom是一個JavaScript對象,通過對象的方式來表示DOM結(jié)構(gòu)。將頁面的狀態(tài)抽象為JS對象的形式,配合不同的渲染工具,使跨平臺渲染成為可能。通過事務(wù)處理機制,將多次DOM修改的結(jié)果一次性的更新到頁面上,從而有效的減少頁面渲染的次數(shù),減少修改DOM的重繪重排次數(shù),提高渲染性能。
虛擬DOM是對DOM的抽象,這個對象是更加輕量級的對 DOM的描述。它設(shè)計的最初目的,就是更好的跨平臺,比如Node.js就沒有DOM,如果想實現(xiàn)SSR,那么一個方式就是借助虛擬DOM,因為虛擬DOM本身是js對象。 在代碼渲染到頁面之前,vue會把代碼轉(zhuǎn)換成一個對象(虛擬 DOM)。以對象的形式來描述真實DOM結(jié)構(gòu),最終渲染到頁面。在每次數(shù)據(jù)發(fā)生變化前,虛擬DOM都會緩存一份,變化之時,現(xiàn)在的虛擬DOM會與緩存的虛擬DOM進行比較。在vue內(nèi)部封裝了diff算法,通過這個算法來進行比較,渲染時修改改變的變化,原先沒有發(fā)生改變的通過原先的數(shù)據(jù)進行渲染。
另外現(xiàn)代前端框架的一個基本要求就是無須手動操作DOM,一方面是因為手動操作DOM無法保證程序性能,多人協(xié)作的項目中如果review不嚴(yán)格,可能會有開發(fā)者寫出性能較低的代碼,另一方面更重要的是省略手動DOM操作可以大大提高開發(fā)效率。
虛擬DOM的解析過程:
(1)保證性能下限,在不進行手動優(yōu)化的情況下,提供過得去的性能
看一下頁面渲染的流程:解析HTML -> 生成DOM -> 生成 CSSOM -> Layout -> Paint -> Compiler
下面對比一下修改DOM時真實DOM操作和Virtual DOM的過程,來看一下它們重排重繪的性能消耗∶
Virtual DOM的更新DOM的準(zhǔn)備工作耗費更多的時間,也就是JS層面,相比于更多的DOM操作它的消費是極其便宜的。尤雨溪在社區(qū)論壇中說道∶ 框架給你的保證是,你不需要手動優(yōu)化的情況下,依然可以給你提供過得去的性能。
(2)跨平臺
Virtual DOM本質(zhì)上是JavaScript的對象,它可以很方便的跨平臺操作,比如服務(wù)端渲染、uniapp等。
在新老虛擬DOM對比時:
在diff中,只對同層的子節(jié)點進行比較,放棄跨級的節(jié)點比較,使得時間復(fù)雜從O(n3)降低值O(n),也就是說,只有當(dāng)新舊children都為多個子節(jié)點時才需要用核心的Diff算法進行同層級比較。
標(biāo)識元素的身份,實現(xiàn)高效復(fù)用,在更新渲染時更加高效,準(zhǔn)確key隨元素移動不會產(chǎn)生順序錯誤。 vue 中 key 值的作用可以分為兩種情況來考慮:
key 是為 Vue 中 vnode 的唯一標(biāo)記,通過這個 key,diff 操作可以更準(zhǔn)確、更快速
使用index 作為 key和沒寫基本上沒區(qū)別,因為不管數(shù)組的順序怎么顛倒,index 都是 0, 1, 2...這樣排列,導(dǎo)致 Vue 會復(fù)用錯誤的舊子節(jié)點,做很多額外的工作。
更多建議: