知食记
搜索文档…
vue-router原理
参考:
vue-router是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。这篇文章讲述的主要内容便是vue-router的实现原理,相当于vue-router源码的粗浅理解。行文主要包括vue-router的使用方法、实现原理两大部分,使用方法的介绍是为了辅助实现原理的陈述。

使用方法

    1.
    注册VueRouter插件:
    1
    import Vue from 'vue'
    2
    import VueRouter from 'vue-router'
    3
    Vue.use(VueRouter)
    Copied!
    2.
    定义(路由)组件与路由。
    1
    const User = { template: '<div>用户</div>' }
    2
    const Role = { template: '<div>角色</div>' }
    3
    const routes = [
    4
    { path: '/user', component: User },
    5
    { path: '/home', component: Home }
    6
    ]
    Copied!
    3.
    创建 router 实例,并传 routes 配置 const router = new VueRouter({routes })
    4.
    创建和挂载根实例。
    1
    const app = new Vue({
    2
    router,
    3
    template: `
    4
    <div id="app">
    5
    <h1>Basic</h1>
    6
    <ul>
    7
    <li><router-link to="/">/</router-link></li>
    8
    <li><router-link to="/user">用户</router-link></li>
    9
    <li><router-link to="/role">角色</router-link></li>
    10
    <router-link tag="li" to="/user">/用户</router-link>
    11
    </ul>
    12
    <router-view class="view"></router-view>
    13
    </div>
    14
    `
    15
    }).$mount('#app')
    Copied!

实现原理

每一个工具实现的原理都依赖JS/HTML基本语法,比如Vue的双向数据绑定依赖的是JS的Object.defineProperty。而Vue Router的实现依赖于两种前端路由,即模式history模式和hash模式:
    history模式
      HTML5中的两个API:pushStatereplaceState,改变url之后页面不会重新刷新,也不会带有#号,页面地址美观,url的改变会触发popState事件,监听该事件也可以实现根据不同的url渲染对应的页面内容
      但是因为没有#会导致用户在刷新页面的时候,还会发送请求到服务端,为避免这种情况,需要每次url改变的时候,都将所有的路由重新定位到跟路由下
    hash模式
      url hash: http://foo.com/#help
      #后面hash值的改变,并不会重新加载页面,同时hash值的变化会触发hashchange事件,该事件可以监听,可根据不同的哈希值渲染不同的页面内容
1. 插件注册
Vue.use将VueRouter插件注入Vue中,use方法会调用vue-router的install方法,对VueRouter进行安装,整个安装过程精简版的源码,全部源码地址
1
Vue.mixin({
2
beforeCreate () { ...},
3
})
4
Object.defineProperty(Vue.prototype, '$router', {
5
get () { return this._routerRoot._router }
6
})
7
Object.defineProperty(Vue.prototype, '$route', {
8
get () { return this._routerRoot._route }
9
})
10
Vue.component('RouterView', View)
11
Vue.component('RouterLink', Link)
Copied!
install这个过程主要做了这样的三件事:
    对Vue实例混入beforeCreate/destroyed两个钩子操作,在Vue实例创建生命周期中钩子函数执行前被调用,这个在创建Vue实例部分详细介绍;
    设置Vue.prototype代理,当访问this.$router和this.$route的时候,返回this._routerRoot._router和_route,方便所有组件可以获取到这两个属性
    注册RouterLink和RouterView两个组件
      router-view: 获取到匹配的组件,在非keepalive模式下,每次都要设置钩子进而更新匹配了的实例元素
      router-link: 绑定click事件,在其点击的时候根据设置的 to 的值去调用 router 的 push 或者 replace 来更新路由的,同时呢,会检查自身是否和当前路由匹配(严格匹配和包含匹配)来决定自身的 activeClass 是否添加
2. 创建router实例
全部源码地址,核心源码:
1
this.matcher = createMatcher(options.routes || [], this)
2
let mode = options.mode || 'hash'
3
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
4
if (this.fallback) {
5
mode = 'hash'
6
}
7
if (!inBrowser) {
8
mode = 'abstract'
9
}
10
this.mode = mode
11
switch (mode) {
12
case 'history':
13
this.history = new HTML5History(this, options.base)
14
break
15
case 'hash':
16
this.history = new HashHistory(this, options.base, this.fallback)
17
break
18
case 'abstract':
19
this.history = new AbstractHistory(this, options.base)
20
break
21
default:
22
if (process.env.NODE_ENV !== 'production') {
23
assert(false, `invalid mode: ${mode}`)
24
}
25
}
26
init(){....}
Copied!
可以看到这个阶段,Vue Router主要做了两件主要的事情:
    创建 match 匹配函数:根据用户路由配置对象生成普通的根据 path 来对应的路由记录以及根据 name 来对应的路由记录的 map,方便后续匹配对应
    根据配置的mode创建路由实现的实例:
      如果当前环境不支持history模式,会强制切换到hash模式;默认hash模式
      如果当前环境不是浏览器环境,会切换到abstract模式下。
3. 创建Vue实例
在这里创建Vue实例,首先会执行install时候混入的钩子函数,这一部分的源码:
1
Vue.mixin({
2
beforeCreate () {
3
if (isDef(this.$options.router)) {
4
this._routerRoot = this
5
this._router = this.$options.router
6
this._router.init(this)
7
Vue.util.defineReactive(this, '_route', this._router.history.current) //为vue实例定义数据劫持
8
} else {
9
10
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this //非跟组件则直接从父组件中取
11
}
12
registerInstance(this, this)
13
},
14
destroyed () {
15
registerInstance(this)
16
}
17
})
Copied!
这一部分,主要做了两件事情
    对_router赋值,此时this.$options.router包含着传入的路由参数
    执行创建vue router实例中的init方法,根据不同模式生成监控路由变化的History对象
1
init (app: any /* Vue component instance */) {
2
...
3
const history = this.history
4
if (history instanceof HTML5History) {
5
history.transitionTo(history.getCurrentLocation())
6
} else if (history instanceof HashHistory) {
7
const setupHashListener = () => {
8
history.setupListeners()
9
}
10
history.transitionTo(
11
history.getCurrentLocation(),
12
setupHashListener,
13
setupHashListener
14
)
15
}
16
history.listen(route => {
17
this.apps.forEach((app) => {
18
app._route = route
19
})
20
})
21
}
Copied!
这一部分,主要做了两件事情:
    通过history的模式来确定不同路由的切换动作history.transitionTo,更新浏览器地址
      对于hash模式,更新hash的值
      对于history模式,利用pushState/replaceState更新浏览器地址
      对于Abstract模式下,所做的仅仅是用一个数组当做栈来模拟浏览器历史记录,拿一个变量来标示当前处于哪个位置。
    通过history.listen来注册路由变化的响应回调
一旦this._router.history.curren,即history实例当前的路由对象有变化,就会触发更新机制,继而调用应用实例的 render 重新渲染
    hash模式的更新机制:触发onHashChange的事件,调用transitionTo
    history模式的更新机制:触发popState的事件,调用transitionTo
为什么不直接在初始化 HashHistory 的时候监听 hashchange 事件呢?
这个是为了修复https://github.com/vuejs/vue-router/issues/725 这个 bug 而这样做的,简要来说就是说如果在 beforeEnter 这样的钩子函数中是异步的话,beforeEnter 钩子就会被触发两次,原因是因为在初始化的时候如果此时的 hash 值不是以 / 开头的话就会补上 #/,这个过程会触发 hashchange 事件,所以会再走一次生命周期钩子,也就意味着会再次调用 beforeEnter 钩子函数。
最近更新 1yr ago
复制链接