前言
在上一篇文章《从 0 到 1 手写 Vue3 核心原理》中,我们吃透了 Vue3 底层的「响应式、虚拟 DOM、computed/watch」,搞定了面试中最硬核的底层原理问题。
但实际开发中,我们更常接触「上层应用核心」—— 组件之间怎么传值?页面怎么跳转?全局数据怎么管理?这些也是面试高频考点,很多同学只会用 API,却不懂底层逻辑。
今天这篇文章,延续上一篇「手写简化版」风格,不堆砌 API,从 0 手写「组件通信核心、简易 Vue Router、简易 Pinia」,帮你打通「底层原理→上层应用」的任督二脉,既会用、又懂原理!
💡 本文你将收获:
- 彻底理解 Vue3 组件通信 3 种核心方式的底层逻辑
- 手写简易 Vue Router(吃透 hash 监听、路由匹配核心)
- 手写简易 Pinia(理解状态管理的响应式本质)
- 掌握面试中「组件通信、路由、状态管理」的高频问答
- 获得可直接运行的手写简化版代码(复用上一篇响应式核心)
一、Vue3 上层核心整体逻辑(衔接底层)
上一篇我们手写了 Vue3 底层「响应式系统(reactive/ref/effect)」,而「组件通信、路由、状态管理」,本质上都是「基于响应式系统的上层封装」:
// 核心逻辑衔接(复用上一篇响应式核心)
Vue3 上层核心 = 响应式系统(底层) + 特定场景封装
- 组件通信:响应式数据的跨组件传递
- 路由:hash/history 监听 + 响应式路由状态
- 状态管理:全局响应式数据的统一管理
也就是说,掌握了底层响应式,再看上层核心,本质都是“换汤不换药”—— 所有功能都围绕「响应式数据的读取、修改、触发更新」展开。
二、组件通信:3 种核心方式(手写简化版)
Vue3 组件通信的核心的是「数据的跨组件传递+响应式更新」,重点掌握 3 种高频方式,手写核心逻辑,拒绝死记 API。
1. props / emits:父子组件通信(最基础)
核心逻辑
父组件传递响应式数据给子组件(props),子组件通过触发事件(emits)向父组件传递数据,本质是「响应式数据的单向传递+事件回调」。
手写简化版(可直接运行)
// 复用上一篇的响应式核心(reactive/ref)
function ref(val) { /* 上一篇手写代码,此处省略 */ }
// 1. 子组件
function Child(props, emits) {
return {
// 读取父组件传递的 props(响应式)
render() {
return `<div>子组件接收:${props.msg},count:${props.count.value}</div>`
},
// 子组件触发事件,向父组件传值
handleClick() {
emits('change-count', props.count.value + 1)
}
}
}
// 2. 父组件
function Parent() {
const count = ref(1)
const msg = '父组件传递的消息'
// 接收子组件传递的数据,修改响应式数据
const handleChangeCount = (newVal) => {
count.value = newVal
}
// 传递 props 和 emits 给子组件
const child = Child({ msg, count }, { 'change-count': handleChangeCount })
return {
render() {
return `<div>${child.render()}<button onclick="child.handleClick()">修改count</button></div>`
}
}
}
面试考点
❓ 问:props 为什么是单向数据流?
✅ 答:为了避免组件数据混乱,子组件不能直接修改 props(修改不会触发父组件更新),必须通过 emits 触发父组件修改,保证数据来源唯一。
2. provide / inject:跨层级组件通信
核心逻辑
父组件通过 provide 提供响应式数据,所有子组件(无论层级多深)都能通过 inject 获取数据,本质是「响应式数据的全局共享(局部作用域)」。
手写简化版
// 复用上一篇的 reactive
function reactive(target) { /* 上一篇手写代码,此处省略 */ }
// 1. 全局存储 provide 的数据
const provideMap = new Map()
// 2. provide:提供数据(父组件)
function provide(key, value) {
provideMap.set(key, value)
}
// 3. inject:获取数据(子/孙组件)
function inject(key) {
return provideMap.get(key)
}
// 测试
function GrandParent() {
// 提供响应式数据
const globalData = reactive({ name: 'Vue3', version: '3.4.15' })
provide('globalData', globalData)
return {
render() {
return `<div>${Parent().render()}</div>`
}
}
}
function Parent() {
return {
render() {
return `<div>${Child().render()}</div>`
}
}
}
function Child() {
// 跨层级获取数据
const globalData = inject('globalData')
return {
render() {
return `<div>全局数据:${globalData.name} ${globalData.version}</div>`
}
}
}
3. mitt:事件总线(任意组件通信)
核心逻辑
通过一个全局事件中心,实现任意组件之间的通信(发布-订阅模式),本质是「事件的监听与触发」,适合非父子、跨层级的简单通信。
手写简易 mitt(30 行核心代码)
// 手写简易 mitt 事件总线
function mitt() {
// 存储事件:key=事件名,value=事件回调数组
const events = new Map()
return {
// 1. 监听事件
on(eventName, callback) {
if (!events.has(eventName)) {
events.set(eventName, [])
}
events.get(eventName).push(callback)
},
// 2. 触发事件(可传参)
emit(eventName, ...args) {
if (events.has(eventName)) {
events.get(eventName).forEach(callback => callback(...args))
}
},
// 3. 取消监听
off(eventName, callback) {
if (events.has(eventName)) {
const callbacks = events.get(eventName)
const index = callbacks.findIndex(cb => cb === callback)
index !== -1 && callbacks.splice(index, 1)
}
}
}
}
// 测试(任意组件通信)
const bus = mitt()
// 组件A:触发事件
function ComponentA() {
return {
sendMsg() {
bus.emit('send-msg', '来自组件A的消息')
}
}
}
// 组件B:监听事件
function ComponentB() {
bus.on('send-msg', (msg) => {
console.log('组件B收到:', msg) // 输出:组件B收到:来自组件A的消息
})
}
三、Vue Router 核心:手写简易路由(面试高频)
Vue Router 的核心是「监听 URL 变化,匹配对应组件,渲染到页面」,重点掌握 hash 模式(最常用),手写核心逻辑,吃透底层原理。
1. 核心原理(3 步)
1. 监听 hash 变化(window.onhashchange)
2. 定义路由规则(路径 → 组件)
3. 匹配路由规则,渲染对应组件
2. 手写简易 Vue Router(50 行核心代码)
// 手写简易 Vue Router(hash 模式)
function createRouter(routes) {
return {
// 路由规则(路径→组件)
routes,
// 当前路由(响应式,复用 ref)
currentRoute: ref({ path: '/', name: 'home' }),
// 初始化:监听 hash 变化
init() {
// 1. 初始渲染(页面刷新时)
this.handleHashChange()
// 2. 监听 hash 变化
window.addEventListener('hashchange', () => this.handleHashChange())
},
// 处理 hash 变化
handleHashChange() {
// 获取当前 hash(去掉 #)
const hash = window.location.hash.slice(1) || '/'
// 匹配路由规则
const matchedRoute = this.routes.find(route => route.path === hash)
// 更新当前路由(响应式更新,触发页面渲染)
this.currentRoute.value = matchedRoute || { path: '/', name: '404' }
},
// 跳转路由(修改 hash)
push(path) {
window.location.hash = path
}
}
}
// 测试
// 1. 定义路由规则
const routes = [
{ path: '/', name: 'home', component: () => '<div>首页</div>' },
{ path: '/about', name: 'about', component: () => '<div>关于页</div>' },
{ path: '/user', name: 'user', component: () => '<div>用户页</div>' }
]
// 2. 创建路由实例
const router = createRouter(routes)
router.init()
// 3. 渲染路由(响应式,hash 变化自动重新渲染)
effect(() => {
const app = document.querySelector('#app')
app.innerHTML = router.currentRoute.value.component()
})
3. 面试考点
❓ 问:Vue Router hash 模式和 history 模式的区别?
✅ 答:
- hash 模式:基于 URL 中的 #(锚点),监听 onhashchange 事件,兼容性好,无需后端配置。
- history 模式:基于 HTML5 的 history API(pushState/replaceState),URL 无 #,美观,但刷新页面会 404,需要后端配置(所有路由指向 index.html)。
四、Pinia 核心:手写简易状态管理(响应式本质)
Pinia 是 Vue3 官方推荐的状态管理工具,核心是「全局响应式数据的统一管理」,本质是「reactive 封装 + 全局共享」,手写简化版,吃透核心逻辑。
1. 核心原理
用 reactive 创建全局响应式数据,通过 getters 实现计算属性(复用 computed),通过 actions 实现修改数据的方法(支持异步),本质是「响应式数据的全局封装」。
2. 手写简易 Pinia(40 行核心代码)
// 复用上一篇的 reactive 和 computed
function reactive(target) { /* 上一篇手写代码,此处省略 */ }
function computed(getter) { /* 上一篇手写代码,此处省略 */ }
// 手写简易 Pinia
function createPinia() {
// 存储所有 store
const stores = new Map()
return {
// 创建 store
createStore(options) {
const { state, getters, actions } = options
// 1. 状态(响应式)
const stateProxy = reactive(state())
// 2. getters(计算属性,依赖状态)
const gettersProxy = {}
Object.keys(getters).forEach(key => {
gettersProxy[key] = computed(() => getters[key](stateProxy))
})
// 3. actions(修改状态,支持异步)
const actionsProxy = {}
Object.keys(actions).forEach(key => {
actionsProxy[key] = (...args) => actions[key].call(stateProxy, stateProxy, ...args)
})
// 存储 store,全局共享
const store = { state: stateProxy, getters: gettersProxy, actions: actionsProxy }
stores.set(options.id, store)
return store
}
}
}
// 测试
// 1. 创建 pinia 实例
const pinia = createPinia()
// 2. 创建 store
const userStore = pinia.createStore({
id: 'user',
state: () => ({
name: '张三',
age: 20
}),
getters: {
// 计算属性(依赖 state)
fullInfo: (state) => `${state.name}, ${state.age}岁`
},
actions: {
// 修改状态(支持异步)
changeName(newName) {
this.name = newName // this 指向 stateProxy
},
async fetchAge() {
// 模拟异步请求
const res = await new Promise(resolve => setTimeout(() => resolve(22), 1000))
this.age = res
}
}
})
// 3. 使用 store(响应式,修改自动更新)
effect(() => {
console.log('用户信息:', userStore.getters.fullInfo.value)
})
// 修改状态
userStore.actions.changeName('李四')
userStore.actions.fetchAge()
3. 面试考点
❓ 问:Pinia 和 Vuex 的区别?
✅ 答:
- Pinia 更简洁,无需 mutations(直接在 actions 中修改状态),支持异步 actions。
- Pinia 天生支持 Vue3 组合式 API,体积更小,性能更好。
- Pinia 无需模块嵌套,可创建多个 store,管理更灵活。
五、完整版可运行代码(衔接上一篇)
<!DOCTYPE html>
<html lang="zh-CN">
<body>
<div id="app"></div>
<script>
// ==================== 复用上一篇:响应式核心 ====================
let activeEffect = null
const targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let dep = depsMap.get(key)
if (!dep) depsMap.set(key, (dep = new Set()))
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) dep.forEach(effect => effect())
}
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
},
set(target, key, value, receiver) {
const oldVal = target[key]
const ok = Reflect.set(target, key, value, receiver)
if (oldVal !== value) trigger(target, key)
return ok
}
})
}
function ref(val) {
return {
get value() {
track(this, 'value')
return val
},
set value(newVal) {
val = newVal
trigger(this, 'value')
}
}
}
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
function computed(getter) {
const value = ref()
effect(() => {
value.value = getter()
})
return value
}
// ==================== 本章新增:组件通信 + 路由 + Pinia ====================
// 1. 组件通信:mitt 事件总线
function mitt() {
const events = new Map()
return {
on(eventName, callback) {
if (!events.has(eventName)) events.set(eventName, [])
events.get(eventName).push(callback)
},
emit(eventName, ...args) {
events.has(eventName) && events.get(eventName).forEach(cb => cb(...args))
}
}
}
// 2. 简易 Vue Router
function createRouter(routes) {
const currentRoute = ref({ path: '/', name: 'home' })
const handleHashChange = () => {
const hash = window.location.hash.slice(1) || '/'
const matched = routes.find(route => route.path === hash)
currentRoute.value = matched || { path: '/', name: '404', component: () => '<div>404页面</div>' }
}
return {
routes, currentRoute,
init() {
handleHashChange()
window.addEventListener('hashchange', handleHashChange)
},
push(path) { window.location.hash = path }
}
}
// 3. 简易 Pinia
function createPinia() {
const stores = new Map()
return {
createStore(options) {
const stateProxy = reactive(options.state())
const gettersProxy = {}
Object.keys(options.getters).forEach(key => {
gettersProxy[key] = computed(() => options.getters[key](stateProxy))
})
const actionsProxy = {}
Object.keys(options.actions).forEach(key => {
actionsProxy[key] = (...args) => options.actions[key].call(stateProxy, stateProxy, ...args)
})
const store = { state: stateProxy, getters: gettersProxy, actions: actionsProxy }
stores.set(options.id, store)
return store
}
}
}
// ==================== 测试代码 ====================
// 测试 Pinia
const pinia = createPinia()
const userStore = pinia.createStore({
id: 'user',
state: () => ({ name: '张三', age: 20 }),
getters: { fullInfo: (state) => `${state.name}, ${state.age}岁` },
actions: { changeName(newName) { this.name = newName } }
})
// 测试路由
const routes = [
{ path: '/', component: () => `<div>首页 | ${userStore.getters.fullInfo.value}</div>` },
{ path: '/about', component: () => `<div>关于页 | ${userStore.getters.fullInfo.value}</div>` }
]
const router = createRouter(routes)
router.init()
// 测试组件通信(mitt)
const bus = mitt()
bus.on('change-name', (newName) => {
userStore.actions.changeName(newName)
})
// 渲染页面(响应式更新)
effect(() => {
const app = document.querySelector('#app')
app.innerHTML = `
${router.currentRoute.value.component()}
<button onclick="router.push('/about')">跳转到关于页</button>
<button onclick="bus.emit('change-name', '李四')">修改用户名</button>
`
})
</script>
</body>
</html>
六、面试必背总结(衔接上一篇)
1. 组件通信
- props/emits:父子组件,单向数据流,子组件通过事件回调传值。
- provide/inject:跨层级,局部全局共享,依赖响应式。
- mitt:任意组件,发布-订阅模式,适合简单通信。
2. Vue Router
- 核心:监听 URL 变化,匹配路由规则,渲染对应组件。
- hash 模式:基于 #,onhashchange 监听,无需后端配置。
- history 模式:基于 history API,需后端配置,URL 无 #。
3. Pinia
- 核心:基于 reactive 封装,全局响应式数据管理。
- getters:计算属性,依赖 state,带缓存。
- actions:修改 state,支持异步,无需 mutations。
🔥 究极总结:Vue 到底是什么?(看透本质版)
写到这里,我们可以跳出 API、跳出源码,直接看透 Vue 的本质:
1. 组件 —— 本质就是函数
- 一个组件,就是一个函数
props是函数的入参emit是函数的回调- 组件嵌套,就是父函数调用子函数
- 组件通信,本质就是函数之间传参、回调、通知
没有任何黑魔法,组件体系 = 函数调用体系。
2. Vue 应用 —— 本质就是单页里的一个大运行时
- 整个页面只有 一个 HTML、一个全局环境(window)
- 所有组件、逻辑、工具库,都跑在 同一个全局作用域 里
- 所谓“页面切换”,根本没有刷新页面,只是组件替换 + DOM 更新
所以:
Vue 就是一个跑在单页里的、长期存活的大型 JS 程序。
3. Router、Pinia —— 本质就是全局变量
为什么它们能跨组件共享、跨页面生效?
Vue Router:维护一个全局响应式对象,记录当前路径Pinia:维护一个全局响应式状态树,存放全局数据- 组件从同一个全局变量里读数据、写数据
- 变化依靠响应式系统通知所有用到的地方更新
一句话:
路由、状态管理,本质都是:全局变量 + 响应式。
🔥 最终一句话总结(全文最强金句)
Vue 组件 = 函数 组件通信 = 函数调用与传参 Vue 应用 = 单页全局运行时 Router / Pinia = 全局响应式变量 响应式 = 监听变量变化并自动更新
你看透这一层, Vue 所有源码、API、生态,在你面前全是透明的。
结尾
到这里,你已经打通了 Vue3「底层原理→上层应用」的完整逻辑:
上一篇:吃透 reactive/ref/computed/watch 底层响应式;
这一篇:吃透组件通信、路由、状态管理上层核心。
以后面试再问 Vue3 相关问题,你既能讲底层原理,又能说上层应用,直接秒杀面试官!