Vue 3高级特性

Vue 3引入了许多高级特性,这些特性不仅提高了开发效率,还增强了框架的灵活性和扩展性。本章将详细介绍Vue 3的一些高级特性,包括自定义渲染器、Teleport传送门、Suspense异步组件和响应式系统源码分析。

15.38.1 自定义渲染器

什么是自定义渲染器

自定义渲染器是Vue 3的一个核心特性,它允许开发者将Vue的声明式API与任意平台的渲染逻辑结合起来。通过自定义渲染器,你可以将Vue组件渲染到WebGL、Canvas、原生移动应用甚至终端等非DOM环境中。

自定义渲染器的基本结构

// src/renderers/custom-renderer.js
import { createRenderer } from 'vue'

// 创建自定义渲染器
const renderer = createRenderer({
  // DOM创建相关
  createElement(tag) {
    // 创建元素
    console.log(`创建元素: ${tag}`)
    return { tag }
  },

  // 设置元素属性
  patchProp(el, key, prevValue, nextValue) {
    // 设置属性
    console.log(`设置属性: ${key} = ${nextValue}`)
    el[key] = nextValue
  },

  // 插入元素
  insert(el, parent, anchor = null) {
    // 插入元素
    console.log(`插入元素: ${el.tag} 到 ${parent.tag}`)
    if (!parent.children) {
      parent.children = []
    }
    parent.children.push(el)
  },

  // 移除元素
  remove(el) {
    // 移除元素
    console.log(`移除元素: ${el.tag}`)
  },

  // 创建文本节点
  createText(text) {
    // 创建文本节点
    console.log(`创建文本节点: ${text}`)
    return { type: 'text', text }
  },

  // 设置文本内容
  setText(node, text) {
    // 设置文本内容
    console.log(`设置文本内容: ${text}`)
    node.text = text
  },

  // 创建注释节点
  createComment(text) {
    // 创建注释节点
    console.log(`创建注释节点: ${text}`)
    return { type: 'comment', text }
  },

  // 设置注释内容
  setComment(node, text) {
    // 设置注释内容
    console.log(`设置注释内容: ${text}`)
    node.text = text
  },

  // 父元素的第一个子节点
  nextSibling(node) {
    // 获取下一个兄弟节点
    return null
  },

  // 父元素的最后一个子节点
  parentNode(node) {
    // 获取父节点
    return null
  },

  // 克隆节点
  cloneNode(node) {
    // 克隆节点
    return { ...node }
  },

  // 插入文档片段
  insertStaticContent(content, parent, anchor, namespace, shouldCache) {
    // 插入静态内容
    return { remove: () => {} }
  }
})

// 创建应用实例
const app = renderer.createApp({
  template: `<div><h1>Hello Custom Renderer</h1><p>{{ message }}</p></div>`,
  data() {
    return {
      message: '这是一个自定义渲染器示例'
    }
  }
})

// 创建一个根容器
const rootContainer = { tag: 'root' }

// 挂载应用
app.mount(rootContainer)

console.log('渲染结果:', rootContainer)

自定义Canvas渲染器示例

// src/renderers/canvas-renderer.js
import { createRenderer } from 'vue'

// 创建Canvas渲染器
const createCanvasRenderer = (canvas) => {
  const ctx = canvas.getContext('2d')
  if (!ctx) {
    throw new Error('无法获取Canvas上下文')
  }

  // 元素缓存
  const elements = new Map()

  // 绘制元素
  const drawElement = (el) => {
    if (el.tag === 'rect') {
      ctx.fillStyle = el.fill || 'black'
      ctx.fillRect(el.x || 0, el.y || 0, el.width || 100, el.height || 100)
    } else if (el.tag === 'circle') {
      ctx.fillStyle = el.fill || 'black'
      ctx.beginPath()
      ctx.arc(el.x || 50, el.y || 50, el.radius || 50, 0, Math.PI * 2)
      ctx.fill()
    } else if (el.tag === 'text') {
      ctx.fillStyle = el.color || 'black'
      ctx.font = `${el.fontSize || 16}px ${el.fontFamily || 'Arial'}`
      ctx.fillText(el.text || '', el.x || 0, el.y || 20)
    }
  }

  // 清除画布
  const clearCanvas = () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
  }

  // 重绘所有元素
  const redraw = () => {
    clearCanvas()
    elements.forEach(el => {
      drawElement(el)
    })
  }

  return createRenderer({
    createElement(tag) {
      const el = { tag }
      elements.set(el, el)
      return el
    },

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

    insert(el, parent, anchor) {
      redraw()
    },

    remove(el) {
      elements.delete(el)
      redraw()
    },

    createText(text) {
      const el = { tag: 'text', text }
      elements.set(el, el)
      return el
    },

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

    createComment(text) {
      const el = { tag: 'comment', text }
      return el
    },

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

    nextSibling(node) {
      return null
    },

    parentNode(node) {
      return null
    },

    cloneNode(node) {
      return { ...node }
    },

    insertStaticContent(content, parent, anchor, namespace, shouldCache) {
      return { remove: () => {} }
    }
  })
}

