乐趣区

简述vue-router实现原理

router 源码解读
阅读请关注下代码注释
打个广告:哪位大佬教我下 sf 怎么排版啊,不会弄菜单二级导航(扑通.gif)
1. router 是什么
首先,你会从源码里面引入 Router,然后再传入参数实例化一个路由对象
// router/index.js
import Router from ‘vue-router’
new Router({…})

源码基础类:
// 源码 index.js
export default class VueRouter {

constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)

let mode = options.mode || ‘hash’ // 不选择模式会默认使用 hash 模式
this.fallback = mode === ‘history’ && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = ‘hash’
}
if (!inBrowser) {// 非浏览器环境默认 nodejs 环境
mode = ‘abstract’
}
this.mode = mode

switch (mode) {// 根据参数选择三种模式的一种
case ‘history’:
this.history = new HTML5History(this, options.base) // 根据 HTML5 版 History 的方法和属性实现的模式
break
case ‘hash’:
this.history = new HashHistory(this, options.base, this.fallback) // 利用 url 中的 hash 特性实现
break
case ‘abstract’:
this.history = new AbstractHistory(this, options.base) // 这种模式原理暂不清楚
break
default:
if (process.env.NODE_ENV !== ‘production’) {
assert(false, `invalid mode: ${mode}`)
}
}
}

// 一些 api 方法,你应该很熟悉,$router.push(…)
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}

go (n: number) {
this.history.go(n)
}

back () {
this.go(-1)
}

forward () {
this.go(1)
}

}
我们创建的路由都是 VueRouter 类的实例化,用来管理我们的【key-components-view】,一个 key(代码中的 path)对应一个组件,view 也就是 <router-view> 在 template 里面占个坑,用来根据 key 展示对应的组件, 实例上的 func 让我们可以控制路由,也就是官网的 api 说简单点,路由就是一个‘轮播图’,emmmmmm,说轮播好像也不过分哈,写个循环切换 key 的 func 就是‘轮播了’,而 key 就是轮播的 index,手动滑稽。那么,vue-router 是如何实现不发送请求就更新视图的呢,让我们来看看 vue 如何使用路由的实例化后的路由输出:区分下 route 和 router
2. router 工作原理如果你要使用到 router,你会在实例化 Vue 的参数 options 中加入 router
// main.js
improt xxx from xxx
import router from xxx
new Vue({
el: ‘#app’,
router: router,
components: {App},
template: ‘<App/>’
})
那,Vue 是如何使用这个参数呢,vue-router 是作为插件加入使用的,通过 mixin(混合)来影响每一个 Vue 实例化,在 beforeCreate 钩子的时候就会完成 router 的初始化,从参数获取 router -> 调用 init 初始化 -> 加入响应式(defineReactive 方法在 vue 源码用的很多,也是底层实现响应式的核心方法)
// 源码 install.js
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router // 获取 options 里面的 router 配置
this._router.init(this) // 初始化,这个 init 是 VueRouter 类里面的方法,实例化后会继承这个方法,方法代码见下方
Vue.util.defineReactive(this, ‘_route’, this._router.history.current) // 这个是把_route 加入数据监控,所以你可以 watch 到_route
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
初始化会做些什么:- 判断主程序状态(已经初始化了的 vue 实例不会再重新初始化路由,也就是你不能手动去再 init 一次)- 把实例加入内置数组 - 判断 history 的类型,做一些类似优化的事,比如 hash 模式的 setupListeners 方法,就是延迟监听 hashChange 事件,等到 vue 完成挂载再监听,太细节不用深入 -listen 定义一个 callback,listen 是定义在最底层 History 类上的,作用就是定义一个 callback,listen 会在需要的时候被调用,在路由发生变化的时候会执行这个 callback
// 源码 index.js
export default class VueRouter {

init (app: any /* Vue component instance */) {
process.env.NODE_ENV !== ‘production’ && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)

this.apps.push(app) // 这个 apps 存储了让所有的 Vue 实例化(根组件),后面遍历的时候,会把当前标记 route 挂到所有根组件的,也就是 vm._route 也是 vm._router.history.current

// main app already initialized.
if (this.app) {
return
}

this.app = app

const history = this.history

if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}

