簡述
hashchange
-->
match route
-->
set vm._route
-->
<router-view> render()
-->
render matched component
監聽hashchange方法
window.addEventListener('hashchange', () => {
// this.transitionTo(...)
})
進行地址匹配,得到對應當前地址的 route。
將其設置到對應的 vm._route 上。侵入vue監聽_route變量而觸發更新流程
最后是router-view組件調用render函數渲染匹配到的route
測試代碼
<!DOCTYPE html>
<html>
<head>
<title>vue test</title>
</head>
<body>
<div id="app">
<h1>Hello App!</h1>
<button @click="goBack">click me and go back</button>
<button @click="goIndex">click me and go to index</button>
<p>
<router-link to="/foo">Go to Foo</router-link>
</p>
<p>
<router-link to="/bar">Go to Bar</router-link>
</p>
<router-view></router-view>
</div>
<!-- Vue.js v2.6.11 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.1.3/vue-router.js"></script>
<script>
const Foo = { template: '<div>render foo</div>' }
const Bar = { template: '<div>render bar</div>' }
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
const router = new VueRouter({
routes // (縮寫) 相當於 routes: routes
})
var app = new Vue({
el: '#app',
router,
methods: {
goBack() {
this.$router.back();
},
goIndex() {
this.$router.push('/');
}
}
})
console.log(app);
// var event = new CustomEvent('test', { 'detail': 5 }); window.dispatchEvent(event);
</script>
</body>
</html>
怎么注入進的 vue
一個 install 函數,把 $route 掛載到了 Vue.prototype 上,保證 Vue 的所有組件實例,都是取同一份 router。並且在里面注冊了 RouterView 和 RouterLink 組件
function install(Vue) {
// ...
Vue.mixin({
beforeCreate: function beforeCreate() {
// ...
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
// ...
},
destroyed: function destroyed() {
// ...
}
});
Object.defineProperty(Vue.prototype, '$router', {
get: function get() { return this._routerRoot._router }
});
Object.defineProperty(Vue.prototype, '$route', {
get: function get() { return this._routerRoot._route }
});
Vue.component('RouterView', View);
Vue.component('RouterLink', Link);
// ...
}
VueRouter.install = install;
最后進入了 vue 的初始化邏輯里 initUse 函數里去觸發插件的 install 函數執行。
router 是個什么結構
詳見 function VueRouter (options)
,下面代碼中需要注意三點:
- app 將會掛上 vue 實例對象
- mode 代表用戶配置的路由模式,默認是 hash,也就是使用 url 上的 hash 部分作為路由路徑的判定。
- history 將會掛載上用戶曾經的訪問的記錄數組。
var VueRouter = function VueRouter (options) {
this.app = null;
this.apps = [];
this.options = options;
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
this.matcher = createMatcher(options.routes || [], this);
var mode = options.mode || 'hash';
// ...
this.mode = mode;
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base);
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback);
break
case 'abstract':
this.history = new AbstractHistory(this, options.base);
break
default:
{
assert(false, ("invalid mode: " + mode));
}
}
};
RouterView 組件長什么樣
看下文代碼,總結一下關鍵的步驟:
最關鍵的一步 var component = cache[name] = matched.components[name];
獲取到具體是那個組件,這里的 component 其實是
{
template: "<div>render bar</div>"
_Ctor: {0: ƒ}
__proto__: Object
}
然后最后面就是調用 h(component, data, children)
完成渲染,h其實是 Vue 實例的 $createElement 函數,它會具體解析此 template 成為視圖渲染。
var View = {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render: function render (_, ref) {
var props = ref.props;
var children = ref.children;
var parent = ref.parent;
var data = ref.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
var h = parent.$createElement;
var name = props.name;
var route = parent.$route;
var cache = parent._routerViewCache || (parent._routerViewCache = {});
// ...
var component = cache[name] = matched.components[name];
// ...
return h(component, data, children)
}
};
RouterLink 呢?
很精妙,此組件的 props 默認把 tag 設置為 a,並且代碼中還支持 slotScope 插槽。
最后一樣 h(this.tag, data, this.$slots.default)
去渲染,所以此組件渲染后的標簽才會默認是 a 標簽呀。。
var Link = {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click'
}
},
render: function render(h) {
var router = this.$router;
var current = this.$route;
var ref = router.resolve(
this.to,
current,
this.append
);
// ...
var href = ref.href;
// ...
var data = { class: classes };
var scopedSlot =
!this.$scopedSlots.$hasNormal &&
this.$scopedSlots.default
// ...
if (scopedSlot) {
if (scopedSlot.length === 1) {
return scopedSlot[0]
} else if (scopedSlot.length > 1 || !scopedSlot.length) {
// ...
return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
}
}
if (this.tag === 'a') {
data.on = on;
data.attrs = { href: href };
} else {
// ...
}
return h(this.tag, data, this.$slots.default)
}
};
路由控制是怎么做的
本質上就是改變了 hash
hashchange 的事件監聽觸發,接着去觸發 HashHistory 實例里的 updateRoute 函數,updateRoute 函數里觸發回調去更新 route 對象,route 對象更新就走入了 vue 自身的 set 觸發廣播通知被觀察者了。
VueRouter.prototype.back = function back () {
this.go(-1);
};
VueRouter.prototype.go = function go (n) {
this.history.go(n);
};
HashHistory.prototype.go = function go (n) {
window.history.go(n);
};
// ...
window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
function () {
var current = this$1.current;
// ...
this$1.transitionTo(getHash(), function (route) {
if (supportsScroll) {
handleScroll(this$1.router, route, current, true);
}
if (!supportsPushState) {
replaceHash(route.fullPath);
}
});
}
);
// ...
History.prototype.transitionTo = function transitionTo(
location,
onComplete,
onAbort
) {
var this$1 = this;
var route = this.router.match(location, this.current);
this.confirmTransition(
route,
function () {
this$1.updateRoute(route);
// ...
},
function (err) {
// ...
}
);
};
// ...
History.prototype.updateRoute = function updateRoute(route) {
var prev = this.current;
this.current = route;
// 這里的 cb 就是下面一段的 history.listen
this.cb && this.cb(route);
this.router.afterHooks.forEach(function (hook) {
hook && hook(route, prev);
});
};
// ...
history.listen(function (route) {
this$1.apps.forEach(function (app) {
// 改變 app._route 就會進入 vue 實例自身的 get/set 攔截器中,然后自己觸發更新。
// 因為上文 install 函數里做了屬性劫持 Vue.util.defineReactive(this, '_route', this._router.history.current);
app._route = route;
});
});
鈎子是怎么做的
this.beforeHooks 是個數組,registerHook 函數做的就只是往前面的數組里添加進入這個方法。
VueRouter.prototype.beforeEach = function beforeEach(fn) {
return registerHook(this.beforeHooks, fn)
};
VueRouter.prototype.beforeResolve = function beforeResolve(fn) {
return registerHook(this.resolveHooks, fn)
};
VueRouter.prototype.afterEach = function afterEach(fn) {
return registerHook(this.afterHooks, fn)
};
beforeHooks 在每次觸發更新前的隊列里調用
resolveHooks 執行是在下文的 runQueue 里,也就是是在觸發更新前,但比 beforeHooks 晚,主要用於異步組件
afterHooks 的觸發,是在 updateRoute 函數后,也就是開始觸發 vue 的更新邏輯時,但並不一定視圖已經更新完畢,因為 vue 自身也有不少的隊列操作,不會立即更新。
// beforeHooks
var queue = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(function (m) { return m.beforeEnter; }),
// async components
resolveAsyncComponents(activated)
);
runQueue(queue, iterator, function () {
// ...
}
// resolveHooks
runQueue(queue, iterator, function () {
// ...
// wait until async components are resolved before
// extracting in-component enter guards
var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
var queue = enterGuards.concat(this$1.router.resolveHooks);
runQueue(queue, iterator, function () {
//...
onComplete(route);
//...
});
});
// afterHooks
History.prototype.updateRoute = function updateRoute(route) {
var prev = this.current;
this.current = route;
this.cb && this.cb(route);
this.router.afterHooks.forEach(function (hook) {
hook && hook(route, prev);
});
};
history 是怎么做的
hash 模式的路由是采用的 hash change 函數來做監聽,並且操作瀏覽器 hash 做標識,
而 history 模式采用的 popstate event 來記住路由的狀態,而 window.history.state 里的 key 只是用時間來生成的一個緩存。
HTML5History.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
pushState(cleanPath(this$1.base + route.fullPath));
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
}, onAbort);
};
function pushState (url, replace) {
saveScrollPosition();
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
var history = window.history;
try {
if (replace) {
history.replaceState({ key: getStateKey() }, '', url);
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url);
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url);
}
}
function genStateKey () {
return Time.now().toFixed(3)
}