[vue2]熬夜编写为了让你们通俗易懂的去深入理解vue-router并手写一个

358 阅读3分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

更多文章

[vue2]熬夜编写为了让你们通俗易懂的去深入理解nextTick原理

[vue2]熬夜编写为了让你们通俗易懂的去深入理解vuex并手写一个

[vue2]熬夜编写为了让你们通俗易懂的去深入理解双向绑定以及解决监听Array数组变化问题

[vue2]熬夜编写为了让你们通俗易懂的去深入理解v-model原理

熬夜不易,点个赞再走吧

内部实现

router-link

  1. 默认渲染成a标签,可以通过tag生成别的标签

  2. 通过绑定click事件,执行VueRouter中的push进行跳转

<router-link to="/about">about</router-link>

router-view

  1. 当具有层级关系时,每个router-view都有自己的标记,在循环中确认自己的depth,然后在所在的层次中从matched获取组件
 const matched = route.matched[depth]
  1. matched中保存所有父子组件信息,索引从0开始,依次是顶层组件、然后是一层层下来的子组件

初始化

  let _Vue;
  class VueRouter {
    constructor(){}
  }
  
  VueRouter.install = (e) => {
    _Vue = e
  }; 
  
  // router-link
  _Vue.component('router-link', {})
  
  // router-view
  _Vue.component('router-view', {})
  export default VueRouter;

这个时候我们进行了初始化,在install里设置了全局的_Vue,并初始化了router-link和router-view

router-link

我们需要一个render来渲染, 渲染什么呢

<a href="#/about">about</a>

首先他是一个a标签

然后有一个href

中间的文本用插槽渲染

  1. 所以我们先用h() 渲染一个虚拟dom
  2. 第一个参数给一个 'a'
  3. 第二个参数给一个attrs,包含一个href,利用#${this.to}拼接hash链接
  4. 第三个参数利用插槽获取链接中的文本

    // h()返回虚拟dom
    render(h) {
      return h(
        'a',
        {
          attrs: {
            // <a href="#/about">about</a>
            href`#${this.to}`,
          },
        },
        // 通过插槽获取a标签里的文本
        this.$slots.default
      );
    },

接下来看

<router-link to="/about">about</router-link>

标签里的to是一个传参,传的是一个path,利用props来接收,设置一个必传

    props: {
      to: {
        type: String,
        require: true,
      },
    },

于是实现了一个简单的router-link

  // router-link
  _Vue.component('router-link', {
    // <router-link to="/about">about</router-link>
    props: {
      to: {
        typeString,
        requiretrue,
      },
    },
    // h()返回虚拟dom
    render(h) {
      return h(
        'a',
        {
          attrs: {
            // <a href="#/about">about</a>
            href`#${this.to}`,
          },
        },
        // 通过插槽获取a标签里的文本
        this.$slots.default
      );
    },
  });

router-view

router-view是怎么实现的呢?

我们总要渲染组件吧,所以先创建一个变量

let component = null

然后我们在 /router/index.js里有一串这样的代码

const routes = [
    {
      path: '/home',
      name: 'Home',
     component: Home
   },
   {
     path: '/about',
     name: 'About',
     component: About
   }
 ];
  1. 这个routes是一个映射表,之后会挂在this.$router.options里,通过配置去在下面的操作中可以获取对应的组件
  2. 通过获取的path去找到对应的组件

      let current = this.$router.current
      let route = this.$router.$options.routes.find((route)=>
        route.path==current
      )
      if(route){
        component = route.component
      }
  1. 获取到后渲染

      return h(component);

最后是这样


  //router-view
  _Vue.component('router-view', {
    render(h) {
      let component = null

      // 获取当前路由path
      let current = this.$router.current
      // 通过获取的path去找到对应的组件
      let route = this.$router.$options.routes.find((route)=>
        route.path==current
      )
      if(route){
        component = route.component
      }
      // 获取到后渲染
      return h(component);
    },
  });

其实到这里并没结束,看到这里也许有些疑问,比如this.$router是从哪里来的?

class VueRouter {
  constructor(options){
    console.log(options)
    // 保存这个配置
    this.$options = options
  })