// 使用示例
const canvas = document.getElementById('canvas')
const renderer = createCanvasRenderer(canvas)

const app = renderer.createApp({
  template: `
    <rect x="10" y="10" width="100" height="100" fill="red" />
    <circle x="150" y="60" radius="50" fill="blue" />
    <text x="10" y="150" text="Hello Canvas Renderer" color="green" fontSize="20" />
  `
})

app.mount(canvas)

自定义渲染器的应用场景

  1. 跨平台开发:将Vue组件渲染到不同平台,如WebGL、Canvas、原生移动应用等
  2. 游戏开发:使用Vue的声明式API开发游戏界面
  3. 终端应用:将Vue组件渲染到终端界面
  4. 服务器端渲染:实现自定义的服务器端渲染逻辑
  5. 特殊DOM环境:在受限的DOM环境中使用Vue

15.38.2 Teleport传送门

什么是Teleport

Teleport是Vue 3的一个新特性,它允许你将组件的DOM结构"传送"到DOM树中的任何位置,而不受组件层级结构的限制。这对于创建模态框、弹出菜单、通知等组件非常有用。

Teleport的基本用法

<!-- src/components/Modal.vue -->
<template>
  <Teleport to="body">
    <div class="modal-overlay" v-if="show">
      <div class="modal-content">
        <div class="modal-header">
          <h3>{{ title }}</h3>
          <button @click="close" class="close-btn">×</button>
        </div>
        <div class="modal-body">
          <slot></slot>
        </div>
        <div class="modal-footer">
          <slot name="footer">
            <button @click="close" class="btn btn-secondary">关闭</button>
          </slot>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

const props = defineProps<{
  show: boolean
  title?: string
}>()

const emit = defineEmits<{
  (e: 'close'): void
}>()

const close = () => {
  emit('close')
}
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  width: 90%;
  max-width: 500px;
  max-height: 80vh;
  overflow-y: auto;
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  border-bottom: 1px solid #e9ecef;
}

.modal-header h3 {
  margin: 0;
  font-size: 18px;
}

.close-btn {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #6c757d;
}

.modal-body {
  padding: 16px;
}

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 16px;
  border-top: 1px solid #e9ecef;
}

.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
}
</style>

使用Teleport组件

<!-- src/views/Home.vue -->
<template>
  <div class="home">
    <h1>Vue 3 Teleport示例</h1>
    <button @click="showModal = true" class="btn btn-primary">打开模态框</button>
    
    <Modal :show="showModal" title="示例模态框" @close="showModal = false">
      <p>这是一个使用Teleport实现的模态框示例。</p>
      <p>模态框的DOM结构会被传送到底部,而不受父组件样式的影响。</p>
      <template #footer>
        <button @click="showModal = false" class="btn btn-primary">确定</button>
        <button @click="showModal = false" class="btn btn-secondary">取消</button>
      </template>
    </Modal>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Modal from '@/components/Modal.vue'

const showModal = ref(false)
</script>

Teleport的高级用法

1. 动态目标

<template>
  <Teleport :to="targetElement">
    <div class="dynamic-teleport">
      动态目标的Teleport内容
    </div>
  </Teleport>
  
  <select v-model="targetId">
    <option value="#target1">目标1</option>
    <option value="#target2">目标2</option>
  </select>
  
  <div id="target1" class="target">目标1</div>
  <div id="target2" class="target">目标2</div>
</template>

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

const targetId = ref('#target1')
const targetElement = computed(() => document.querySelector(targetId.value) || 'body')
</script>

2. 多个Teleport到同一目标

<template>
  <Teleport to="#shared-target">
    <div class="teleport-item">Teleport内容1</div>
  </Teleport>
  
  <Teleport to="#shared-target">
    <div class="teleport-item">Teleport内容2</div>
  </Teleport>
  
  <div id="shared-target" class="shared-target"></div>
</template>

Teleport的注意事项

  1. CSS作用域:Teleport内容的样式仍然受父组件的作用域样式影响
  2. 事件冒泡:Teleport内容的事件会冒泡到父组件
  3. SSR支持:Teleport在服务器端渲染中也能正常工作
  4. 目标元素:确保目标元素在Teleport挂载之前已经存在

15.38.2 Teleport传送门(续)

Teleport与CSS过渡动画

Teleport可以与Vue的过渡系统结合使用,实现平滑的动画效果:

