vuerouter地址改变了页面没有跳转_Vue Router源码解析一

Anli Li:Vue Router源码解析一​zhuanlan.zhihu.com
07a01bb50f68027987ba51cbc5463b65.png
Anli Li:Vue Router源码解析二​zhuanlan.zhihu.com
915757baf7f6d14810364affc124a5ef.png
Anli Li:Vue Router源码解析三​zhuanlan.zhihu.com
2b34e97a7549d169ff5045f4177b7dc1.png
Anli Li:Vue Router源码解析四​zhuanlan.zhihu.com
40b9f74acc65997e679cdf974cd743e8.png

照例还是读官方文档,边读边写问题。

d6407caababf0fb6d4139abe199ae83d.png

准备

必须先介绍前端路由的基本知识,有了深厚的内功之后可以驾驭任何外公,不,外功,举证参见张无忌。

ef52efad7e3945acce04b4d52e6fa52e.png

首先看看如果前端实现路由,要满足哪些前提条件,以下四点是我总结的

  1. 路由体现在URL上
  2. 路由能携带参数
  3. 路由的改变不会引起页面刷新
  4. 能让浏览器历史里记录下路由变化

于是只剩hash和HTML5的stateAPI可选了。

hash是URL上带#的部分,本意是用来做页面的锚点的,在锚点之间的跳转是不会触发页面刷新的,只在浏览器侧发生变化。监听hash的改变有hashchange事件。

stateAPI目前主流浏览器都支持了(不要跟我再提什么IE9,来气)。一句话描述就是,history提供了pushStatereplaceState两个API能改变URL,但不会触发页面刷新。监听state的改变没有事件,但是对于变化后浏览器的后退行为,有popstate事件。

至于两者的优劣,网上比较的太多了,我就不掺和了。除了这些以外,我们要做的实验是这两者是否会相互影响,测试代码如下

 <!DOCTYPE html>
 <html>
   <head>
     <title>Route</title>
   </head>
   <body>
     <a href="#foo">foo</a>
     <a href="#bar">bar</a>
     <button id="btn1">Change Normal State</button>
     <button id="btn2">Change Hash State</button>
     <div id="foo" style="border: 1px solid red; height: 800px">foo</div>
     <div id="bar" style="border: 1px solid blue; height: 800px">bar</div>
     <script>
       window.addEventListener("hashchange", e => {
         console.log("hash改变了");
       });
       window.addEventListener("popstate", e => {
         console.log("popstate被触发了");
       });
       document.getElementById("btn1").addEventListener("click", e => {
         history.pushState("test", "test", "test");
       });
       document.getElementById("btn2").addEventListener("click", e => {
         history.pushState("test", "test", "#test");
       });
     </script>
   </body>
 </html>

首先测试两个超链接,发现无论是点击超链接,还是之后再点击浏览器的返回,都会触发hashchangepopstate事件。

接下来测试按钮的行为,其中一个按钮将URL变为/test,另一个按钮将URL变为/#test。测试后发现,点击按钮,即便是修改hash的行为,也不会触发hashchange事件,在点击浏览器返回时,都会触发popstate事件。

总结一下,popstate是好兄弟,hashchange触发的时候,它也会跟着一起上。

想起了一个重口味的梗,小便和大便哪个更讲义气?

结构

 │__create-matcher.js
 │__create-route-map.js
 │__index.js
 │__install.js
 │
 ├─components
 │______link.js
 │______view.js
 │
 ├─history
 │______abstract.js
 │______base.js
 │______errors.js
 │______hash.js
 │______html5.js
 │
 └─util
 │______async.js
 │______dom.js
 │______location.js
 │______misc.js
 │______params.js
 │______path.js
 │______push-state.js
 │______query.js
 │______resolve-components.js
 │______route.js
 │______scroll.js
 │______state-key.js
 │______warn.js

源码使用Flow开发,rollup来做构建。

对象声明

Vue生态圈的各框架初始化过程大致相似。

 export default class VueRouter {
     //...
 }
 ​
 VueRouter.install = install
 VueRouter.version = '__VERSION__'
 ​
 if (inBrowser && window.Vue) {
   window.Vue.use(VueRouter)
 }

除了暴露出VueRouter之外,在它之上还加了install方法,用作Vue的插件。另外还判断了浏览器和全局Vue的存在,装载插件,就是为了在全局引用的时候不需要再调用Vue.use(VueRouter)

install

install.js就光暴露了install方法,在装载插件的时候执行,因此它是第二层的初始化,主要针对的不是Vue-Router对象本身而是针对整个Vue组件树,之前创建Vue Router对象的时候还没有感知Vue,但此时就跟Vue挂钩了。

这里局部的变量终于起名为了_Vue,让我舒服了很多,之前读Vuex的时候,相同作用的这个变量取名为Vue,着实让我困惑了一把。

Vuex一样,它也是定义了一个mixin

 Vue.mixin({
   beforeCreate () {
     if (isDef(this.$options.router)) {
       this._routerRoot = this
       this._router = this.$options.router
       this._router.init(this) // 后文会提到这个方法
       Vue.util.defineReactive(this, '_route', this._router.history.current)
     } else {
       this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
     }
     registerInstance(this, this)
   },
   destroyed () {
     registerInstance(this)
   }
 })

