HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • Vue & CSS 进阶

    • 第0章:工具链与开发体验
    • 第1章:TypeScript 核心
    • 第2章:CSS 现代能力
    • 第3章:Vue3 核心与原理
    • 第4章:路由与状态管理
    • 第5章:工程化与性能
    • 第6章:测试与质量
    • 第7章:CSS 进阶专题
    • 第8章:项目实战A - SaaS仪表盘
    • 第9章:项目实战B - 内容社区
    • 第10章:Vue 内核深入
    • 第11章:微前端与部署
    • CSS 变量体系与主题系统深度指南
    • 前端安全与防护专题
    • CSS 专题:动效优化与微交互
    • CSS 专题:图片与图形处理
    • 实时通信与WebSocket专题
    • 开发工具与调试技巧专题
    • 新兴技术与未来趋势专题
    • 移动端开发与PWA专题
    • 附录A:代码片段库
    • 附录B:练习题集

第10章:Vue 内核深入

深入理解 Vue3 内部机制,掌握调度器、Diff 算法、Transition 系统等核心原理

📚 本章目标

通过本章学习,你将掌握:

  • 调度器与批量更新机制
  • Diff 算法原理与实现
  • Transition 过渡系统
  • 异步组件与 Suspense
  • 自定义渲染器开发

⚡ 10.1 调度器与批量更新

10.1.1 调度器核心概念

Vue3 的调度器负责管理异步更新,确保在同一个事件循环中多次数据变更只触发一次重新渲染。

// 调度器核心实现
class Scheduler {
  private queue: Function[] = []
  private isFlushing = false
  private isFlushPending = false

  // 添加任务到队列
  queueJob(job: Function) {
    if (!this.queue.includes(job)) {
      this.queue.push(job)
    }
    this.queueFlush()
  }

  // 刷新队列
  private queueFlush() {
    if (!this.isFlushing && !this.isFlushPending) {
      this.isFlushPending = true
      // 使用微任务执行
      Promise.resolve().then(() => {
        this.flushJobs()
      })
    }
  }

  // 执行队列中的任务
  private flushJobs() {
    this.isFlushPending = false
    this.isFlushing = true

    // 按 id 排序,确保父组件在子组件之前更新
    this.queue.sort((a, b) => a.id - b.id)

    try {
      for (let i = 0; i < this.queue.length; i++) {
        const job = this.queue[i]
        job()
      }
    } finally {
      this.queue.length = 0
      this.isFlushing = false
    }
  }
}

const scheduler = new Scheduler()

10.1.2 nextTick 实现

// nextTick 实现
const callbacks: Function[] = []
let pending = false

function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

function nextTick(cb?: Function): Promise<void> {
  return new Promise((resolve) => {
    callbacks.push(() => {
      if (cb) cb()
      resolve()
    })
    
    if (!pending) {
      pending = true
      // 优先使用微任务
      if (typeof Promise !== 'undefined') {
        Promise.resolve().then(flushCallbacks)
      } else if (typeof MutationObserver !== 'undefined') {
        const textNode = document.createTextNode('')
        const observer = new MutationObserver(flushCallbacks)
        observer.observe(textNode, { characterData: true })
        textNode.data = '1'
      } else {
        setTimeout(flushCallbacks, 0)
      }
    }
  })
}

10.1.3 批量更新示例

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ double }}</p>
    <button @click="updateMultiple">批量更新</button>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'

const count = ref(0)
const double = computed(() => count.value * 2)

const updateMultiple = async () => {
  console.log('更新前:', count.value, double.value) // 0, 0
  
  // 多次同步更新
  count.value = 1
  count.value = 2
  count.value = 3
  
  console.log('同步更新后:', count.value, double.value) // 3, 6
  
  // 等待 DOM 更新
  await nextTick()
  console.log('DOM 更新后:', count.value, double.value) // 3, 6
}
</script>

🔄 10.2 Diff 算法详解

10.2.1 同层比较策略

Vue3 的 Diff 算法采用同层比较策略,避免跨层级的比较,提高性能。