history.listen(route => { // 注意这个 listen 会在后面用到
this.apps.forEach((app) => {
app._route = route // 根组件全部获取当前 route
})
})
}

}
关于 route 的变化过程会在下面具体模式中说明,这里先跳过,接下来先说 vue 拿到 router 后,怎么使用 router 来渲染组件的
3. vue 如何使用 router 的
在安装 vue-router 插件的时候
export function install (Vue) {

Vue.component(‘RouterView’, View) // <router-link> & <router-view> 你应该很熟悉,本质就是 vue 组件,看源码之前我的猜测也是组件
Vue.component(‘RouterLink’, Link)

}
router-link 你不一定会使用,但是 router-view 你肯定会使用,它就是作为 ’ 窗口 ’ 的存在,来渲染你需要展示的组件。那,从这个组件开始说,一个前提条件是:vnode 是通过 render 来创建的,也就是说改变_route 的值会执行 render 函数,Router-View 这个组件定义了自己的 render,省略了大部分代码,这两行够了,你最终通过 <router-view> 看到的视图就是这么来的
// vue 源码 render.js
export function renderMixin (Vue: Class<Component>) {

vnode = render.call(vm._renderProxy, vm.$createElement)

}
// router 源码 view.js
render (_, { props, children, parent, data}) {

const h = parent.$createElement

return h(component, data, children)
}
第一种:hashHistory 模式
流程
$router.push() –> HashHistory.push() –> History.transitionTo() –> History.updateRoute() –> {app._route = route} –> vm.render()
1. 关于 hashurl 中 #号后面的参数,别名哈希值,关于 hash 的一些特性
1. 改变 hash 并不会引起页面重载
2.HTTP 请求不包括 #,所以使用 hash 不会影响到其他功能
3. 改变 #会改变浏览器的访问历史
4.window.location.hash 可以读取哈希值
5.JavaScript 可以通过 onhashchange 监听到 hash 值的变化,这就意味着可以知道用户在浏览器手动改变了 hash 值

因为这些特性才有的 hashHistory 更多关于 hash 知识见 URL 的井号 – 阮一峰的网络日志
2. hashHistory 源码首先,这三种模式都是通过继承一个基础类 History 来的
export class HashHistory extends History {

}
那,三种模式肯定有相同的属性,相同的方法,肯定不会去创建三次所以从一个基类继承,然后各自的部分属性 or 方法会有差异,至于 History 这个类,我是不会去细看的,反正我也看不懂,哈哈哈哈

router 上的实例属性、方法可以在 VueRouter、HashHistory/HTML5History/AbstractHistory、History 上找到,这里说下 HashHistory 的几个 func 的实现、
// router 源码 hash.js
export class HTML5History extends History {

go (n: number) {
window.history.go(n)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const {current: fromRoute} = this
this.transitionTo(location, route => { // History 类上的 func
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}

function pushHash (path) {
if (supportsPushState) {// 是否浏览器环境且环境支持 pushstat 方法,这个 func 下面会说
pushState(getUrl(path)) // 支持的话往 window.history 添加一条数据
} else {
window.location.hash = path // 不支持的话直接修改 location 的 hash
}
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const {current: fromRoute} = this
this.transitionTo(location, route => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// 其实 replace 和 push 只有两个区别
1.
window.location.hash = path
window.location.replace(getUrl(path))
2.
if (replace) {// replace 调这个 func 会传一个 true
history.replaceState({key: _key}, ”, url)
} else {
_key = genKey()
history.pushState({key: _key}, ”, url)
}

}
还有一点就是,在初始化 hash 模式路由的时候,会执行一个 func, 监听 hashchange 事件
setupListeners () {
window.addEventListener(supportsPushState ? ‘popstate’ : ‘hashchange’, () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
})
}
第二种:HTML5History 模式
HTML5–History 科普
主要是新增的两个 api
1.History.pushState()
[优点写的清清楚楚]

HTML5History 的 push、replace 跟 hash 模式的差不多,就不上代码了一个标记是否支持 HTML5 的 flag,这么写的,有需要的可以刨回去用
export const supportsPushState = inBrowser && (function () {
const ua = window.navigator.userAgent

if (
(ua.indexOf(‘Android 2.’) !== -1 || ua.indexOf(‘Android 4.0’) !== -1) &&
ua.indexOf(‘Mobile Safari’) !== -1 &&
ua.indexOf(‘Chrome’) === -1 &&
ua.indexOf(‘Windows Phone’) === -1
) {
return false
}

return window.history && ‘pushState’ in window.history
})()
还有一个就是 scrollBehavior,用来记录路由跳转的时候滚动条的位置,这个只能在 HTML5 模式下使用,即支持 pushState 方法的时候,部分博客说只有在 HTML5History 下才能使用,这个等我明天验证一下,我个人觉得支持 HTML5 就可以了
2.History.replaceState()
说的也很直观,就是不创新新纪录而覆盖一条记录,just do it
结束语
别问第三种情况(我是谁、我在哪、谁打我)
我兜子好沃,早知道不做前端了~
在学习 router 源码的时候阅读了熵与单子的代码本的文章,看完这篇文章配合源码基本都可以很好掌握 vue-router 的大概,感谢作者,另外说明下本文由本人学习结束后加上自己的理解一字一字敲出来的,可能有些相似之处,侵删请联系我,写文章的目的是看看自己能否表述清楚,对知识点的掌握情况,讲的不对的地方,请各位大佬指正~
~ 感谢潘童鞋的指导 (^▽^)
当然,我也稀罕你的小❤❤,点个赞再走咯~
以上图片均来自 MDN 网页截图、vue 官网截图、百度首页截图,不存在版权问题 / 滑稽
【注】:内容有不当或者错误处请指正~ 转载请注明出处~ 谢谢合作!

退出移动版