【Vue 故地重游】01.VueRouter 篇

731 阅读3分钟

写在前面

最近有时间,准备重新学习一下 vue,工作之余整理了一下对原理及源码的收获,在此记录一下,方便以后回来复习

ps:本篇文章以vue2.6.xvue3.2.x为例

基本使用

  1. 使用VueRouter插件
import Vue from 'vue';
import VueRouter from 'vue-router';

// 1.使用VueRouter插件
Vue.use(VueRouter);
  1. 创建实例
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
];

// 2.创建实例
const router = new VueRouter({
  routes,
});
  1. 配置到Vue的options
// 3.配置到选项中
new Vue({
  // ...其他options
  router,
  render: h => h(App),
}).$mount('#app');

// 4.在组件中使用
<router-link to="/home">home</router-link>
<router-view></router-view>

问题

  1. 为什么要先注册插件,注册插件做了哪些事情:Vue.use(VueRouter)
  2. new VueRouter(...)做了哪些事情
  3. 全局的<router-link />组件和<router-view />组件是哪里来的

原理探究

1.为什么要先注册插件,注册插件做了哪些事情

  1. Vue.use接收的参数是一个带有函数 install的对象,install包含参数Vue,将此参数保存,用于后续使用

    VueRouter.install = function(_Vue) {
      Vue = _Vue;
    };
    
  2. 通过mixins延迟执行,取出根实例中的router,挂载到Vue原型链

    Vue.mixin({
      beforeCreate() {
        // 这个钩子函数在每个组件创建实例时都会调用,但只有根组件被加入了router
        if (this.$options.router) {
          Vue.prototype.$router = this.$options.router;
        }
      },
    });
    
  3. 全局声明router-link组件和router-view组件

    • 3.1. router-link组件:只做展示,点击时更新 url this.$slots.default是一个虚拟dom构成的数组
      Vue.component('router-link', {
        props: {
          to: {
            type: String,
            required: true,
          },
        },
        render(h) {
          return h(
            'a',
            {
              attrs: {
                href: '#' + this.to,
              },
            },
            this.$slots.default // 虚拟dom构成的数组
          );
        },
      });
      
    • 3.2. router-view组件:url 变化时,展示的组件对应改变,Vue 最大的特点是响应式,而组件每次渲染都是调用了render函数,因此我们希望将来某个数据变化时能够触发render执行,这部分内容在new VueRouter(...)时操作

2.new VueRouter(...) 做了哪些事情

new VueRouter(...)返回的实例将来要挂载到 Vue原型链上,即组件中用的this.$router

  1. 定义一个响应式的属性current,改变时触发 <router-view /> 更新

  2. Vue 提供了定义响应式数据的方法:Vue.util.defineReactive

    这个方法会给我们的属性添加observer, 然后通过 getter函数 触发watcher的收集机制,将<router-view />的 render 方法收集到依赖中, 这样每次更新时就会执行render函数,从而更新视图。

    核心代码如下:

    // 定义响应式数据
    Vue.util.defineReactive(this, 'current', initial);
    
    Vue.component('router-view', {
      render(h) {
        let component = null;
        // 调用一次getter,观察者会将render函数收集
        const route = this.$router.$options.routes[this.$router.current];
        if (route) {
          component = route.component;
        }
    
        return h(component);
      },
    });
    
  3. 监听路由改变

    window.addEventListener('hashchange', () => {
      this.current = window.location.hash.slice(1);
    });
    

    因为此时current已经是响应式数据,所以改变时会自动触发watcher更新:即<router-view />更新

    到这里,VueRouter的基本原理就结束了。

思考与总结

1. 定义响应式数据的方式除了Vue.util.defineReactive还有哪些,有何区别?

  • 1.1. 创建一个 Vue 对象

    const state = new Vue({
      data: { count: 0 },
    });
    

    state.count就是响应式对象,可以直接在视图中使用

  • 1.2. Vue.observable

    const state = Vue.observable({ count: 0 });
    

    同样state.count是一个响应式对象,直接在视图中使用

  • 1.3. 区别:defineReactiveVue源码中的内容,并没有在官方文档中出现过,而observable2.6版本新出现的一个API,在低于 2.6 版本的 Vue 项目 中会报is not a function的错误。所以保险起见,使用defineReactivenew Vue(...)比较好

2. mixins的妙用:延迟执行

  • 2.1. 这里是给全局Vue混入生命周期,因此每个组件都享用该mixins,需要在执行函数中加以判断,只在根组件中挂载$router

    Vue.mixin({
      beforeCreate() {
        // 根实例才有该选项
        if (this.$options.router) {
          Vue.prototype.$router = this.$options.router;
        }
      },
    });
    
  • 2.2. 由于混入组件的内容与当前组件容易出现命名冲突、以及方法变量来源不明,一直以来都对这个 api 绕着走。今天才发现了原来还有这么妙的使用方式,最开始以为是借助事件总线实现延迟执行,直到看了源码的这部分,才有了恍然大悟的感觉

3. 嵌套<router-view/>如何解决

源码是这样处理的:

VueRouter 中新增一个响应式属性matched,每次路由变化时执行一个叫做match的函数用来匹配当前路由。

<router-view/>组件中,给虚拟 dom 的data中添加一个属性routerView,表示当前组件是<router-view />

然后在 render 函数中,创建一个变量depth表示当前路由的深度,其计算方式是通过while循环向上遍历,遇到<router-view />+1

最后,<router-view/>组件渲染时从$router中取出matched,根据matched[depth]就可以获取到当前 url 下对应深度的路由组件

核心代码:

VueRouter.js

constructor(options) {
  this.$options = options;

  // 表示当前url
  this.current = window.location.hash.slice(1) || '/';
  // 定义一个响应式数组 matched,用来存储当前匹配的路由
  Vue.util.defineReactive(this, 'matched', []);
  this.match();

  // 每次hash改变都重置matched,然后重新匹配
  window.addEventListener('hashchange', () => {
    this.current = window.location.hash.slice(1);
    this.matched = [];
    this.match();
  });
}

match(routes) {
  routes = routes || this.$options.routes;

  for (const route of routes) {
    if (route.path === '/' && this.current === '/') {
      this.matched.push(route.component);
    } else if (route.path !== '/' && this.current.includes(route.path)) {
      this.matched.push(route.component);
    }
    if (route.children) {
      this.match(route.children);
    }
  }
}

router-view (ps:源码中是函数式组件,这里为了简化就直接用非函数组件了)

Vue.component('router-view', {
  render(h) {
    // <router-view /> 的标志
    this.$vnode.data.routerView = true;

    // 嵌套路由
    let depth = 0;
    let parent = this.$parent;
    while (parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {};
      if (vnodeData.routerView) {
        depth++;
      }
      parent = parent.$parent;
    }

    const component = this.$router.matched[depth];

    return component ? h(component) : null;
  },
});

4. 当前webpack环境在Vue.component中为什么不能用template?

在写router-view组件时,发现template无法使用,虽然不是VueRouter的内容,不过遇到了就总结下吧 在webpack环境中,默认情况下打包的 vueruntime 版本,借助loader就完成了编译工作,并且runtime识别的是render 函数,因此没有 compiler模块,也就不能解析template