// 简化的 Diff 算法实现
function diffChildren(
  oldChildren: VNode[],
  newChildren: VNode[],
  container: Element
) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  let newEndIdx = newChildren.length - 1
  
  let oldStartVNode = oldChildren[oldStartIdx]
  let oldEndVNode = oldChildren[oldEndIdx]
  let newStartVNode = newChildren[newStartIdx]
  let newEndVNode = newChildren[newEndIdx]

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 跳过已处理的节点
    if (!oldStartVNode) {
      oldStartVNode = oldChildren[++oldStartIdx]
    } else if (!oldEndVNode) {
      oldEndVNode = oldChildren[--oldEndIdx]
    }
    // 头头比较
    else if (isSameVNode(oldStartVNode, newStartVNode)) {
      patchVNode(oldStartVNode, newStartVNode, container)
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    }
    // 尾尾比较
    else if (isSameVNode(oldEndVNode, newEndVNode)) {
      patchVNode(oldEndVNode, newEndVNode, container)
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    }
    // 头尾比较
    else if (isSameVNode(oldStartVNode, newEndVNode)) {
      patchVNode(oldStartVNode, newEndVNode, container)
      insertBefore(container, oldStartVNode.el, oldEndVNode.el.nextSibling)
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    }
    // 尾头比较
    else if (isSameVNode(oldEndVNode, newStartVNode)) {
      patchVNode(oldEndVNode, newStartVNode, container)
      insertBefore(container, oldEndVNode.el, oldStartVNode.el)
      oldEndVNode = oldChildren[--oldEndIdx]
      newStartVNode = newChildren[++newStartIdx]
    }
    // 都不匹配,查找新节点在旧节点中的位置
    else {
      const idxInOld = findIdxInOld(newStartVNode, oldChildren, oldStartIdx, oldEndIdx)
      if (idxInOld > 0) {
        const vnodeToMove = oldChildren[idxInOld]
        patchVNode(vnodeToMove, newStartVNode, container)
        insertBefore(container, vnodeToMove.el, oldStartVNode.el)
        oldChildren[idxInOld] = undefined
      } else {
        // 新节点,直接创建
        mountElement(newStartVNode, container, oldStartVNode.el)
      }
      newStartVNode = newChildren[++newStartIdx]
    }
  }

  // 处理剩余节点
  if (oldStartIdx > oldEndIdx) {
    // 新节点有剩余,批量插入
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      mountElement(newChildren[i], container, newEndVNode.el.nextSibling)
    }
  } else if (newStartIdx > newEndIdx) {
    // 旧节点有剩余,批量删除
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      if (oldChildren[i]) {
        unmount(oldChildren[i])
      }
    }
  }
}

function isSameVNode(n1: VNode, n2: VNode): boolean {
  return n1.type === n2.type && n1.key === n2.key
}

10.2.2 Key 的作用

Key 是 Diff 算法的重要优化手段,帮助 Vue 识别节点的身份。

<template>
  <div>
    <h3>没有 Key 的列表(性能较差)</h3>
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.name }}
        <input type="text" />
      </li>
    </ul>

    <h3>有 Key 的列表(性能更好)</h3>
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.name }}
        <input type="text" />
      </li>
    </ul>

    <button @click="addItem">添加项目</button>
    <button @click="removeItem">删除项目</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface Item {
  id: number
  name: string
}

const items = ref<Item[]>([
  { id: 1, name: '项目 1' },
  { id: 2, name: '项目 2' },
  { id: 3, name: '项目 3' }
])

const addItem = () => {
  const newId = Math.max(...items.value.map(i => i.id)) + 1
  items.value.push({ id: newId, name: `项目 ${newId}` })
}

const removeItem = () => {
  if (items.value.length > 0) {
    items.value.pop()
  }
}
</script>

10.2.3 最长递增子序列优化

Vue3 使用最长递增子序列算法优化移动节点的性能。