<template>
  <Teleport to="body">
    <Transition name="modal">
      <div class="modal-overlay" v-if="show">
        <div class="modal-content">
          <!-- 模态框内容 -->
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<style scoped>
.modal-enter-active,
.modal-leave-active {
  transition: opacity 0.3s, transform 0.3s;
}

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

.modal-enter-from .modal-content,
.modal-leave-to .modal-content {
  transform: scale(0.9) translateY(-20px);
}
</style>

15.38.3 Suspense异步组件

什么是Suspense

Suspense是Vue 3的一个新组件,它允许你在等待异步组件加载完成时显示一个 fallback 内容。Suspense可以处理:

  • 异步组件(通过defineAsyncComponent定义的组件)
  • 带有异步setup函数的组件
  • 组件树中任何层级的异步依赖

Suspense的基本用法

<!-- src/App.vue -->
<template>
  <div class="app">
    <h1>Vue 3 Suspense示例</h1>
    
    <Suspense>
      <template #default>
        <AsyncComponent />
      </template>
      <template #fallback>
        <div class="loading">
          <el-icon class="is-loading"><loading /></el-icon>
          <span>加载中...</span>
        </div>
      </template>
    </Suspense>
  </div>
</template>

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import { Loading } from '@element-plus/icons-vue'

// 定义异步组件
const AsyncComponent = defineAsyncComponent({
  // 加载组件
  loader: () => import('./components/AsyncComponent.vue'),
  // 加载超时时间
  timeout: 3000,
  // 加载失败时显示的组件
  errorComponent: () => import('./components/ErrorComponent.vue')
})
</script>

异步组件

<!-- src/components/AsyncComponent.vue -->
<template>
  <div class="async-component">
    <h2>异步组件</h2>
    <p>{{ data.message }}</p>
  </div>
</template>

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

// 模拟异步数据获取
const fetchData = () => {
  return new Promise<{ message: string }>((resolve) => {
    setTimeout(() => {
      resolve({ message: '异步数据加载成功!' })
    }, 2000)
  })
}

// 异步setup函数
const data = await fetchData()
</script>

Suspense的高级用法

1. 嵌套Suspense

<template>
  <Suspense>
    <template #default>
      <div class="outer-suspense">
        <h2>外层Suspense</h2>
        <Suspense>
          <template #default>
            <DeepAsyncComponent />
          </template>
          <template #fallback>
            <div class="inner-loading">内层加载中...</div>
          </template>
        </Suspense>
      </div>
    </template>
    <template #fallback>
      <div class="outer-loading">外层加载中...</div>
    </template>
  </Suspense>
</template>

2. 错误处理

<template>
  <Suspense>
    <template #default>
      <ErrorBoundary>
        <AsyncComponent />
      </ErrorBoundary>
    </template>
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
  </Suspense>
</template>

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import ErrorBoundary from '@/components/ErrorBoundary.vue'

const AsyncComponent = defineAsyncComponent({
  loader: () => import('./components/FailingComponent.vue')
})
</script>
<!-- src/components/ErrorBoundary.vue -->
<template>
  <div v-if="error" class="error-boundary">
    <h3>发生错误</h3>
    <p>{{ error.message }}</p>
    <button @click="resetError">重试</button>
  </div>
  <slot v-else></slot>
</template>

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

const error = ref<Error | null>(null)

const resetError = () => {
  error.value = null
  // 重新渲染组件
  window.location.reload()
}

onErrorCaptured((err) => {
  error.value = err as Error
  return false
})
</script>

Suspense的注意事项

  1. 仅处理异步依赖:Suspense只能处理组件树中的异步依赖,不能处理同步错误
  2. 需要异步组件或异步setup:Suspense需要配合异步组件或带有异步setup函数的组件使用
  3. 错误处理:Suspense本身不处理错误,需要配合ErrorBoundary组件使用
  4. SSR支持:Suspense在服务器端渲染中也能正常工作

15.38.4 响应式系统源码分析

Vue 3响应式系统概述

Vue 3的响应式系统是基于ES6的Proxy API实现的,它提供了更全面的响应式支持,包括:

  • 对象和数组的响应式
  • 嵌套对象的响应式
  • Map、Set、WeakMap、WeakSet的响应式
  • 计算属性和侦听器

响应式系统核心API

  1. **reactive**:创建响应式对象
  2. **ref**:创建响应式基本类型
  3. **computed**:创建计算属性
  4. **watch**:创建侦听器
  5. **watchEffect**:创建响应式副作用
  6. **toRefs**:将响应式对象转换为ref对象集合
  7. **shallowReactive**:创建浅层响应式对象
  8. **shallowRef**:创建浅层响应式基本类型
  9. **readonly**:创建只读响应式对象
  10. **shallowReadonly**:创建浅层只读响应式对象

响应式系统源码结构

