Vue-route底层实现原理
- 先抛出一堆问题:
- 如何改变 URL 却不引起页面刷新?
- 如何检测 URL 变化了?
- inistall方法做了什么?
- 如何$router与$route绑定到所有组件上?
- 如何确保插件只安装一次?
- 1、引入
vue-router
,并使用Vue.use(VueRouter)
- 2、定义路由数组,并将数组传入
VueRouter实例
,并将实例暴露出去 - 3、将
VueRouter
实例注册到根Vue实例上
//简洁版:仅列出关键信息
import VueRouter from 'vue-router' //会调用vue-router上的install方法初始化vuerouter这个类
Vue.use(VueRouter) // 第一步
const routes = [
{
path: '/home',
component: home,
}, .....
]
export default new VueRouter({
routes // 第二步 new出router类的实例
})
new Vue({
router, // 第三步 注册到根实例上
render: h => h(App)
}).$mount('#app')
确保插件不会重复安装:
Vue.use = function(plugin){
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if(installedPlugins.indexOf(plugin)>-1){
return this;
}
<!-- 其他参数 -->
const args = toArray(arguments,1);
args.unshift(this);
if(typeof plugin.install === 'function'){
plugin.install.apply(plugin,args);
}else if(typeof plugin === 'function'){
plugin.apply(null,plugin,args);
}
installedPlugins.push(plugin);
return this;
}
vue.js上新增use方法,判断插件是不是已经注册过,维护一个数组,没注册过的则插入,注册过的return出去。
install方法
Vue.use(XXX)
,就是执行XXX
上的install
方法,并将Vue作为参数传入install方法。也就是Vue.use(VueRouter) === VueRouter.install()
调用install方法时,会将Vue作为参数传入
install一般是给每个vue实例加东西,在这里就是给每个组件添加$route
和$router
。
$route
和$router
有什么区别?
$router
是VueRouter的实例对象,$route
是当前路由对象,也就是说$route
是$router
的一个属性 注意每个组件添加的$route
是是同一个,$router
也是同一个,所有组件共享的。
// src/my-router.js
....省略了部分代码,只列出关键部分
let _Vue
VueRouter.install = (Vue) => {
_Vue = Vue
// 使用Vue.mixin混入每一个组件
Vue.mixin({
// 在每一个组件的beforeCreate生命周期去执行
beforeCreate() {
if (this.$options.router) { // 如果是根组件,即router实例存在,vuerouter实例在第三步就已挂载到根组件上
// this 是 根组件本身
this._routerRoot = this
// this.$options.router就是挂在根组件上的VueRouter实例
this.$router = this.$options.router
// 执行VueRouter实例上的init方法,初始化
this.$router.init(this)
} else {
// 非根组件,也要把父组件的_routerRoot保存到自身身上,相当于把根组件传递下去给子组件
this._routerRoot = this.$parent && this.$parent._routerRoot
// 子组件也要挂上$router
this.$router = this._routerRoot.$router
}
}
})
Object.defineProperty(Vue.prototype, '$router', {//将$router挂载到组件实例上。
get () { return this._routerRoot._router }
})
Vue.component('RouterView', View) //注册全局组件
Vue.component('RouterLink', Link)
}
一种代理的思想,我们获取组件的$router
,其实返回的是根组件的_root._router
个人总结:
1、use方法中,用installed数组维护,标识只执行一次,不会重复安装,
2、通过递归将所有的vue组件混入beforeCreate和destroyed钩子函数,因为在钩子函数中this指向其本身,在钩子中对每个子组件绑定_route即VueRouter实例. 将VueRouter实例对象注入到所有的vue实例上,
在原型对象上将$router和router实例对象绑定
3、后面通过Vue.component定义全局的<router-link>和<route-view>组件,link相当于a标签,可以改变hash地址
routes转map结构
//router/index.js
import Vue from 'vue'
import VueRouter from './myVueRouter'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
const router = new VueRouter({
mode:"history",
routes
})
export default router
//myVueRouter.js
let Vue = null;
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash"
this.routes = options.routes || [] //你传递的这个路由是一个数组表
this.routesMap = this.createMap(this.routes)
console.log(this.routesMap);
}
createMap(routes){
return routes.reduce((pre,current)=>{
pre[current.path] = current.component
return pre;
},{})
}
}
router实例添加history.current属性,属性可获取到当前路由路径
//myVueRouter.js
let Vue = null;
class HistoryRoute {
constructor(){
this.current = null
}
}
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash"
this.routes = options.routes || [] //你传递的这个路由是一个数组表
this.routesMap = this.createMap(this.routes)
this.history = new HistoryRoute();
新增代码
this.init()
}
新增代码
init(){
if (this.mode === "hash"){
// 先判断用户打开时有没有hash值,没有的话跳转到#/
location.hash? '':location.hash = "/";
window.addEventListener("load",()=>{
this.history.current = location.hash.slice(1)
})
window.addEventListener("hashchange",()=>{
this.history.current = location.hash.slice(1)
})
} else{
location.pathname? '':location.pathname = "/";
window.addEventListener('load',()=>{
this.history.current = location.pathname
})
window.addEventListener("popstate",()=>{
this.history.current = location.pathname
})
}
}
createMap(routes){
return routes.reduce((pre,current)=>{
pre[current.path] = current.component
return pre;
},{})
}
}
Vue.mixin({
beforeCreate(){
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router;
新增代码
Vue.util.defineReactive(this,"xxx",this._router.history)
}else { //如果是子组件
this._root= this.$parent && this.$parent._root
}
Object.defineProperty(this,'$router',{
get(){
return this._root._router
}
});
Object.defineProperty(this,'$route',{
get(){
return this._root._router.history.current
}
})
}
})
利用Vue提供的API:defineReactive,使得this._router.history对象得到监听。
第一次渲染router-view这个组件的时候,会获取到this._router.history
这个对象,从而就会被监听到获取this._router.history
。就会把router-view组件的依赖wacther收集到this._router.history
对应的收集器dep中,因此this._router.history
每次改变的时候。this._router.history
对应的收集器dep就会通知router-view的组件依赖的wacther执行update(),从而使得router-view
重新渲染(其实这就是vue响应式的内部原理)
下面以hash模式为例子:
hash模式的原理就是,通过window.addeventlistener监听hashchange这个事件(history的话是监听popstate这个事件),可以监听浏览器url中hash值的变化,通过window.location.hash可以获取到组件名,再通过map结构找出对应的组件。
class HashHistory {
constructor(router) {
// 将传进来的VueRouter实例保存
this.router = router
// 如果url没有 # ,自动填充 /#/
ensureSlash()
// 监听hash变化
this.setupHashLister()
}
// 监听hash的变化
setupHashLister() {
window.addEventListener('hashchange', () => {
// 传入当前url的hash,并触发跳转
this.transitionTo(window.location.hash.slice(1))
})
}
// 跳转路由时触发的函数
transitionTo(location) {
console.log(location) // 每次hash变化都会触发,可以自己在浏览器修改试试
// 比如 http://localhost:8080/#/home/child1 最新hash就是 /home/child1
// 找出所有对应组件,router是VueRouter实例,createMathcer在其身上
let route = this.router.createMathcer(location)
}
}
// 如果浏览器url上没有#,则自动补充/#/
function ensureSlash() {
if (window.location.hash) {
return
}
window.location.hash = '/'
}
// 这个先不讲,后面会用到
function createRoute(record, location) {
const res = []
if (record) {
while (record) {
res.unshift(record)
record = record.parent
}
}
return {
...location,
matched: res
}
}
export default HashHistory
注意:手动刷新的话不会出发hashchange,所以在初始化的时候执行一次原地跳转
// src/my-router.js
class VueRouter {
// ...原先代码
init(app) {
// 初始化时执行一次,保证刷新能渲染
this.history.transitionTo(window.location.hash.slice(1))
}
// ...原先代码
}
贴一下完整代码:
/src/my-router.js
//myVueRouter.js
let Vue = null;
class HistoryRoute {
constructor(){
this.current = null
}
}
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash"
this.routes = options.routes || [] //你传递的这个路由是一个数组表
this.routesMap = this.createMap(this.routes)
this.history = new HistoryRoute();
this.init()
}
init(){
if (this.mode === "hash"){
// 先判断用户打开时有没有hash值,没有的话跳转到#/
location.hash? '':location.hash = "/";
window.addEventListener("load",()=>{
this.history.current = location.hash.slice(1)
})
window.addEventListener("hashchange",()=>{
this.history.current = location.hash.slice(1)
})
} else{
location.pathname? '':location.pathname = "/";
window.addEventListener('load',()=>{
this.history.current = location.pathname
})
window.addEventListener("popstate",()=>{
this.history.current = location.pathname
})
}
}
createMap(routes){
return routes.reduce((pre,current)=>{
pre[current.path] = current.component
return pre;
},{})
}
}
VueRouter.install = function (v) {
Vue = v;
Vue.mixin({
beforeCreate(){
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router;
Vue.util.defineReactive(this,"xxx",this._router.history)
}else { //如果是子组件
this._root= this.$parent && this.$parent._root
}
Object.defineProperty(this,'$router',{
get(){
return this._root._router
}
});
Object.defineProperty(this,'$route',{
get(){
return this._root._router.history.current
}
})
}
})
Vue.component('router-link',{
props:{
to:String
},
render(h){
let mode = this._self._root._router.mode;
let to = mode === "hash"?"#"+this.to:this.to
return h('a',{attrs:{href:to}},this.$slots.default)
}
})
Vue.component('router-view',{
render(h){
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap;
return h(routeMap[current])
}
})
};
export default VueRouter
src/view.js link.js
Vue.component('router-view',{
render(h){
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routesMap;
return h(routeMap[current])
}
})
Vue.component('router-link',{
props:{
to:String
},
render(h){
let mode = this._self._root._router.mode;
let to = mode === "hash"?"#"+this.to:this.to
return h('a',{attrs:{href:to}},this.$slots.default)
}
})
render函数里的this指向的是一个Proxy代理对象,代理Vue组件,而我们前面讲到每个组件都有一个_root属性指向根组件,根组件上有_router这个路由实例。 所以我们可以从router实例上获得路由表,也可以获得当前路径。 然后再把获得的组件放到h()里进行渲染。
参考https://juejin.cn/post/6854573222231605256