Logo个人技术学习笔记

从 0 到 1 手写 Vue3 上层核心:组件通信、路由、状态管理(面试+实战版)

2026-05-30T19:15:27
213 阅读
vue

前言

在上一篇文章《从 0 到 1 手写 Vue3 核心原理》中,我们吃透了 Vue3 底层的「响应式、虚拟 DOM、computed/watch」,搞定了面试中最硬核的底层原理问题。

但实际开发中,我们更常接触「上层应用核心」—— 组件之间怎么传值?页面怎么跳转?全局数据怎么管理?这些也是面试高频考点,很多同学只会用 API,却不懂底层逻辑。

今天这篇文章,延续上一篇「手写简化版」风格,不堆砌 API,从 0 手写「组件通信核心、简易 Vue Router、简易 Pinia」,帮你打通「底层原理→上层应用」的任督二脉,既会用、又懂原理!

💡 本文你将收获:

  1. 彻底理解 Vue3 组件通信 3 种核心方式的底层逻辑
  2. 手写简易 Vue Router(吃透 hash 监听、路由匹配核心)
  3. 手写简易 Pinia(理解状态管理的响应式本质)
  4. 掌握面试中「组件通信、路由、状态管理」的高频问答
  5. 获得可直接运行的手写简化版代码(复用上一篇响应式核心)

一、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 相关问题,你既能讲底层原理,又能说上层应用,直接秒杀面试官!