vue-router底层实现原理
定义:vue用来写路由一个插件。包括 router-link、router-view 两个组件,其中 router-link 用于实现跳转,router-view 用于展示视图vue-router的两种模式hash模式hash模式背后的原理是onhashchange事件因为hash发生变化的url都会被浏览器记录下来,从而你会发现浏览器的前进后退都可以用了,同时点...
定义:
vue用来写路由一个插件。包括 router-link、router-view 两个组件, 其中 router-link 用于实现跳转,router-view 用于展示视图
vue-router的两种模式
hash模式
hash模式背后的原理是onhashchange
事件
因为hash发生变化的url都会被浏览器记录下来,从而你会发现浏览器的前进后退都可以用了,同时点击后退时,页面字体颜色也会发生变化。这样一来,尽管浏览器没有请求服务器,但是页面状态和url一一关联起来,后来人们给它起了一个霸气的名字叫前端路由,成为了单页应用标配。
在hash模式下,前端路由修改的是#中的信息,而浏览器请求时是不带它玩的,仅hash
符号之前的url会被包含在请求中所以没有问题.
history模式
通过history api,我们丢掉了丑陋的#,但是它也有个毛病:
不怕前进,不怕后退,就怕刷新,f5,(如果服务器中没有相应的响应或者资源,会分分钟刷出一个404来。),因为刷新是实实在在地去请求服务器的,不玩虚的。
history
模式下,前端的url必须和实际向后端发起请求的url一致,如http://abc.com/user/id
,后端如果没有对user/id
的路由处理,将返回404错误。
vue-router底层实现方式
2.1实例化 Vue/Vue组件创建
new Vue({
router,
template: `
<div id="app">
<h1>Basic</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
<router-link tag="li" to="/bar">/bar</router-link>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')
2.2执行 vue-router 注入的 beforeCreate
钩子函数
通过Vue.mixin()
// ...
Vue.mixin({
beforeCreate () {
// 判断是否有 router
if (this.$options.router) {
// 赋值 _router
this._router = this.$options.router
// 初始化 init
this._router.init(this)
// 定义响应式的 _route 对象
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
}
})
具体来说,首先判断实例化时 options 是否包含 router,如果包含也就意味着是一个带有路由配置的实例被创建了,此时才有必要继续初始化路由相关逻辑。然后给当前实例赋值_router,这样在访问原型上的 $router 的时候就可以得到 router 了。
下边来看里边两个关键:router.init 和 定义响应式的 _route 对象。
2.3执行 router.init(vm)
router.init
然后来看 router 的 init 方法就干了哪些事情
(注册了对地址变更的监听,history.setupListeners())
依旧是在 src/index.js 中:
/* @flow */
import { install } from './install'
import { createMatcher } from './create-matcher'
import { HashHistory, getHash } from './history/hash'
import { HTML5History, getLocation } from './history/html5'
import { AbstractHistory } from './history/abstract'
import { inBrowser, supportsHistory } from './util/dom'
import { assert } from './util/warn'
export default class VueRouter {
// ...
init (app: any /* Vue component instance */) {
// ...
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(getLocation(history.base))
} else if (history instanceof HashHistory) {
history.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () => {
history.onHashChange()
})
})
}
history.listen(route => {
this.app._route = route
})
}
// ...
}
// ...
可以看到初始化主要就是给 app 赋值,针对于 HTML5History 和 HashHistory 特殊处理,因为在这两种模式下才有可能存在进入时候的不是默认页,需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由,此时就是通过调用 transitionTo 来达到目的;
而且此时还有个注意点是针对于 HashHistory 有特殊处理,
2.3.1为什么不直接在初始化 HashHistory 的时候监听 hashchange 事件呢?
答:这个是为了修复vuejs/vue-router#725这个 bug 而这样做的,
原因是因为在初始化的时候如果此时的 hash 值不是以 / 开头的话就会补上 #/,这个过程会触发 hashchange 事件,所以会再走一次生命周期钩子,也就意味着会再次调用 beforeEnter 钩子函数。即beforeEnter 钩子就会被触发两次。
2.3.2transitionTo 方法的大概逻辑
在 src/history/base.js 中:
/* @flow */
import type VueRouter from '../index'
import { warn } from '../util/warn'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { START, isSameRoute } from '../util/route'
import { _Vue } from '../install'
export class History {
// ...
transitionTo (location: RawLocation, cb?: Function) {
// 调用 match 得到匹配的 route 对象
const route = this.router.match(location, this.current)
// 确认过渡
this.confirmTransition(route, () => {
// 更新当前 route 对象
this.updateRoute(route)
cb && cb(route)
// 子类实现的更新url地址
// 对于 hash 模式的话 就是更新 hash 的值
// 对于 history 模式的话 就是利用 pushstate / replacestate 来更新
// 浏览器地址
this.ensureURL()
})
}
2.hashchange
或者路由跳转时,执行 history.transitionTo(...)
,在这个过程中,会进行地址匹配,得到一个对应当前地址的 route
,然后将其设置到对应的 vm._route
上。updateRoute.
只是进行了赋值,
2.3.3为什么 vm
就可以感知到路由的改变了呢?
答案在 vue-router 注入 Vue 的 beforeCreate
钩子函数中:
Vue.util.defineReactive(this, '_route', this._router.history.current)
采用与 Vue 本身数据相同的“数据劫持”方式,这样对 vm._route
的赋值会被 Vue 拦截到,并且触发 Vue 组件的更新渲染流程。
2.4视图更新
地址变更如何同步视图更新?
接上一步,vm._route
已经接收到路由的变更,从而触发视图更新。而当视图更新进一步调用到 <router-view>
的 render()
时,即进入了 <router-view>
的处理。
<router-view>
的 render()
采用函数调用(h()
)模式,而非通过模板生成。这也是 Vue2 支持的定义组件渲染逻辑的方式,类似 React 的 render()
。
采用这种模式的好处是
可以完全利用 JavaScript 的能力来编写逻辑,不必受制于 Vue 的类 HTML 模板语法。
这里的主要处理逻辑是从根组件中取出当前的路由对象(parent.$route
),然后取得该路由下对应的组件,然后交由该组件进行渲染:
return h(component, data, children)
这其中还涉及 <router-view>
嵌套的处理,不过主要逻辑就是这样了。
小结
vue-router 以插件方式“侵入”Vue,从而支持一个额外的
router
属性,以提供监听并改变组件路由数据的能力。这样每次路由发生改变后,可以同步到数据,进而“响应式”地触发组件的更新。
<router-view>
作为根组件下的子组件,从根组件那里可以获取到当前的路由对象,进而根据路由对象的组件配置,取出组件并用其替换当前位置的内容。这样,也就完成整个路由变更到视图变更的过程。
① 路由变更到视图变更的过程整理为:
hashchange
-->
match route
-->
set vm._route
-->
<router-view> render()
-->
render matched component
② 实现过程中的技术点包括:
- Vue 插件机制
- 响应式数据机制
- Vue 渲染机制
- 地址变更监听
结束
如果文章对你有帮助,麻烦点赞哦!一起走码农花路~
更多推荐
所有评论(0)