2021-01-01 22:47:46 配套了网站和git仓库
2020-05-04 15:41:02 补充了源码的解释
2019-12-11 21:37:07 完成初稿

按:

第一次尝试写本文,还是去年年底(2019-12-11 21:37:07),后来入职新公司忙的要死,也就鸽了。今天重新梳理,争取弄得清楚一些:带你读 Vue-Router源码。

vue-router 是什么?

  • 是官方组件
  • 有 history 和 hash 两种模式
  • 注册了 this.$routerthis.$route
  • 注册了 router-linkrouter-view 两个全局组件
  • 其他特性

是官方组件

是 vue 官方的 router 插件,我们通过使用这个插件来完成 vue 项目中路由的导航。

既然提到插件,就不得不说 vue 的插件系统, Vue.use的技术细节看这里 《Vue 从基础到高级的概念》 (未来统一整合吧)。

因此,可以料想,router 插件的代码结构大致如下:

  1. // step 1
  2. class VueRouter {}
  3. VueRouter.install = function() {};

两种模式

HTML5 history mode or hash mode, with auto-fallback in IE9

显然我们需要监听路由变化,这是一个主动通知也就是响应式的过程。

注册两个方法

我们可以在全局通过 this.$routethis.$router 来控制 router 的逻辑细节。

注册两个组件

  • route-link 导航
  • route-view 视图

使用流程

Vue-Router 的核心使用流程:

  1. // 1 导入 main.js
  2. import VueRouter from 'vue-router'
  3. Vue.use(VueRouter) // 说明是Vue插件,自然要想到install方法
  4. // 2 创建router实例 规则。router.js
  5. const routes = [
  6. { path: '/foo', component: Foo },
  7. { path: '/bar', component: Bar }
  8. ] //export
  9. // 3 添加到根实例 main.js
  10. import routes from 'router.js'
  11. const router = new VueRouter({
  12. routes // (缩写)相当于 routes: routes
  13. })
  14. new Vue({
  15. router
  16. }).$mount('#app')
  17. // 4 两个全局组件 App.vue
  18. // <router-view /> // 路由视图
  19. // <router-link to="/">去首页</router-link> // 路由导航

源码解析

如果要实现这种使用方式,需要完成下面的需求:

  • 是一个 Vue插件 — Vue插件编写, install
  • 书写规则,解析规则 router.js
  • 随 根实例一起创建
  • 注册全局组件
  • 监听路由变化,展示内容

实现 vue-router 插件

刚才已经提到,我们的结构如下:

  1. // step 1
  2. class VueRouter{}
  3. VueRouter.install = function(){}

困难点:router install时候,还没有 new Vue,也就是拿不到实例。该如何处理?

针对这个方法,官方妥善使用了 mixin混入,在 Vue的 beforeCreate 声明周期中执行相关逻辑,也需要进一步判断是否是根实例。

官方是这么做的,点开查看 官方router/install.js 。当install方法执行的时候。忽略一些代码,能看到 mixin.

  1. // 使用混入来做router挂载这件事情
  2. Vue.mixin({
  3. beforeCreate() {
  4. // 只有根实例才有router选项 此时this是vue实例
  5. if (this.$options.router) {
  6. Vue.prototype.$router = this.$options.router;
  7. }
  8. }
  9. });

接下来就可以准备 注册全局组件。

  1. // step2
  2. let Vue; // 这里定义 Vue 方便后续使用
  3. class vueRouter{} // 这里先不关注
  4. // 插件需要实现install静态方法
  5. // 接收一个参数,Vue构造函数,主要用于数据响应式
  6. VueRouter.install = function (_Vue) {
  7. // 保存Vue构造函数在VueRouter中使用
  8. Vue = _Vue
  9. // 任务1:使用混入来做router挂载这件事情
  10. Vue.mixin({
  11. beforeCreate() {
  12. // 只有根实例才有router选项
  13. if (this.$options.router) {
  14. Vue.prototype.$router = this.$options.router
  15. }
  16. }
  17. })
  18. // 任务2:实现两个全局组件
  19. // 这部分代码放到下面
  20. Vue.component('router-link',{
  21. template: "<a>aaa</a>"
  22. })
  23. Vue.component('router-view',{template: "<a>aaa</a>"})
  24. }

You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

不能要求用户一定使用 runtime 的vue。

注册全局组件

文档中也提到了,插件中是如何注册组件的

  • Import 组件导入
  • Vue.component() 注册全局组件

router-link