// 最长递增子序列算法
function getSequence(arr: number[]): number[] {
  const p = arr.slice()
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      u = 0
      v = result.length - 1
      while (u < v) {
        c = (u + v) >> 1
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

🎭 10.3 Transition 过渡系统

10.3.1 过渡类名机制

Vue3 的 Transition 组件通过类名控制过渡效果。

<template>
  <div>
    <button @click="show = !show">切换</button>
    
    <Transition name="fade">
      <div v-if="show" class="box">内容</div>
    </Transition>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const show = ref(false)
</script>

<style scoped>
/* 过渡类名 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.box {
  width: 100px;
  height: 100px;
  background: #42b883;
  margin: 20px 0;
}
</style>

10.3.2 过渡钩子函数

<template>
  <div>
    <button @click="show = !show">切换</button>
    
    <Transition
      name="slide"
      @before-enter="onBeforeEnter"
      @enter="onEnter"
      @after-enter="onAfterEnter"
      @before-leave="onBeforeLeave"
      @leave="onLeave"
      @after-leave="onAfterLeave"
    >
      <div v-if="show" class="box">内容</div>
    </Transition>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const show = ref(false)

const onBeforeEnter = (el: Element) => {
  console.log('进入前')
  el.setAttribute('data-old-overflow', (el as HTMLElement).style.overflow)
  ;(el as HTMLElement).style.overflow = 'hidden'
}

const onEnter = (el: Element, done: () => void) => {
  console.log('进入中')
  const el = el as HTMLElement
  el.style.height = '0px'
  el.offsetHeight // 触发重排
  el.style.transition = 'height 0.3s ease'
  el.style.height = '100px'
  
  el.addEventListener('transitionend', done, { once: true })
}

const onAfterEnter = (el: Element) => {
  console.log('进入后')
  const el = el as HTMLElement
  el.style.height = 'auto'
  el.style.overflow = el.getAttribute('data-old-overflow') || ''
}

const onBeforeLeave = (el: Element) => {
  console.log('离开前')
  const el = el as HTMLElement
  el.style.height = el.offsetHeight + 'px'
  el.offsetHeight // 触发重排
}

const onLeave = (el: Element, done: () => void) => {
  console.log('离开中')
  const el = el as HTMLElement
  el.style.transition = 'height 0.3s ease'
  el.style.height = '0px'
  
  el.addEventListener('transitionend', done, { once: true })
}

const onAfterLeave = (el: Element) => {
  console.log('离开后')
}
</script>

<style scoped>
.slide-enter-active,
.slide-leave-active {
  transition: height 0.3s ease;
}

.box {
  width: 100px;
  background: #42b883;
  margin: 20px 0;
  overflow: hidden;
}
</style>

10.3.3 列表过渡

<template>
  <div>
    <button @click="addItem">添加项目</button>
    <button @click="removeItem">删除项目</button>
    
    <TransitionGroup name="list" tag="ul">
      <li v-for="item in items" :key="item.id" class="list-item">
        {{ item.name }}
        <button @click="removeItemById(item.id)">删除</button>
      </li>
    </TransitionGroup>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface Item {
  id: number
  name: string
}

const items = ref<Item[]>([
  { id: 1, name: '项目 1' },
  { id: 2, name: '项目 2' },
  { id: 3, name: '项目 3' }
])

let nextId = 4

const addItem = () => {
  items.value.push({ id: nextId++, name: `项目 ${nextId - 1}` })
}

const removeItem = () => {
  if (items.value.length > 0) {
    items.value.pop()
  }
}

const removeItemById = (id: number) => {
  const index = items.value.findIndex(item => item.id === id)
  if (index > -1) {
    items.value.splice(index, 1)
  }
}
</script>

<style scoped>
.list-enter-active,
.list-leave-active {
  transition: all 0.3s ease;
}

.list-enter-from {
  opacity: 0;
  transform: translateX(-30px);
}

.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

.list-move {
  transition: transform 0.3s ease;
}

.list-item {
  padding: 10px;
  margin: 5px 0;
  background: #f0f0f0;
  border-radius: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>

🔄 10.4 异步组件与 Suspense

10.4.1 异步组件定义

// 基础异步组件
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

// 高级异步组件
const AdvancedAsyncComponent = defineAsyncComponent({
  loader: () => import('./AdvancedComponent.vue'),
  loadingComponent: {
    template: '<div class="loading">加载中...</div>'
  },
  errorComponent: {
    template: '<div class="error">加载失败</div>'
  },
  delay: 200,
  timeout: 3000,
  onError(error, retry, fail, attempts) {
    if (error.message.match(/fetch/) && attempts <= 3) {
      retry()
    } else {
      fail()
    }
  }
})

10.4.2 Suspense 使用

<template>
  <div>
    <h1>异步组件示例</h1>
    
    <Suspense>
      <template #default>
        <AsyncDataComponent />
      </template>
      <template #fallback>
        <div class="loading">加载中...</div>
      </template>
    </Suspense>
  </div>
</template>

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

// 异步数据组件
const AsyncDataComponent = defineAsyncComponent(async () => {
  // 模拟异步数据获取
  const data = await fetchData()
  return {
    template: `
      <div>
        <h2>数据加载完成</h2>
        <p>数据: {{ data }}</p>
      </div>
    `,
    setup() {
      return { data }
    }
  }
})

async function fetchData() {
  await new Promise(resolve => setTimeout(resolve, 2000))
  return '这是异步获取的数据'
}
</script>

<style scoped>
.loading {
  text-align: center;
  padding: 20px;
  color: #666;
}
</style>

10.4.3 异步组件最佳实践

<template>
  <div>
    <Suspense>
      <template #default>
        <LazyChart :data="chartData" />
      </template>
      <template #fallback>
        <ChartSkeleton />
      </template>
    </Suspense>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const chartData = ref(null)

// 懒加载图表组件
const LazyChart = defineAsyncComponent({
  loader: () => import('@/components/Chart.vue'),
  loadingComponent: {
    template: '<div class="chart-loading">图表加载中...</div>'
  },
  errorComponent: {
    template: '<div class="chart-error">图表加载失败</div>'
  },
  delay: 100,
  timeout: 5000
})

// 骨架屏组件
const ChartSkeleton = {
  template: `
    <div class="chart-skeleton">
      <div class="skeleton-header"></div>
      <div class="skeleton-chart"></div>
      <div class="skeleton-legend"></div>
    </div>
  `
}

onMounted(async () => {
  // 模拟数据获取
  await new Promise(resolve => setTimeout(resolve, 1000))
  chartData.value = {
    labels: ['一月', '二月', '三月', '四月', '五月'],
    datasets: [{
      label: '销售额',
      data: [12, 19, 3, 5, 2]
    }]
  }
})
</script>

<style scoped>
.chart-skeleton {
  padding: 20px;
}

.skeleton-header {
  height: 20px;
  background: #f0f0f0;
  border-radius: 4px;
  margin-bottom: 20px;
}

.skeleton-chart {
  height: 300px;
  background: #f0f0f0;
  border-radius: 4px;
  margin-bottom: 20px;
}

.skeleton-legend {
  height: 40px;
  background: #f0f0f0;
  border-radius: 4px;
}
</style>

🎨 10.5 自定义渲染器

10.5.1 基础自定义渲染器

// 自定义渲染器实现
import { createRenderer } from '@vue/runtime-core'

const { createApp } = createRenderer({
  // 创建元素
  createElement(type, isSVG, isCustomizedBuiltIn) {
    const el = document.createElement(type)
    return el
  },

  // 设置元素文本内容
  setElementText(el, text) {
    el.textContent = text
  },

  // 插入元素
  insert(el, parent, anchor) {
    parent.insertBefore(el, anchor || null)
  },

  // 移除元素
  remove(el) {
    const parent = el.parentNode
    if (parent) {
      parent.removeChild(el)
    }
  },

  // 设置元素属性
  patchProp(el, key, prevValue, nextValue) {
    if (key.startsWith('on')) {
      // 事件处理
      const eventName = key.slice(2).toLowerCase()
      if (prevValue) {
        el.removeEventListener(eventName, prevValue)
      }
      if (nextValue) {
        el.addEventListener(eventName, nextValue)
      }
    } else {
      // 属性设置
      if (nextValue === null || nextValue === undefined) {
        el.removeAttribute(key)
      } else {
        el.setAttribute(key, nextValue)
      }
    }
  },

  // 创建文本节点
  createText(text) {
    return document.createTextNode(text)
  },

  // 设置文本内容
  setText(node, text) {
    node.nodeValue = text
  },

  // 获取父节点
  parentNode(node) {
    return node.parentNode
  },

  // 获取下一个兄弟节点
  nextSibling(node) {
    return node.nextSibling
  }
})

// 使用自定义渲染器
const app = createApp({
  template: `
    <div>
      <h1>自定义渲染器示例</h1>
      <button @click="count++">点击次数: {{ count }}</button>
    </div>
  `,
  setup() {
    const count = ref(0)
    return { count }
  }
})

app.mount('#app')

10.5.2 Canvas 渲染器

// Canvas 渲染器
import { createRenderer } from '@vue/runtime-core'

interface CanvasElement {
  type: string
  x: number
  y: number
  width: number
  height: number
  color: string
  children?: CanvasElement[]
}

const { createApp } = createRenderer({
  createElement(type, isSVG, isCustomizedBuiltIn) {
    return {
      type,
      x: 0,
      y: 0,
      width: 0,
      height: 0,
      color: '#000000',
      children: []
    }
  },

  setElementText(el, text) {
    el.text = text
  },

  insert(el, parent, anchor) {
    if (parent.children) {
      parent.children.push(el)
    }
  },

  remove(el) {
    // Canvas 中不需要特殊处理
  },

  patchProp(el, key, prevValue, nextValue) {
    if (key === 'x') el.x = nextValue
    else if (key === 'y') el.y = nextValue
    else if (key === 'width') el.width = nextValue
    else if (key === 'height') el.height = nextValue
    else if (key === 'color') el.color = nextValue
  },

  createText(text) {
    return { type: 'text', text, x: 0, y: 0, color: '#000000' }
  },

  setText(node, text) {
    node.text = text
  },

  parentNode(node) {
    return node.parent
  },

  nextSibling(node) {
    return node.nextSibling
  }
})

// Canvas 渲染函数
function renderToCanvas(canvas: HTMLCanvasElement, rootElement: CanvasElement) {
  const ctx = canvas.getContext('2d')!
  
  function renderElement(element: CanvasElement) {
    if (element.type === 'rect') {
      ctx.fillStyle = element.color
      ctx.fillRect(element.x, element.y, element.width, element.height)
    } else if (element.type === 'text') {
      ctx.fillStyle = element.color
      ctx.fillText(element.text, element.x, element.y)
    }
    
    if (element.children) {
      element.children.forEach(renderElement)
    }
  }
  
  renderElement(rootElement)
}

// 使用 Canvas 渲染器
const app = createApp({
  template: `
    <rect x="10" y="10" width="100" height="50" color="red" />
    <text x="20" y="30" color="white">Hello Canvas!</text>
  `
})

const canvas = document.getElementById('canvas') as HTMLCanvasElement
const rootElement = app.mount(canvas)
renderToCanvas(canvas, rootElement)

10.5.3 服务端渲染器

// 服务端渲染器
import { createRenderer } from '@vue/runtime-core'

const { renderToString } = createRenderer({
  createElement(type, isSVG, isCustomizedBuiltIn) {
    return {
      type,
      props: {},
      children: []
    }
  },

  setElementText(el, text) {
    el.text = text
  },

  insert(el, parent, anchor) {
    if (parent.children) {
      parent.children.push(el)
    }
  },

  remove(el) {
    // 服务端不需要处理
  },

  patchProp(el, key, prevValue, nextValue) {
    el.props[key] = nextValue
  },

  createText(text) {
    return { type: 'text', text }
  },

  setText(node, text) {
    node.text = text
  },

  parentNode(node) {
    return node.parent
  },

  nextSibling(node) {
    return node.nextSibling
  }
})

// 将虚拟 DOM 转换为 HTML 字符串
function vnodeToHTML(vnode: any): string {
  if (vnode.type === 'text') {
    return vnode.text
  }
  
  const tag = vnode.type
  const props = vnode.props || {}
  const children = vnode.children || []
  
  const propsStr = Object.entries(props)
    .map(([key, value]) => `${key}="${value}"`)
    .join(' ')
  
  const childrenStr = children.map(vnodeToHTML).join('')
  
  return `<${tag}${propsStr ? ' ' + propsStr : ''}>${childrenStr}</${tag}>`
}

// 使用服务端渲染器
const app = createApp({
  template: `
    <div>
      <h1>服务端渲染</h1>
      <p>这是服务端渲染的内容</p>
    </div>
  `
})

const html = renderToString(app)
console.log(vnodeToHTML(html))

🎯 本章总结

通过本章学习,你掌握了:

  1. 调度器机制:批量更新、nextTick 实现
  2. Diff 算法:同层比较、Key 优化、最长递增子序列
  3. Transition 系统:类名机制、钩子函数、列表过渡
  4. 异步组件:defineAsyncComponent、Suspense 使用
  5. 自定义渲染器:Canvas 渲染、服务端渲染

📝 练习题

  1. 实现一个简化版的调度器,支持任务队列和批量更新
  2. 编写一个 Diff 算法的可视化演示工具
  3. 创建一个复杂的 Transition 动画组件
  4. 实现一个基于 Canvas 的图表组件
  5. 开发一个支持服务端渲染的简单应用

🔗 相关资源

  • Vue 3 源码
  • Vue 3 设计文档
  • Vue 3 官方文档
  • MDN Canvas API
Prev
第9章:项目实战B - 内容社区
Next
第11章:微前端与部署