震惊!Vue路由竟然是这样实现的!

源码共读

共 13420字,需浏览 27分钟

 ·

2021-08-28 23:32

👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇


作者丨前端发现者

来源丨前端发现

今日心血来潮,想起我们使用Vue开发单页面项目基本会用到 vue-router 路由插件,通过改变Url,在不刷新页面的情况下,更新页面视图。那么 vue-router 它是怎么实现路由跳转页面的呢?

好吧,没人理我就自己玩😂。我(们)先来回忆下路由的配置:

router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

//声明路由表
const routes = [
  {
    name'login',
    path'/login',
    component() => import('@/views/login/index')
  },
  {
    name'register',
    path'/register',
    component() => import('@/views/register/index')
  }
]

export default new Router({
  routes
})

main.js引入

import router from './router'

new Vue({
  el'#app',
  router,
  renderh => h(App)
})

App.vue使用路由组件

<template>
  <div id="app">
    <router-view />
  </div>

</template>

目前vue-router提供路由跳转的方法有:

  • router.push          添加新路由
  • router.replace       替换当前路由
  • router.go            跳转到指定索引路由
  • router.back          返回上一个路由
  • router.forward       跳转下一个路由

以及常用的<view-link to="/login">去登录</view-link>

好了,vue-router路由的使用回忆完了,脑海是否存在一下问题?

  1. Vue.use(Router)时做了什么事情?
  2. <router-view />组件是怎么来的?
  3. <router-link />组件是怎么来的?
  4. router路由提供的编程式导航是怎么实现的?
  5. 浏览器Url地址发生变化时怎么渲染对应组件的?

我们知道,Vue中使用Vue-router的时候,实际是引入一个提供路由功能的插件,既然是插件,那么它就会向外提供一些方法供开发者使用。下面我们就针对上述的疑问一步步揭开谜底。

Vue.use(Router)时做了什么事情?

用户执行Vue.use的时候,其实是执行vue-router插件的 install 方法,并且把Vue的实例作为参数传递进去。

注:在Vue定义,只要插件中提供 install 方法就可以被Vue作为Vue.use(xx)来使用。翻看Vue-router源码的 src/install.js 文件,我们就可以看到下面这样的代码:

可以看到这个文件向外提供了 install 的方法。方法里面使用了Vue实例,并在实例中使用了 mixin 。那么在mixin中做了什么事呢?

  • beforeCreate 生命周期中判断 this.$options.router 是否存在,这个东西就是我们在main.js文件中new Vue({})创建路由实例时传入的touter对象。
  • 在Vue实例中指定_routerRoot缓存下自身
  • 在Vue实例中指定_router缓存传入的router路由实例
  • 路由实例调用init方法,参数为Vue实例
  • 通过Vue的defineReactive方法将_route变成响应式,指向当前路由的URL。
  • 劫持数据_route,一旦_route数据发生变化后,通知router-view执行render方法

我们再来看看src/util/toute.js文件中创建路由的方法。

export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route 
{
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}

  const route: Route = {                             // 添加一个route对象
    name: location.name || (record && record.name), // 路由表配置的name属性
    meta: (record && record.meta) || {},           // 路由表配置的meta对象
    path: location.path || '/',                   // 路由表配置的path属性
    hash: location.hash || '',   
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)       // 最后将route对象冻结并返回(即不允许新增属性) 
}

方法参数解析:

  • record:路由记录信息
  • location:需要跳转的路由地址(包含path、query、hash和params的对象)
  • router:router实例

<router-view />和<router-link />组件怎么来的?

你可能会注意到,我们在App.vue页面中会使用<router-view>和<router-link />组件,但是我们并没有手动引入和注册这两个组件,其实是vue-router内部帮我们去全局注册了组件。

还是刚才那个 install.js 文件

import View from './components/view'
import Link from './components/link'
...
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)

会看到这个几行代码。没错了,就是在执行install方法的时候就在Vue注册了组件了。

router路由提供的编程式导航是怎么实现的?

说到这里就要翻到src/index.js文件了。这里写了一个VueRouter类,VueRouter里面实现了编程式导航功能以及在constructor中看到了mode选项的配置。

从这也就知道了默认的路由渲染模式是hash,其中出现的options就是路由的配置。

接着往下走,来到第167行,会看到如下代码:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  // $flow-disable-line
  if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
    return new Promise((resolve, reject) => {
      this.history.push(location, resolve, reject)
    })
  } else {
    this.history.push(location, onComplete, onAbort)
  }
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  // $flow-disable-line
  if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
    return new Promise((resolve, reject) => {
      this.history.replace(location, resolve, reject)
    })
  } else {
    this.history.replace(location, onComplete, onAbort)
  }
}
go (n: number) {
  this.history.go(n)
}
back () {
  this.go(-1)
}
forward () {
  this.go(1)
}

如此你的代码就可以这么写啦

router.push(location, onComplete?, onAbort?)
router.replace(location, onComplete?, onAbort?)
router.go(n)
router.back()
router.forward()

浏览器Url地址发生变化时怎么渲染对应组件的?

我们需要知道的是,当浏览器地址发生变化时:

HashHistory和HTML5History会分别监控hashchange和popstate来对路由变化作对应的处理。HashHistory和HTML5History捕获到变化后会对应执行push或replace方法,从而调用transitionTo来对路由变化作对应的处理。

上面提到在install方法的mixin中,会监听_route数据变化,一旦_route数据发生变化后,通知router-view执行render方法。这里就要回到刚才注册<router-view>组件那里去了。

翻到sec/components/view.js就能看到刚才注册的组件render函数啦

export default {
  name'RouterView',
  functionaltrue,
  props: {
    name: {
      typeString,
      default'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true
    const h = parent.$createElement
    // 得到渲染的组件
    const name = props.name
    // route 对象
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth
    if (inactive) {
      // 非 keepalive 模式下 每次都需要设置钩子
      // 进而更新(赋值&销毁)匹配了的实例元素
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
        return h()
      }
    }
    const matched = route.matched[depth]
    const component = matched && matched.components[name]
    if (!matched || !component) {
      cache[name] = null
      return h()
    }
    cache[name] = { component }
    data.registerRouteInstance = (vm, val) => {
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }
    data.hook.init = (vnode) => {
      if (vnode.data.keepAlive &&
        vnode.componentInstance &&
        vnode.componentInstance !== matched.instances[name]
      ) {
        matched.instances[name] = vnode.componentInstance
      }
      handleRouteEntered(route)
    }
    const configProps = matched.props && matched.props[name]
    if (configProps) {
      extend(cache[name], {
        route,
        configProps
      })
      fillPropsinData(component, data, route, configProps)
    }
    return h(component, data, children)
  }
}

最后做个总结就是:

  • 向外暴露 installrouter ,接着初始化路由。
  • 内部注册<router-view>和<router-link>组件。
  • 设置变量保存当前路由地址,监听hash变化,切换当前组件,然后render渲染对应的组件(hash模式)


-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击👆卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

浏览 19
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报