Vue 3源码极客之:`Vue`的`Vue Router`:`router`实例如何被注入到组件中。

各位观众老爷,大家好!我是你们的老朋友,今天咱们来聊聊 Vue 3 源码里一个很有意思的话题:Vue Router 实例是怎么被“偷偷塞进”组件里的?听起来是不是有点谍战片的味道? 别担心,咱们用最轻松的方式,把这事儿给扒个底朝天!

开场白:组件,你从哪里来?我的 Router

相信各位对 Vue 组件都不陌生,但你有没有想过,为啥每个组件都能直接用 this.$routerthis.$route? 难道是 Vue 偷偷给每个组件都 new 了一个 Router 实例? 当然不是!Vue 才没那么傻,它用了一种更优雅的方式,那就是“依赖注入”。

第一幕: createApp 闪亮登场

一切的根源,都藏在 createApp 这个 API 里。 当我们创建一个 Vue 应用的时候,通常会这么写:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

看到 app.use(router) 了吗? 别小看这一行代码,它可是“魔法”开始的地方! 实际上,app.use 方法会调用 Router 实例的 install 方法。

第二幕:Router 的 install 方法,暗藏玄机

Router 的 install 方法长什么样呢? 咱们简化一下,核心代码大概是这样的:

// 简化版的 Router.install 方法
function install(app) {
  const router = this //这里的this就是Router实例

  app.provide(routerKey, router) //关键一步:provide

  app.mixin({ //全局混入,让所有组件都能访问 router
    beforeCreate() {
      if (this.$options.router) {
        // 根组件,router 实例直接挂载到组件上
        this.$router = this.$options.router
        this.$route = this.$options.router.currentRoute.value
      } else {
        // 非根组件,通过 inject 获取 router 实例
        this.$router = inject(routerKey)
        this.$route = inject(routeLocationKey) //routeLocationKey是另外一个key,这里先忽略
      }
    }
  })

  // 添加全局组件,例如 <router-link> 和 <router-view>
  app.component('RouterLink', RouterLink)
  app.component('RouterView', RouterView)
}

这个 install 方法做了三件事:

  1. app.provide(routerKey, router) 这行代码是依赖注入的关键! provide 方法允许我们在应用级别提供一些数据, 并且指定一个 key, 这样,所有的子组件都可以通过这个 key 来获取这些数据。这里的 routerKey 是一个 Symbol,用来保证唯一性。

    const routerKey = Symbol('router')
    const routeLocationKey = Symbol('route location')
  2. app.mixin({...}) mixin 方法允许我们全局混入一些选项到所有的组件中。 在这里,我们混入了一个 beforeCreate 钩子函数。

    • 根组件的处理: 如果组件的 options 中有 router 选项(通常只有根组件才有), 那么就把 router 实例直接挂载到组件的 $router 属性上, 同时把当前的 route 信息挂载到 $route 属性上。
    • 非根组件的处理: 如果组件不是根组件, 那么就通过 inject(routerKey) 方法来获取之前通过 provide 方法提供的 router 实例, 同样挂载到 $router 属性上。 inject 方法就是用来接收 provide 数据的。
  3. 添加全局组件: 这部分代码负责注册 router-linkrouter-view 这两个全局组件, 这里我们先不展开讲。

第三幕:provideinject 的魔法

现在,我们来重点看看 provideinject 这两个 API。

  • provide provide 允许我们在父组件中提供一些数据, 供子组件使用。 它可以接收两个参数:

    • key: 一个字符串或者 Symbol, 用来标识提供的数据。
    • value: 任何类型的数据, 例如一个对象、一个函数、甚至是一个响应式的数据。
    // 在父组件中
    export default {
      provide: {
        message: 'Hello from parent!'
      },
      template: `
        <div>
          <child-component></child-component>
        </div>
      `
    }
  • inject inject 允许我们在子组件中获取父组件通过 provide 提供的数据。 它也可以接收一个参数:

    • key: 一个字符串或者 Symbol, 必须和 provide 中使用的 key 保持一致。
    // 在子组件中
    export default {
      inject: ['message'],
      mounted() {
        console.log(this.message) // 输出 "Hello from parent!"
      },
      template: `
        <div>
          <p>{{ message }}</p>
        </div>
      `
    }

    或者

    // 在子组件中
    export default {
      inject: {
        message: {
            from: 'message',
            default: 'default value'
        }
      },
      mounted() {
        console.log(this.message) // 输出 "Hello from parent!"
      },
      template: `
        <div>
          <p>{{ message }}</p>
        </div>
      `
    }

