各位观众老爷,大家好!我是你们的老朋友,今天咱们来聊聊 Vue 3 源码里一个很有意思的话题:Vue Router 实例是怎么被“偷偷塞进”组件里的?听起来是不是有点谍战片的味道? 别担心,咱们用最轻松的方式,把这事儿给扒个底朝天!
开场白:组件,你从哪里来?我的 Router
相信各位对 Vue 组件都不陌生,但你有没有想过,为啥每个组件都能直接用 this.$router
和 this.$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
方法做了三件事:
-
app.provide(routerKey, router)
: 这行代码是依赖注入的关键!provide
方法允许我们在应用级别提供一些数据, 并且指定一个key
, 这样,所有的子组件都可以通过这个key
来获取这些数据。这里的routerKey
是一个 Symbol,用来保证唯一性。const routerKey = Symbol('router') const routeLocationKey = Symbol('route location')
-
app.mixin({...})
:mixin
方法允许我们全局混入一些选项到所有的组件中。 在这里,我们混入了一个beforeCreate
钩子函数。- 根组件的处理: 如果组件的
options
中有router
选项(通常只有根组件才有), 那么就把router
实例直接挂载到组件的$router
属性上, 同时把当前的route
信息挂载到$route
属性上。 - 非根组件的处理: 如果组件不是根组件, 那么就通过
inject(routerKey)
方法来获取之前通过provide
方法提供的router
实例, 同样挂载到$router
属性上。inject
方法就是用来接收provide
数据的。
- 根组件的处理: 如果组件的
-
添加全局组件: 这部分代码负责注册
router-link
和router-view
这两个全局组件, 这里我们先不展开讲。
第三幕:provide
和 inject
的魔法
现在,我们来重点看看 provide
和 inject
这两个 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
的地方。
总结:依赖注入,优雅的解耦
现在,我们来总结一下:
- 通过
createApp
创建 Vue 应用,并使用app.use(router)
安装 Router 插件。 - Router 的
install
方法使用app.provide(routerKey, router)
提供 Router 实例。 - Router 的
install
方法使用app.mixin({...})
全局混入beforeCreate
钩子函数, 在钩子函数中使用inject(routerKey)
获取 Router 实例, 并挂载到组件的$router
属性上。 - 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
方法非常相似, 都是通过 provide
和 inject
来实现依赖注入。
总结的总结:源码的乐趣
通过这次“扒皮”,我们了解了 Vue Router 实例是如何被注入到组件中的。 希望这次的分享能帮助你更好地理解 Vue 3 的源码, 也希望你能从中体会到源码的乐趣!
下次有机会,我们再来聊聊 router-link
和 router-view
这两个全局组件的实现原理, 保证精彩!
各位观众老爷,咱们下期再见!