1. 依赖收集与触发

// src/reactivity/src/effect.ts
// 依赖收集
class ReactiveEffect {
  private _fn: any
  deps: Dep[] = []
  active = true
  onStop?: () => void
  
  constructor(fn, public scheduler?: EffectScheduler) {
    this._fn = fn
  }
  
  run() {
    // 运行副作用函数
    if (!this.active) {
      return this._fn()
    }
    
    // 依赖收集
    activeEffect = this
    cleanupEffect(this)
    const result = this._fn()
    activeEffect = undefined
    
    return result
  }
  
  stop() {
    // 停止副作用
    if (this.active) {
      this.active = false
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
    }
  }
}

// 依赖触发
function triggerEffects(dep: Dep) {
  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

2. Reactive实现

// src/reactivity/src/reactive.ts
// 创建响应式对象
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) {
  // 检查目标是否为对象
  if (!isObject(target)) {
    return target
  }
  
  // 如果目标已经是响应式对象,直接返回
  if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
    return target
  }
  
  // 创建Proxy
  const proxy = new Proxy(
    target,
    // 根据目标类型选择不同的处理器
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  
  return proxy
}

// 基础处理器
const mutableHandlers: ProxyHandler<object> = {
  get(target, key, receiver) {
    // 处理特殊键
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true
    }
    
    // 依赖收集
    track(target, TrackOpTypes.GET, key)
    
    // 获取值
    const res = Reflect.get(target, key, receiver)
    
    // 递归处理嵌套对象
    if (isObject(res)) {
      return reactive(res)
    }
    
    return res
  },
  
  set(target, key, value, receiver) {
    // 获取旧值
    const oldValue = (target as any)[key]
    
    // 设置值
    const result = Reflect.set(target, key, value, receiver)
    
    // 依赖触发
    if (hasChanged(value, oldValue)) {
      trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
    
    return result
  },
  
  deleteProperty(target, key) {
    // 检查属性是否存在
    const hadKey = hasOwn(target, key)
    
    // 删除属性
    const result = Reflect.deleteProperty(target, key)
    
    // 依赖触发
    if (hadKey && result) {
      trigger(target, TriggerOpTypes.DELETE, key)
    }
    
    return result
  },
  
  has(target, key) {
    // 依赖收集
    track(target, TrackOpTypes.HAS, key)
    
    return Reflect.has(target, key)
  },
  
  ownKeys(target) {
    // 依赖收集
    track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
    
    return Reflect.ownKeys(target)
  }
}

3. Ref实现

// src/reactivity/src/ref.ts
// 创建ref
class RefImpl<T> {
  private _value: T
  private _rawValue: T
  public dep?: Dep = undefined
  public readonly __v_isRef = true
  
  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }
  
  get value() {
    // 依赖收集
    trackRefValue(this)
    return this._value
  }
  
  set value(newVal) {
    // 获取原始值
    const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    
    // 检查值是否变化
    if (hasChanged(newVal, this._rawValue)) {
      // 更新值
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      // 依赖触发
      triggerRefValue(this)
    }
  }
}

响应式系统工作流程

  1. 创建响应式对象:使用reactiveref创建响应式对象
  2. 依赖收集:当访问响应式对象的属性时,收集依赖(副作用函数)
  3. 响应式更新:当修改响应式对象的属性时,触发依赖(重新运行副作用函数)

响应式系统优化

  1. 懒依赖收集:只有在访问响应式属性时才收集依赖
  2. 精准依赖触发:只有修改了被依赖的属性时才触发依赖
  3. 避免循环依赖:通过toRaw函数避免循环依赖
  4. 浅层响应式:提供shallowReactiveshallowRef创建浅层响应式对象
  5. 只读响应式:提供readonlyshallowReadonly创建只读响应式对象

Vue 3高级特性总结

  1. 自定义渲染器

    • 允许将Vue组件渲染到任意平台
    • 提高了框架的灵活性和扩展性
    • 适用于跨平台开发
  2. Teleport传送门

    • 允许将组件内容渲染到DOM树的任意位置
    • 解决了模态框、弹出菜单等组件的样式隔离问题
    • 支持动态目标和多个Teleport到同一目标
  3. Suspense异步组件

    • 简化了异步组件的处理
    • 提供了统一的加载状态管理
    • 支持嵌套和错误处理
  4. 响应式系统

    • 基于ES6 Proxy API实现
    • 支持更全面的响应式数据类型
    • 提供了多种响应式API
    • 依赖收集和触发机制更加高效

通过学习这些高级特性,你可以更好地理解Vue 3的设计理念和内部工作原理,从而在实际开发中更加灵活地使用Vue 3,构建出更高质量的应用程序。

« 上一篇 36-real-time-chat-app 下一篇 » 38-ecosystem-expansion