第四幕:Router 的 currentRoute,响应式的秘密

等等,还没完! this.$route 里的数据可是会实时更新的, 比如当 URL 发生变化时,this.$route.path 也会跟着变。 这是怎么做到的呢?

答案就在 Router 的 currentRoute 属性里。 它是一个 ref 对象,当 URL 发生变化时,会更新这个 ref 对象的值, Vue 的响应式系统会自动更新所有依赖这个 ref 对象的组件。

简化后的代码可能是这样的:

import { ref, computed } from 'vue'

// 创建一个 ref 对象,用来存储当前的 route 信息
const currentRoute = ref({
  path: '/',
  query: {},
  params: {}
})

// 创建一个 computed 属性,用来根据 currentRoute 计算一些衍生数据
const fullPath = computed(() => {
  let path = currentRoute.value.path
  const query = currentRoute.value.query
  if (query && Object.keys(query).length > 0) {
    path += '?' + Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&')
  }
  return path
})

// 一个模拟的 Router 类
class Router {
  constructor(options) {
    this.options = options
    this.currentRoute = currentRoute //暴露currentRoute
  }

  push(path) {
    // 模拟更新 URL 的操作
    history.pushState(null, null, path)

    // 更新 currentRoute 的值,触发响应式更新
    currentRoute.value = {
      path: path,
      query: {},
      params: {}
    }
  }
}

// 使用 Router
const router = new Router({
  history: 'browser',
  routes: []
})

// 模拟一个组件
const MyComponent = {
  template: `
    <div>
      <p>Current Path: {{ $route.path }}</p>
      <p>Full Path: {{ fullPath }}</p>
      <button @click="goTo('/about')">Go to About</button>
    </div>
  `,
  computed: {
    fullPath() {
      return router.currentRoute.value.path
    }
  },
  mounted() {
    console.log('Current Route:', this.$route)
  },
  methods: {
    goTo(path) {
      router.push(path)
    }
  }
}

在这个例子中,currentRoute 是一个 ref 对象, 当我们调用 router.push 方法时,会更新 currentRoute.value 的值, Vue 的响应式系统会自动更新 MyComponent 组件中所有使用了 this.$route 的地方。

总结:依赖注入,优雅的解耦

现在,我们来总结一下:

  1. 通过 createApp 创建 Vue 应用,并使用 app.use(router) 安装 Router 插件。
  2. Router 的 install 方法使用 app.provide(routerKey, router) 提供 Router 实例。
  3. Router 的 install 方法使用 app.mixin({...}) 全局混入 beforeCreate 钩子函数, 在钩子函数中使用 inject(routerKey) 获取 Router 实例, 并挂载到组件的 $router 属性上。
  4. Router 使用 ref 对象来存储当前的 route 信息, 并通过 Vue 的响应式系统来更新组件。

用表格来概括一下:

步骤 核心代码 作用
1. 安装 Router app.use(router) 调用 Router 的 install 方法
2. 提供 Router app.provide(routerKey, router) 在应用级别提供 Router 实例
3. 注入 Router inject(routerKey) 在组件中获取 Router 实例
4. 响应式更新 currentRoute = ref(...)currentRoute.value = ... 使用 ref 对象存储 route 信息,并通过 Vue 的响应式系统更新组件

这种依赖注入的方式, 有效地解耦了组件和 Router 实例, 使得组件可以独立于 Router 实例进行开发和测试。

举一反三:不只是 Router

其实,这种依赖注入的模式在 Vue 3 中非常常见。 例如,Vuex 的 store 实例也是通过类似的方式注入到组件中的。

// Vuex 的 install 方法
function install(app, store) {
  app.provide(storeKey, store)

  app.mixin({
    beforeCreate() {
      const options = this.$options
      if (options.store) {
        this.$store = options.store
      } else if (options.parent && options.parent.$store) {
        this.$store = options.parent.$store
      }
    }
  })
}

可以看到, Vuex 的 install 方法和 Router 的 install 方法非常相似, 都是通过 provideinject 来实现依赖注入。

总结的总结:源码的乐趣

通过这次“扒皮”,我们了解了 Vue Router 实例是如何被注入到组件中的。 希望这次的分享能帮助你更好地理解 Vue 3 的源码, 也希望你能从中体会到源码的乐趣!

下次有机会,我们再来聊聊 router-linkrouter-view 这两个全局组件的实现原理, 保证精彩!

各位观众老爷,咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注