先只考虑 hash模式。<router-link to="/">aaa</router-link> 这个组件有 props slot,注意插件不适用template,vue版本不一定存在编译器,所以唯一选项是使用render函数。因为 href 是 attribute,所以这样编写如下组件:

  1. // router-link: 生成一个a标签,在url后面添加#
  2. // <a href="#/about">aaaa</a>
  3. Vue.component('router-link', {
  4. props: {
  5. to: {
  6. type: String,
  7. required: true
  8. }
  9. },
  10. render(h) {
  11. // h(tag, props, children)
  12. return h('a',
  13. { attrs: { href: '#' + this.to } },
  14. this.$slots.default
  15. )
  16. // 使用jsx
  17. // return <a href={'#'+this.to}>{this.$slots.default}</a>
  18. }
  19. })

这样就实现了渲染为html:<a href='#/'>aaa</a>

此时,点击页面已经可以正常跳转了,虽然页面不是响应式的。

route-view

暂时先不实现。

  1. Vue.component('router-view', {
  2. render(h) {
  3. return h('div','route-view ...')
  4. }
  5. })

完善逻辑

接下来实现点击a连接,变化 hash,也就是监听hashchange事件。

我们补充下面代码

  1. class VueRouter {
  2. constructor(options) {
  3. this.$options = options;
  4. // 当前hash
  5. this.current = window.location.hash.slice(1) || "/";
  6. // 监听变化
  7. window.addEventListener("hashchange", () => {
  8. this.current = window.location.hash.slice(1);
  9. console.log("this.current", this.current);
  10. });
  11. }
  12. }

和这个代码

  1. Vue.component("router-view", {
  2. render(h) {
  3. // 实例中获取当前hash
  4. console.log("render", this.$router.current);
  5. return h("div", "route-view ...");
  6. }
  7. });

观察控制台效果。可以看到 render只触发了一次。原因是的 this.current 不是响应式的数据。

要解决很简单,添加一行代码即可:注意 line8

  1. class VueRouter {
  2. constructor(options) {
  3. this.$options = options;
  4. // 当前hash
  5. const currentHash = window.location.hash.slice(1) || "/";
  6. this.current = currentHash;
  7. console.log("this.current", this.current);
  8. // 定义数据响应式
  9. // 定义一个响应式的current,则如果他变了,那么使用它的组件会rerender
  10. Vue.util.defineReactive(this, "current", currentHash);
  11. // 监听变化
  12. window.addEventListener("hashchange", () => {
  13. this.current = window.location.hash.slice(1);
  14. // console.log("this.current", this.current);
  15. });
  16. }
  17. }

接下来就是渲染组件了,找到组件交给render即可

  1. Vue.component("router-view", {
  2. render(h) {
  3. // 当前this是组件实例
  4. // console.log("this", this.$router);
  5. // 实例中获取当前hash
  6. console.log("render", this.$router, this.$router.current);
  7. // 从路由表里匹配当前的路由
  8. const component =
  9. this.$router.$options.routes.find(
  10. route => route.path === this.$router.current
  11. )?.component ?? "div";
  12. return h(component);
  13. }
  14. });

最终,页面导出类

  1. export default VueRouter

这样就 my-vue-router.js 就写完了,可以看我 code/01 文件夹里的内容了。

当然很粗糙,可以进一步优化,比如路由表缓存,而不是每次都循环遍历。line10-lin13

  1. class VueRouter {
  2. constructor(options) {
  3. // ...
  4. // 一次性缓存路由映射表
  5. this.routeMap = {};
  6. this.$options.routes.forEach(route => {
  7. this.routeMap[route.path] = route;
  8. });
  9. // ...
  10. }
  11. }

这里做修改

  1. Vue.component("router-view", {
  2. render(h) {
  3. const { current, routeMap } = this.$router;
  4. const component = routeMap[current]?.component ?? "div";
  5. return h(component);
  6. }
  7. });

至此,需要去搞定嵌套路由的逻辑了。

嵌套路由

嵌套路由,是 route-view 的嵌套。在官方源码中的 src/component/view.js 可以看到解决方式。

给路由增加层级 depth。

  • 给 view组件挂载 view tag 表名是 RouteView
  • 控制matched 数组

虽然是函数式组件,但不重要,line16 设定routeview=true 做标记
Lin27 表示深度 depth ,看祖辈有没有 routeview,如果有 虚拟dom的data里有,就+1 表示深度

在 line58 匹配,匹配的是数组,是个数组,打印下看看。
line59 component
lin115 渲染时候,渲染 component

源码怎么写的?

官方git仓库 Vue-router