Vuex里另一点相似的是_routerRoot——在根部,这个变量指向Vue实例自己,在其他的Vue实例上,这个变量指向根Vue实例,并且也是通过父组件的该变量一层层传下来的。

registerInstance看着有点意思,在beforeCreatedestroyed里调用的时候只差一个参数,而这一个参数的差别肯定引起的效果是相反的。

 const registerInstance = (vm, callVal) => {
   let i = vm.$options._parentVnode
   if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
     i(vm, callVal)
   }
 }

初看有点不明所以,这时就要借助于debugger手段了。多读了几遍之后发现,原来坑就坑在这个i最后相当于是vm.$options._parentVnode.data.registerRouteInstance,但是又没给中间变量取别的名字。

通过全局搜索发现,registerRouteInstance这个方法定义在了<router-view>组件里,也就是说只有<router-view>的下一层组件实例的beforeCreate方法,才会进到if里,调用registerRouteInstance方法,这个方法的作用如其名字,在全局注册实例,而如果callValundefined的话,实际效果就是unregister取消注册了。

接着就是在原型上定义两个变量,这样让每一个Vue组件都能访问到$router$route,并且是指向同一个对象实例的。

 Object.defineProperty(Vue.prototype, '$router', {
   get () { return this._routerRoot._router }
 })
 ​
 Object.defineProperty(Vue.prototype, '$route', {
   get () { return this._routerRoot._route }
 })

然后把<router-view><router-link>两个组件定义在全局,在哪里都能直接用它们。

 Vue.component('RouterView', View)
 Vue.component('RouterLink', Link)

最后是自定义选项合并策略,可以参考https://cn.vuejs.org/v2/guide/mixins.html#自定义选项合并策略)

 const strats = Vue.config.optionMergeStrategies
 // use the same hook merging strategy for route hooks
 strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created

初始化

构造函数是第一层的初始化,主要针对VueRouter对象本身。

兵马未动粮草先行,先定义实例变量。

 this.app = null
 this.apps = []
 this.options = options
 this.beforeHooks = []
 this.resolveHooks = []
 this.afterHooks = []
 this.matcher = createMatcher(options.routes || [], this)

接下来先设置好默认的模式,Vue Router一共支持三种模式。

 let mode = options.mode || 'hash'
 this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
 if (this.fallback) {
   mode = 'hash'
 }
 if (!inBrowser) {
   mode = 'abstract'
 }
 this.mode = mode

有点绕,我觉得还不如堆砌if else呢。实际逻辑是

  1. 如果运行环境不是浏览器,那就设置为abstract
  2. 如果是浏览器运行环境,默认为hash
  3. 但即便你设置了history,我也要先检查一下你浏览器是不是支持history的BOM API,要是不支持,那么对不起,仍然滚回hash

有了模式之后,就用简单工厂模式来创建对象了

 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:
     if (process.env.NODE_ENV !== 'production') {
       assert(false, `invalid mode: ${mode}`)
     }
 }

看着好像很简单的样子,但那是代码给你的错觉,还有一部分的初始化工作在init方法里。这个init方法在什么时候会被调用?其实就在刚才看到的mixin代码里。

 beforeCreate () {
   if (isDef(this.$options.router)) {
     this._routerRoot = this
     this._router = this.$options.router
     this._router.init(this)
     Vue.util.defineReactive(this, '_route', this._router.history.current)
   } else {
     this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
   }
   registerInstance(this, this)
 }

Vue组件树根实例上(严格的说是Vue Router挂载的那个Vue实例上,因为可以把Vue Router挂载在全量组件树的下面某一层上)执行这个init方法。

看一下init方法,抠掉不重要的代码。

 init (app: any /* Vue component instance */) {
 ​
     this.apps.push(app)
 ​
     // main app previously initialized
     // return as we don't need to set up new history listener
     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 => {
       this.apps.forEach((app) => {
         app._route = route
       })
     })
 }

传入的app就是Vue Router对象实例所挂载的Vue实例,这个很好理解,但居然用了一个数组this.apps来缓存起来,想想除非是一个Vue Router对象实例被挂到了多个Vue根组件上,那么才会产生在Vue Router对象实例上用一个数组来缓存所有的Vue根组件,暂时想象不出这种业务场景,绝大多数情况应该只会有一个Vue路由根组件。

下一步就是跳转了,因为有可能用户打开的并不是一个根路径,所以要根据实际URL来跳转到对应的路由去,也即是渲染对应的组件,同时这也是路由的意义——按照URL可以直接打开对应的页面。

最后就是监听变化,和之前beforeCreate里的代码再呼应起来。刚才我们漏掉了一句

 Vue.util.defineReactive(this, '_route', this._router.history.current)

Vue路由根组件的_route属性上定义了一个响应,当这个属性发生变化的时候,会引起Vue的重新渲染,一会我们会做实验看到当_route发生变化时,会重新进入<router-view><router-link>render渲染函数。调用history对象监听到变化后,就会去修改这个_route对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值