最近項目中有小伙伴找到我,問我“為啥他寫的頁面第一次進去可以觸發(fā) onCreate
函數(shù)拒秘,第二次再進的時候就不觸發(fā)了呢皇钞?”(因為我們項目是一個大型的項目嘿架,每個開發(fā)可能只接觸到自己開發(fā)的一小部分),然后我就說你可以試著在 activated
鉤子函數(shù)中做處理栅屏,然后他又接著問我“activated 鉤子函數(shù)又是怎么調(diào)用的呢飘千?”,ok栈雳!這小子是問上癮了护奈,我們下面就來詳細解析一下。
keep-alive
<keep-alive>
包裹動態(tài)組件時哥纫,會緩存不活動的組件實例霉旗,而不是銷毀它們。和 <transition>
相似磺箕,<keep-alive>
是一個抽象組件:它自身不會渲染一個 DOM 元素奖慌,也不會出現(xiàn)在組件的父組件鏈中。
當組件在 <keep-alive>
內(nèi)被切換松靡,它的 activated
和 deactivated
這兩個生命周期鉤子函數(shù)將會被對應執(zhí)行简僧。
在 2.2.0 及其更高版本中,
activated
和deactivated
將會在<keep-alive>
樹內(nèi)的所有嵌套組件中觸發(fā)雕欺。
主要用于保留組件狀態(tài)或避免重新渲染岛马。
為了更好的來解析 <keep-alive>
,我們 copy 到一份源碼(vue@^2.6.10)屠列,vue/src/core/components/keep-alive.js
:
/* @flow */
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
type VNodeCache = { [key: string]: ?VNode };
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
const patternTypes: Array<Function> = [String, RegExp, Array]
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot) // 獲取第一個子節(jié)點
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions) // 獲取節(jié)點的名稱
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name)) // 不在范圍內(nèi)的節(jié)點將不會被緩存
) {
return vnode
}
const { cache, keys } = this
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 // 獲取緩存的 key 值
if (cache[key]) { // 如果有緩存就使用緩存
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else { // 沒有緩存就將當前節(jié)點加入到緩存
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) { // 如果緩存超過最大限制將不再緩存
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true // 標記該節(jié)點為 keepAlive 類型
}
return vnode || (slot && slot[0])
}
}
其實從源碼我們可以看到啦逆,代碼并沒有多少,還是比較簡單的笛洛,下面我們用一下 keep-alive
組件夏志。
Props
從源碼中我們可以看到,<keep-alive>
組件有三個屬性:
-
include
- 字符串或正則表達式苛让。只有名稱匹配的組件會被緩存沟蔑。 -
exclude
- 字符串或正則表達式。任何名稱匹配的組件都不會被緩存狱杰。 -
max
- 數(shù)字瘦材。最多可以緩存多少組件實例。
下面我們結(jié)合 Demo 來分析一下仿畸。
我們直接用 vue-cli
創(chuàng)建一個簡單的 vue 項目食棕,取名為 keep-alive-demo
:
vue create keep-alive-demo
然后選一下 Router
后一路回車:
我們修改一下 App.vue
文件:
<template>
<div id="app">
<router-view/>
</div>
</template>
<style>
#app {
text-align: center;
}
</style>
然后 views
目錄創(chuàng)建一個 A
組件當作 頁面 A
:
<template>
<div class="about">
<h1>我是 a 頁面</h1>
<router-link to="/pageB">點我跳轉(zhuǎn)到 b 頁面</router-link>
</div>
</template>
<script>
import LifeRecycle from "../life-recycle";
export default {
name: "page-a",
mixins:[LifeRecycle]
}
</script>
A 頁面很簡單朗和,里面一個按鈕鏈接到了 B 頁面。為了更好的顯示每個組件的生命周期簿晓,我們?yōu)槊總€頁面添加了一個 mixin
:
export default {
computed: {
name(){
return this.$options.name;
}
},
created(){
console.log("created--->"+this.name);
},
activated() {
console.log("activated--->"+this.name);
},
deactivated() {
console.log("deactivated--->"+this.name);
},
destroyed() {
console.log("destoryed--->"+this.name);
}
}
直接 copy 一份 A.vue
代碼創(chuàng)建一個 頁面 B
:
<template>
<div class="about">
<h1>我是 b 頁面</h1>
</div>
</template>
<script>
import LifeRecycle from "../life-recycle";
export default {
name: "page-b",
mixins:[LifeRecycle]
}
</script>
然后修改一下 views/Home.vue
:
<template>
<div class="home">
<h1>我是首頁</h1>
<router-link to="/pageA">點我跳轉(zhuǎn)到 a 頁面</router-link>
</div>
</template>
<script>
import LifeRecycle from "../life-recycle";
export default {
name: 'home',
mixins: [LifeRecycle]
}
</script>
給一個按鈕直接鏈接到了 頁面 A
眶拉。
最后我們修改一下 router.js
:
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
export default new Router({
mode: "history",
routes: [
{
path: '/',
name: 'home',
component: Home,
},
{
path: "/pageA",
name: "pageA",
component: () => import(/* webpackChunkName: "about" */ './views/A.vue')
},
{
path: "/pageB",
name: "pageB",
component: () => import(/* webpackChunkName: "about" */ './views/B.vue')
}
]
})
代碼很簡單,我就不詳細解析了抢蚀,一個簡單的 SPA
(單頁面應用) 就搭建完成了镀层,三個平級的頁面 home
、pageA
皿曲、pageB
唱逢。
我們試著運行一下項目:
npm run serve
可以看到:
-
首頁打開
home
頁面created--->home
直接觸發(fā)了
home
頁面的created
方法。 home
頁面 --->pageA
頁面
created--->page-a
destoryed--->home
home
頁面觸發(fā)了 destoryed
直接銷毀了屋休,然后觸發(fā)了pageA
頁面的 created
方法坞古。
-
pageA
頁面 --->pageB
頁面
created--->page-b
destoryed--->page-a
pageA
頁面觸發(fā)了 destoryed
直接銷毀了,然后觸發(fā)了pageB
頁面的 created
方法劫樟。
-
pageB
頁面返回created--->page-a destoryed--->page-b
pageB
頁面觸發(fā)了destoryed
直接銷毀了痪枫,然后觸發(fā)了pageA
頁面的created
方法。 -
pageA
頁面返回created--->home destoryed--->page-a
pageA
頁面觸發(fā)了destoryed
直接銷毀了叠艳,然后觸發(fā)了home
頁面的created
方法奶陈。效果是沒問題的,但是作為一個
SPA
的項目附较,這種用戶體驗肯定是不友好的吃粒,試想一下,你現(xiàn)在在一個 app 的首頁瀏覽頁面拒课,然后滑呀滑呀徐勃,滑動了很長的頁面好不容易看到了一個自己感興趣的東西,然后點擊查看詳情離開了首頁早像,再回到首頁時候肯定是想停留在之前瀏覽器的地方僻肖,而不是說重新又打開一個新的首頁,又要滑半天卢鹦,這種體驗肯定是不好的臀脏,而且也有點浪費資源,所以下面我們用一下<keep-alive>
把首頁緩存起來冀自。我們修改一下
App.vue
文件:<template> <div id="app"> <keep-alive> <router-view/> </keep-alive> </div> </template>
可以看到揉稚,我們添加了一個
<keep-alive>
組件,然后再次之前的操作:-
首頁打開
home
頁面created--->home activated--->home
直接觸發(fā)了
home
頁面的created
方法凡纳。 home
頁面 --->pageA
頁面
created--->page-a deactivated--->home activated--->page-a
home
頁面觸發(fā)了deactivated
變成非活躍狀態(tài),然后觸發(fā)了pageA
頁面的activated
方法帝蒿。-
pageA
頁面 --->pageB
頁面
created--->page-b deactivated--->page-a activated--->page-b
pageA
頁面觸發(fā)了deactivated
變成非活躍狀態(tài)荐糜,然后觸發(fā)了pageB
頁面的activated
方法。-
pageB
頁面返回deactivated--->page-b activated--->page-a
pageB
頁面觸發(fā)了deactivated
變成非活躍狀態(tài),然后觸發(fā)了pageA
頁面的activated
方法暴氏。 -
pageA
頁面返回deactivated--->page-a activated--->home
-
細心的童鞋應該已經(jīng)發(fā)現(xiàn)區(qū)別了吧延塑?每個頁面的 destoryed
不觸發(fā)了,替換成了 deactivated
答渔,然后第一次創(chuàng)建頁面的時候除了之前的 created
還多了一個 activated
方法关带。
是的!當我們加了<keep-alive>
組件后沼撕,所有頁面都被緩存起來了宋雏,但是我們只需要緩存的是 home
頁面,我們該怎么做呢务豺?
-
利用
include
屬性規(guī)定緩存的范圍我們修改一下
App.vue
給<keep-alive>
組件添加include
屬性:<keep-alive :include="['home']"> <router-view/> </keep-alive>
include
可以是一個字符串數(shù)組磨总,也可以是一個正則表達式,匹配的就是組件的名字笼沥,比如這里的home
蚪燕,其實就是home
組件的名稱:... export default { name: 'home', mixins: [LifeRecycle] } ...
-
利用
exclude
屬性規(guī)定不緩存的范圍這個剛好跟
include
屬性相反,我們可以修改一下App.vue
給<keep-alive>
組件添加exclude
屬性:.. <keep-alive :exclude="/page-/"> <router-view/> </keep-alive> ...
到這里我們思考一個問題奔浅,<keep-alive>
是會幫我們緩存組件馆纳,但是緩存的數(shù)量小倒還好,數(shù)量大了就有點得不償失了汹桦,所以 vue 考慮到這個情況了鲁驶,然后給<keep-alive>
添加了一個 max
屬性,比如我們只需要緩存一個頁面营勤,我們只需要設置 :max=1
即可:
..
<template>
<div id="app">
<keep-alive :max="1">
<router-view/>
</keep-alive>
</div>
</template>
...
<keep-alive>
每次會緩存最新的那個頁面:
-
首頁打開
home
頁面created--->home activated--->home
直接觸發(fā)了
home
頁面的created
方法新創(chuàng)建了一個頁面灵嫌,然后調(diào)用了activated
方法激活了當前頁面。 -
home
頁面 --->pageA
頁面created--->page-a deactivated--->home activated--->page-a
home
頁面觸發(fā)了deactivated
變成了非活躍狀態(tài)葛作,然后觸發(fā)了pageA
頁面的created
方法新創(chuàng)建了一個頁面寿羞,然后調(diào)用了activated
方法激活了當前頁面。 -
pageA
頁面點擊返回created--->home deactivated--->page-a activated--->home
pageA
頁面觸發(fā)了deactivated
變成了非活躍狀態(tài)赂蠢,然后觸發(fā)了home
頁面的created
方法新創(chuàng)建了一個頁面绪穆,然后調(diào)用了activated
方法激活了當前頁面。
當緩存頁面的個數(shù)大于最大限制的時候虱岂,每次都移除數(shù)據(jù)的第 0 個位置的緩存玖院,源碼為:
// 如果緩存數(shù) > 最大緩存數(shù),移除緩存數(shù)組的第 0 位置數(shù)據(jù)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
...
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key] // 獲取需要移除的緩存頁面
if (cached && (!current || cached.tag !== current.tag)) { // 如果當前頁面跟緩存的頁面不一致的時候
// 觸發(fā)移除的緩存頁面的 destroy 方法
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
比如當 :max="2"
的時候第岖,home ---> pageA ---> pageB难菌,當進入 pageB 的時候,home 頁面就會被銷毀蔑滓,會觸發(fā) home 頁面的 destroyed
方法郊酒。
到這里 <keep-alive>
組件的基本用法我們算是 ok 了遇绞,我們解析來分析一下項目中會經(jīng)常遇到的一些問題。
activated 生命周期
通過上面的 demo 我們可以知道燎窘,當頁面被激活的時候會觸發(fā)當前頁面的 activated
方法摹闽,那么 vue 是在什么時候才會去觸發(fā)這個方法呢?
我們找到 vue 源碼位置 /vue/src/core/vdom/create-component.js
:
...
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// 在更新過程中褐健,一個緩存的頁面的子組件可能還會改變付鹿,
// 當前的子組件并不一定就是最后的子組件,所以這個時候去調(diào)用 activaved 方法會不準確
// 當頁面都組件更新完畢之后再去調(diào)用蚜迅。
queueActivatedComponent(componentInstance)
} else {
// 遞歸激活所有子組件
activateChildComponent(componentInstance, true /* direct */)
}
}
},
...
export function activateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
// 先循環(huán)調(diào)用所有子組件的 activated 方法
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
// 再調(diào)用當前組件的 activated 方法
callHook(vm, 'activated')
}
}
當前 vnode
節(jié)點被插入的時候會判斷當前 vnode
節(jié)點 data
上是不是有 keepAlive
標記舵匾,有的話就會激活自身和自己所有的子組件,通過源碼我們還發(fā)現(xiàn)慢叨,當組件第一次創(chuàng)建的時候 activated
方法是在 mounted
方法之后執(zhí)行纽匙。
deactivated 生命周期
通過上面的 demo 我們可以知道,當頁面被隱藏的時候會觸發(fā)當前頁面的 deactivated
方法拍谐,那么 vue 是在什么時候才會去觸發(fā)這個方法呢烛缔?
跟 activated
方法一樣,我們找到 vue 源碼位置 /vue/src/core/vdom/create-component.js
:
...
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
...
export function deactivateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = true
if (isInInactiveTree(vm)) {
return
}
}
if (!vm._inactive) {
vm._inactive = true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')
}
}
當前vnode
節(jié)點被銷毀的時候轩拨,會判斷當前節(jié)點是不是有 keepAlive
標記践瓷,有的話就不會直接調(diào)用組件的 destroyed
了,而是直接調(diào)用組件的 deactivated
方法亡蓉。
那么節(jié)點的 keepAlive
是啥時候被標記的呢晕翠?還記得我們的 <keep-alive>
組件的源碼不?
vue/src/core/components/keep-alive.js
:
...
render () {
...
vnode.data.keepAlive = true // 標記該節(jié)點為 keepAlive 類型
}
return vnode || (slot && slot[0])
}
...
ok砍濒!看到這里是不是就一目了然了呢淋肾?
router-view
router-view
組件的基本用法跟原理我就不在這里解析了,感興趣的童鞋可以去看 官網(wǎng) 爸邢,也可以去看我之前的一些文章 前端入門之(vue-router全解析二)樊卓。
我們看一下router-view
組件中的源碼:
import { warn } from '../util/warn'
import { extend } from '../util/misc'
import { handleRouteEntered } from '../util/route'
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// used by devtools to display a router-view badge
data.routerView = true
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
const h = parent.$createElement
const name = props.name
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
if (inactive) {
const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
// #2301
// pass props
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
return h(cachedComponent, data, children)
} else {
// render previous empty view
return h()
}
}
const matched = route.matched[depth]
const component = matched && matched.components[name]
// render empty node if no matched route or no config component
if (!matched || !component) {
cache[name] = null
return h()
}
// cache component
cache[name] = { component }
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// register instance in init hook
// in case kept-alive component be actived when routes changed
data.hook.init = (vnode) => {
if (vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
// if the route transition has already been confirmed then we weren't
// able to call the cbs during confirmation as the component was not
// registered yet, so we call it here.
handleRouteEntered(route)
}
const configProps = matched.props && matched.props[name]
// save route and configProps in cache
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
return h(component, data, children)
}
}
function fillPropsinData (component, data, route, configProps) {
// resolve props
let propsToPass = data.props = resolveProps(route, configProps)
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass)
// pass non-declared props as attrs
const attrs = data.attrs = data.attrs || {}
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key]
delete propsToPass[key]
}
}
}
}
function resolveProps (route, config) {
switch (typeof config) {
case 'undefined':
return
case 'object':
return config
case 'function':
return config(route)
case 'boolean':
return config ? route.params : undefined
default:
if (process.env.NODE_ENV !== 'production') {
warn(
false,
`props in "${route.path}" is a ${typeof config}, ` +
`expecting an object, function or boolean.`
)
}
}
}
很簡單,我就不一一解析了杠河,我們重點看一下 render
方法中的這一段代碼:
...
render (_, { props, children, parent, data }) {
...
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
// 獲取當前頁面的層級數(shù)(嵌套路由情況)
if (vnodeData.routerView) {
depth++
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
// 如果父組件為非激活狀態(tài)并且是被緩存的時候碌尔,就去渲染之前的組件
if (inactive) {
const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
// #2301
// pass props
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
return h(cachedComponent, data, children)
} else {
// render previous empty view
return h()
}
}
}
...
代碼現(xiàn)在可能不太好理解,我們還是利用 Demo 來實現(xiàn)一下這個場景券敌。
/**
/
+------------------+
| Home |
| +--------------+ |
| | home1 home2 |
| | | |
| +--------------+ |
+------------------+
首頁下面有兩個嵌套路由 home1 跟 home2
/pageA
pageA 跟首頁平級
/pageB
pageB 跟首頁平級
*/
首先創(chuàng)建一個 Home1.vue
頁面:
<template>
<div class="about">
<h1>我是 home1</h1>
</div>
</template>
<script>
import LifeRecycle from "../life-recycle";
export default {
name: "home1",
mixins:[LifeRecycle]
}
</script>
然后創(chuàng)建一個 Home2.vue
頁面:
<template>
<div class="about">
<h1>我是 home2</h1>
</div>
</template>
<script>
import LifeRecycle from "../life-recycle";
export default {
name: "home2",
mixins:[LifeRecycle]
}
</script>
然后修改一下 Home
頁面唾戚,為其添加一個子路由:
<template>
<div class="home">
<h1>我是首頁</h1>
<router-link to="/pageA">點我跳轉(zhuǎn)到 a 頁面</router-link>
<div>
<router-link to="/home1">點我切換到 home1 頁面</router-link>|
<router-link to="/home2">點我跳轉(zhuǎn)到 home2 頁面</router-link>
<router-view/>
</div>
</div>
</template>
<script>
import LifeRecycle from "../life-recycle";
export default {
name: 'home',
mixins: [LifeRecycle]
}
</script>
然后修改一下 router.js
路由列表:
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
export default new Router({
mode: "history",
routes: [
{
path: '/',
name: 'home',
component: Home,
children: [
{
path: 'home1',
name: 'home1',
component: () => import(/* webpackChunkName: "about" */ './views/Home1.vue'),
},
{
path: 'home2',
name: 'home2',
component: () => import(/* webpackChunkName: "about" */ './views/Home2.vue'),
}
]
},
{
path: "/pageA",
name: "pageA",
component: () => import(/* webpackChunkName: "about" */ './views/A.vue')
},
{
path: "/pageB",
name: "pageB",
component: () => import(/* webpackChunkName: "about" */ './views/B.vue')
}
]
})
一切 ok 后,我們運行一下項目:
npm run serve
可以看到:
-
打開 home1 頁面
created--->home created--->home1 activated--->home1 activated--->home
-
home1 --> home2
created--->home2 destoryed--->home1
-
home2 --> pageA
created--->page-a deactivated--->home2 deactivated--->home activated--->page-a
-
pageA 點擊返回
deactivated--->page-a activated--->home2 activated--->home
此時
pageA
失活調(diào)用deactivated
方法待诅,然后激活home
叹坦,而home2
是home
的嵌套頁面,所以也直接走了activated
方法被激活卑雁,而起作用的原因就是前面所列出的router-view
的這一段代碼:... render (_, { props, children, parent, data }) { ... while (parent && parent._routerRoot !== parent) { const vnodeData = parent.$vnode ? parent.$vnode.data : {} // 獲取當前頁面的層級數(shù)(嵌套路由情況) if (vnodeData.routerView) { depth++ } if (vnodeData.keepAlive && parent._directInactive && parent._inactive) { inactive = true } parent = parent.$parent } data.routerViewDepth = depth // render previous view if the tree is inactive and kept-alive // 如果父組件為非激活狀態(tài)并且是被緩存的時候募书,就去渲染之前的組件 if (inactive) { const cachedData = cache[name] const cachedComponent = cachedData && cachedData.component if (cachedComponent) { // #2301 // pass props if (cachedData.configProps) { fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps) } return h(cachedComponent, data, children) } else { // render previous empty view return h() } } } ...
router-view
會判斷當前組件的父組件是不是keep-alive
的緩存組件轧钓,如果是的話,當前頁面(home2)離開的時候會把當前 home2 也緩存起來锐膜,下次再回到 home2 頁面的時候就不會再創(chuàng)建一個 home2 了,而是直接用緩存的組件弛房。
拓展
在 vue-router 的官網(wǎng)中我們看到這么一段介紹:
響應路由參數(shù)的變化
提醒一下道盏,當使用路由參數(shù)時,例如從
/user/foo
導航到/user/bar
文捶,原來的組件實例會被復用荷逞。因為兩個路由都渲染同個組件,比起銷毀再創(chuàng)建粹排,復用則顯得更加高效种远。不過,這也意味著組件的生命周期鉤子不會再被調(diào)用顽耳。復用組件時坠敷,想對路由參數(shù)的變化作出響應的話,你可以簡單地 watch (監(jiān)測變化)
$route
對象:const User = { template: '...', watch: { $route(to, from) { // 對路由變化作出響應... } } }
或者使用 2.2 中引入的
beforeRouteUpdate
導航守衛(wèi):const User = { template: '...', beforeRouteUpdate (to, from, next) { // react to route changes... // don't forget to call next() } }
兩個路由都渲染同個組件射富,比起銷毀再創(chuàng)建膝迎,復用則顯得更加高效。官網(wǎng)是這么介紹的胰耗,但是如果放棄這一點點優(yōu)化限次,我們硬是要重新創(chuàng)建一個組件怎么做呢?
比如我們現(xiàn)在的 Demo柴灯,我們修改一下代碼卖漫,讓 pageA
跟 pageB
都共用一個 pageA
組件:
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
export default new Router({
mode: "history",
routes: [
{
path: '/',
name: 'home',
component: Home,
children: [
{
path: 'home1',
name: 'home1',
component: () => import(/* webpackChunkName: "about" */ './views/Home1.vue'),
},
{
path: 'home2',
name: 'home2',
component: () => import(/* webpackChunkName: "about" */ './views/Home2.vue'),
}
]
},
{
path: "/pageA",
name: "pageA",
component: () => import(/* webpackChunkName: "about" */ './views/A.vue')
},
{
path: "/pageB",
name: "pageB",
component: () => import(/* webpackChunkName: "about" */ './views/A.vue')
}
]
})
然后從 pageA
頁面跳轉(zhuǎn)到 pageB
頁面的時候,你會發(fā)現(xiàn)赠群,沒有任何反應羊始,頁面并不會被創(chuàng)建,而且繼續(xù)復用了 pageA乎串,ok店枣,接下來我們看一下 vue 源碼中判斷是否可以復用的規(guī)則是咋樣的。
我們找到 vue/src/core/vdom/patch.js
源碼中的這么一段代碼:
...
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
所以 vue 源碼認為叹誉,只要 key 一致鸯两,并且標簽名等條件一直就認為是同一個組件了。
ok长豁!知道判斷條件后钧唐,我們來分析一下,我們并沒有給頁面組件指定 key 值匠襟,所以 undefined 是等于 undefined 的钝侠,然后 pageA 頁面跟 pageB 頁面都共用了一個 pageA 組件该园,所以 tag 名稱也是一樣的,然后后面的一些判斷條件也都成立帅韧,所以 vue 認為這兩個節(jié)點一致里初,也就不會再創(chuàng)建了,直接復用了忽舟,所以你會看到 pageA 點擊跳轉(zhuǎn)到 pageB 是沒有任何反應的双妨,那么知道原因后我們該怎么處理呢?
很簡單叮阅!直接給一個 key 就好了刁品,所以你會在很多 vue 項目中看到這樣的操作:
<template>
<div id="app">
<keep-alive>
<!-- 設置當前 router-view 的 key 為 path,來解決復用問題 -->
<router-view :key="$route.path"/>
</keep-alive>
</div>
</template>
看到這是不是就很能理解這個操作了呢浩姥?所以只有對源碼知根知底才能解決某些特殊的問題挑随,這也就是看源碼的重要性。
當然勒叠,頁面少的話拋開 vue 的復用是沒啥問題的兜挨,多頁面大項目的時候考慮內(nèi)存啥的還是保持官網(wǎng)說的 "比起銷毀再創(chuàng)建,復用則顯得更加高效" 較好眯分。
說到這里有小伙伴要疑問了暑劝,<keep-alive>
組件是怎么來獲取緩存的唯一 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 // 獲取緩存的 key 值
如果開發(fā)者有設置 key 值的話就直接用了颗搂,沒有的話用的是:
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
那么 componentOptions.Ctor.cid
又是啥呢担猛?Vue 中 cid
初始值是 0,每當調(diào)用了 Vue.extend()
方法后 cid
會自動加 1丢氢,然后賦值給當前的 vue 組件傅联。
總結(jié)
ok!寫了這么長的內(nèi)容疚察,雖然不管是解決 vue 組件的復用問題還是 activated 等方法的使用蒸走,網(wǎng)上一搜一大把都有對應的方案提供,但是我們得知道為什么要這么做呢貌嫡?所以這個時候就會強迫你去看源碼了比驻,我相信這一圈下來,你一定會有不一樣的收獲的岛抄。
加油吧别惦,騷年!