这里保存了一个option的配置

然后其实在install里我们做了一个这样的操作,利用mixin() 混入在beforeCreate() 的生命周期中去进行了挂载

// 全局混入(如果配置了router,则挂载到根实例上)
// beforeCreate(): 生命周期
 _Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        _Vue.prototype.$router = this.$options.router;
      }
    },
  });

为什么我们能在这里获取呢?

我们看main.js

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

new Vue({
  el'#app',
  // 声明了一个router
  router,
  components: { App },
  template'<App/>'
})

这里我们在new Vue的时候声明了一个router,而我们在beforeCreate中获取到的this,就是vue的实例,就是new的那个Vue,那就能获取到router,就能通过this.$options.router去进行挂载了

其实到这里也没结束,看到这里也许还有些疑问,比如this.$router.current又是从哪里来的呢?

其实这个时候我们的VueRouter还是空的,存在即合理,那是拿来做什么的呢?就是拿来处理这个current

我们先默认一个current为"/"

    this.current = "/"

监听hash的变化,获取current

当URL的片段标识符更改时,将触发hashchange事件

  window.addEventListener('hashchange',()=>{
    this.current = window.location.hash.slice(1) || "/"
    console.log("hashchange"this.current)
  })

其实到这里也没结束,如果调试页面的话会发现current在不断的变化,但是却没有重新渲染,为什么?因为目前current不是一个响应式的,所以只在初始化的时候执行了一次

所以现在要把current变成响应式数据

保证render随着current变化而再次执行

Vue.util.defineReactive定义一个对象的响应属性

Vue.util.defineReactive(obj,key,value,fn)
  obj: 目标对象,
  key: 目标对象属性;
  value: 属性值
  fn: 只在node调试环境下set时调用

在this上对current添加响应式的值initial

  let initial = window.location.hash.slice(1) || "/"
   _Vue.util.defineReactive(this,"current",initial)  
  

完整代码

/router/index.js

import Vue from 'vue'
import VueRouter from './vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"

Vue.use(VueRouter)

const routes = [
  {
    path'/',
    name'Home',
    componentHome
  },
  {
    path'/home',
    name'Home',
    componentHome
  },
  {
    path'/about',
    name'About',
    componentAbout
  }
];
const router = new VueRouter({
  routes
})
export default router

/router/vue-router.js

let _Vue;
class VueRouter {
  constructor(options){
    console.log(options)
    // 保存这个配置
    this.$options = options
    this.current = "/"

    // 把current变成响应式数据
    // 保证render随着current变化而再次执行
    let initial = window.location.hash.slice(1) || "/"
    // 在this上对current添加响应式的值initial
    _Vue.util.defineReactive(this,"current",initial)


    // 监听hash的变化,获取current
    // 当URL的片段标识符更改时,将触发hashchange事件
    window.addEventListener('hashchange',()=>{
      this.current = window.location.hash.slice(1) || "/"
      console.log("hashchange"this.current)
    })
  }
}
VueRouter.install = e => {
  _Vue = e;
  console.log(_Vue)
  // 全局混入(如果配置了router,则挂载到根实例上)
  // beforeCreate(): 生命周期
  _Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        _Vue.prototype.$router = this.$options.router;
      }
    },
  });



  // router-link
  _Vue.component('router-link', {
    // <router-link to="/about">about</router-link>
    props: {
      to: {
        typeString,
        requiretrue,
      },
    },
    // h()返回虚拟dom
    render(h) {
      return h(
        'a',
        {
          attrs: {
            // <a href="#/about">about</a>
            href`#${this.to}`,
          },
        },
        // 通过插槽获取a标签里的文本
        this.$slots.default
      );
    },
  });

  //router-view
  _Vue.component('router-view', {
    render(h) {
      let component = null

      let current = this.$router.current
      let route = this.$router.$options.routes.find((route)=>
        route.path==current
      )
      console.log(route)
      if(route){
        component = route.component
      }
      console.log(component)
      return h(component);
    },
  });
};
export default VueRouter;

main.js

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

new Vue({
  el'#app',
  router,
  components: { App },
  template'<App/>'
})