阅读视图

发现新文章,点击刷新页面。

Vue 底层原理 & 新特性

Vue 底层原理 & 新特性

本文深入探讨 Vue 的底层架构演进、核心原理以及最新版本带来的突破性特性,面向面试和技术提升。


原文地址

墨渊书肆/Vue 底层原理 & 新特性


Vue 版本变动历史

Vue 自发布以来经历了多个重要版本的迭代,每个版本的改动都带来了架构优化和新特性,同时也伴随着一些 Breaking Changes。以下是 Vue 各个重要版本的变动概述:

Vue 2.0 (2016年)

  • 引入 Virtual DOMVue2 正式引入了虚拟 DOM,这是框架性能提升的关键技术。
  • 组件系统增强:增加了异步组件生命周期钩子调整等特性。
  • 支持 SSR:原生支持服务器端渲染,提升了 SEO 和首屏加载性能。
  • Vuex 与 Vue Router:作为官方解决方案提供状态管理路由管理

Vue 2.5 - 2.7 (2017-2022年)

  • Vue 2.5:改进了 TypeScript 支持,增强了响应式系统
  • Vue 2.6:引入了新的模板编译策略,插槽语法改进。
  • Vue 2.7:作为 Vue2 最后的大版本,引入了一些 Composition API 的向下兼容实现,为 Vue3 迁移做铺垫。

Vue 3.0 (2022年)

  • Composition API:引入了全新的组合式 API,提供了更灵活的逻辑组织方式。
  • Proxy 响应式系统:使用 Proxy 替代 Object.defineProperty,解决了 Vue2 响应式的诸多痛点。
  • Teleport & Fragments:新增内置组件,支持跨 DOM 层级渲染和多根节点模板。
  • 性能提升:更快的解析速度和更小的运行时体积,渲染性能提升约 100%。
  • 更好的 TypeScript 支持:原生支持 TypeScript,类型推导更加完善。
  • 自定义渲染器 API:增强的渲染器 API,便于跨平台开发。

Vue 3.1 - 3.4 (2023-2024年)

  • Vue 3.1:引入了 defineOptions 宏,改进编译优化。
  • Vue 3.3:进一步改进宏支持,类型化 props/emits 更加方便,简化了泛型组件的使用。
  • Vue 3.4:性能进一步提升,响应式系统优化,编译器效率改进。

Vue 3.5 及未来 (2024-2025年)

  • Vue 3.5:引入了响应式解构语法(Reactivity Transform),改善了大型应用的开发体验。
  • Vapor ModeVue 团队正在实验的全新渲染策略,跳过虚拟 DOM直接生成高效的 JavaScript 代码。
  • 更完善的生态集成:与 Vite 5PiniaVue Router 4 的深度整合。

响应式原理深度解析

响应式系统是 Vue 的核心,也是面试中的高频考点。Vue2 和 Vue3 在响应式实现上有着本质的区别。

Vue2:Object.defineProperty

Vue2 使用 Object.defineProperty 来劫持数据的 getter 和 setter:

function defineReactive(obj, key, val) {
  // 为每个属性创建 Dep 实例
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 依赖收集
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return
      // 通知更新
      dep.notify()
    }
  })
}

Vue2 响应式的局限性

  1. 无法检测对象属性的添加/删除Object.defineProperty 只能劫持已存在的属性,对于新增属性无能为力。
  2. 数组操作无法响应:通过下标修改数组元素 arr[0] = value 不会触发更新。
  3. 深层监听需要递归:对深层对象的监听会带来性能开销。

解决方案:Vue2 提供了 Vue.set / Vue.delete 以及重写数组方法来应对这些场景。

Vue3:Proxy

Vue3 使用 ES6 的 Proxy 来实现响应式:

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key)
      const result = Reflect.get(target, key, receiver)
      // 如果是对象,递归代理实现深层响应式
      return isObject(result) ? reactive(result) : result
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      // 触发更新
      trigger(target, key)
      return result
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      trigger(target, key)
      return result
    }
  })
}

Vue3 响应式的优势

  1. 原生支持属性增删:Proxy 可以拦截对象的所有操作,包括新增和删除属性。
  2. 数组操作完全响应:下标赋值、数组长度变化等都能被正确拦截。
  3. 更好的性能:Proxy 是懒执行的,只有当访问属性时才会进行依赖收集。
  4. API 统一:ref 和 reactive 内部实现统一,简化了学习成本。

依赖收集与触发机制

Vue 的响应式系统遵循观察者模式,包含三个核心角色:

  1. Observer(观察者):负责劫持数据,收集依赖。
  2. Dep(依赖管理器):存储依赖,管理订阅者。
  3. Watcher(订阅者):在数据变化时执行更新回调。
// Dep 实现
class Dep {
  constructor() {
    this.subs = new Set() // 存储 Watcher
  }
  
  depend() {
    if (Dep.target) {
      this.subs.add(Dep.target)
    }
  }
  
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// Watcher 实现
class Watcher {
  constructor(fn) {
    this.getter = fn
    this.value = this.get()
  }
  
  get() {
    Dep.target = this
    const value = this.getter()
    Dep.target = null
    return value
  }
  
  update() {
    this.value = this.getter()
  }
}

模板编译原理

Vue 的模板编译是将模板字符串转换为可执行渲染函数的过程,主要分为三个阶段。

1. 解析阶段(Parse)

将模板字符串解析为 AST(抽象语法树):

// 模板
<div class="container">
  <h1>{{ title }}</h1>
</div>

// AST 结构
{
  type: 'Element',
  tag: 'div',
  props: [{ type: 'Attribute', name: 'class', value: 'container' }],
  children: [
    {
      type: 'Element',
      tag: 'h1',
      children: [{ type: 'Interpolation', content: { expression: 'title' } }]
    }
  ]
}

2. 优化阶段(Optimize)

Vue3 的编译器会进行静态节点提升(Static Hoisting):

  • 静态节点:不包含任何响应式依赖的节点(如纯文本、静态属性)。
  • 事件缓存:对于不响应式变化的事件处理函数,进行缓存处理。
// 优化前
render() {
  return h('button', { onClick: this.handleClick }, 'Click')
}

// 优化后 - 事件函数被缓存
const handleClick = this.handleClick
render() {
  return h('button', { onClick: handleClick }, 'Click')
}

3. 代码生成阶段(Generate)

将 AST 转换为渲染函数:

// 生成的渲染函数
function render() {
  return _vue.createVNode('div', { class: 'container' }, [
    _vue.createVNode('h1', null, _vue.toDisplayString(this.title))
  ])
}

虚拟 DOM 与 Diff 算法

虚拟 DOM 的本质

虚拟 DOM 是真实 DOM 的 JavaScript 对象表示:

// VNode 结构
const vnode = {
  type: 'div',
  props: { class: 'container' },
  children: [
    { type: 'h1', children: 'Hello' }
  ],
  el: null // 关联的真实 DOM 引用
}

虚拟 DOM 的优势

  1. 跨平台渲染:同一套 VNode 结构可以渲染到不同平台。
  2. 减少 DOM 操作:在内存中进行对比,只更新必要的真实 DOM。
  3. 声明式开发:开发者只需关注数据变化,框架自动处理 DOM 更新。

Vue2 Diff:单端比较

Vue2 采用传统的 Diff 算法,从左到右依次对比:

function updateChildren(oldChildren, newChildren) {
  let oldStartIndex = 0
  let newStartIndex = 0
  let oldEndIndex = oldChildren.length - 1
  let newEndIndex = newChildren.length - 1
  
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    // 简单比较...O(n) 复杂度
  }
}

Vue3 Diff:双端比较 + 最长递增子序列

Vue3 采用了更高效的 Diff 算法

  1. 双端比较:同时从新旧列表的首尾进行对比。
  2. key 映射:通过 Map 快速定位相同 key 的节点。
  3. 最长递增子序列:对于需要移动的节点,使用 LIS 算法最小化移动次数。
// Vue3 Diff 核心逻辑
function diffChildren(n1, n2, parent) {
  const c1 = n1.children
  const c2 = n2.children
  const oldStart = 0
  const newStart = 0
  const oldEnd = c1.length - 1
  const newEnd = c2.length - 1
  
  // 双端比较策略
  while (oldStart <= oldEnd && newStart <= newEnd) {
    if (c1[oldStart].key === c2[newStart].key) {
      // 节点相同,继续
      patch(c1[oldStart], c2[newStart], parent)
      oldStart++
      newStart++
    } else if (c1[oldEnd].key === c2[newEnd].key) {
      // 尾部匹配
      patch(c1[oldEnd], c2[newEnd], parent)
      oldEnd--
      newEnd--
    }
    // ... 更多比较策略
  }
}

组件生命周期与更新机制

Vue2 生命周期

阶段 钩子 说明
初始化 beforeCreate 实例刚创建,数据观测未完成
初始化 created 数据观测完成,DOM 未生成
挂载 beforeMount 模板编译完成,准备挂载
挂载 mounted DOM 挂载完成,可操作 DOM
更新 beforeUpdate 数据变化,DOM 未更新
更新 updated DOM 更新完成
销毁 beforeDestroy 实例销毁前,可清理
销毁 destroyed 实例已销毁

Vue3 生命周期

Vue2 Vue3 (Composition API)
beforeCreate -
created -
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted

组件更新流程

数据变化 → 触发 setter → Dep 通知 Watcher → 
触发 update() → 重新执行 render() 生成新的 VNode → 
Diff 对比 → 更新真实 DOM

Vue3 新特性深度解析

1. Composition API

组合式 API 是 Vue3 最重要的变化,提供了更灵活的逻辑组织方式:

// setup 函数 - 组件逻辑入口
import { ref, computed, onMounted, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const doubled = computed(() => count.value * 2)
    
    function increment() {
      count.value++
    }
    
    onMounted(() => {
      console.log('Component mounted!')
    })
    
    watch(count, (newVal) => {
      console.log(`Count changed to ${newVal}`)
    })
    
    return { count, doubled, increment }
  }
}

ref vs reactive

  • ref:用于原始类型,创建包含 .value 的响应式对象。
  • reactive:用于对象,创建深层响应式对象。
import { ref, reactive } from 'vue'

const count = ref(0)        // 原始类型
const state = reactive({   // 对象类型
  user: { name: 'Vue' }
})

// 模板中自动解包
console.log(count.value)   // JS 中需要 .value
console.log(state.user)    // reactive 直接访问

2. Teleport

将组件渲染到指定 DOM 位置,常用于模态框:

<Teleport to="body">
  <div v-if="show" class="modal">
    <p>Modal Content</p>
  </div>
</Teleport>

3. Fragments

支持多根节点模板:

<!-- Vue3 允许 -->
<template>
  <div>A</div>
  <div>B</div>
</template>

4. Suspense

处理异步组件加载状态:

<Suspense>
  <template #default>
    <AsyncComponent />
  </template>
  <template #fallback>
    Loading...
  </template>
</Suspense>

Vue vs React:核心差异对比

响应式实现

特性 Vue2 Vue3 React
原理 Object.defineProperty Proxy useState/useReducer
触发方式 自动 自动 手动调用 setState
数组响应 重写方法 Proxy 需使用 Immer 或 immutable
深层监听 递归 Proxy 懒加载 useEffect 依赖

模板 vs JSX

  • Vue:模板语法,HTML-like,学习成本低,编译器优化。
  • React:JSX,JavaScript 表达式,更灵活,但需要一定学习曲线。

状态管理

  • Vue:Pinia(推荐)或 Vuex,采用模块化设计。
  • React:Redux/Zustand/Jotai,函数式风格。

渲染性能

Vue3 由于模板编译优化和 Proxy 响应式,在大多数场景下性能优于 React。React 的优势在于 Fiber 架构带来的精细化控制和并发渲染能力。


性能优化策略

1. 渲染优化

// 使用 v-once 静态内容
<div v-once>{{ staticContent }}</div>

// 正确使用 key
<li v-for="item in items" :key="item.id">{{ item.name }}</li>

// v-if vs v-show 选择
<div v-if="show">很少切换</div>
<div v-show="show">频繁切换</div>

2. 响应式优化

import { shallowRef, markRaw } from 'vue'

// 浅层响应式 - 适合大型数据
const largeList = shallowRef([])

// 非响应式数据 - 适合不需要响应式的对象
const plainObj = markRaw({ /* ... */ })

3. 组件懒加载

// 路由懒加载
const Home = () => import('./views/Home.vue')

// 异步组件
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))

4. KeepAlive 缓存

<KeepAlive include="Home,About">
  <router-view />
</KeepAlive>

面试常见问题汇总

1. Vue2 和 Vue3 响应式的区别?

Vue2 使用 Object.defineProperty,需要递归监听所有属性,无法检测新增/删除属性;Vue3 使用 Proxy,原生支持属性增删,性能更好。

2. Vue 的依赖收集是如何实现的?

通过 Dep 类管理订阅者,Watcher 在读取响应式属性时将自身添加到 Dep,属性变化时 Dep 通知所有 Watcher 更新。

3. Vue3 Diff 算法相比 Vue2 有什么优化?

Vue3 采用 双端比较 策略,结合 最长递增子序列 算法,最小化 DOM 移动次数,复杂度从 O(n³) 优化到 O(n)。

4. Vue3 的 Composition API 有什么优势?

  • 更好的 TypeScript 支持
  • 代码更容易复用和抽取
  • 逻辑相关代码组织在一起,而不是按选项分散

5. Vue3 的性能为什么比 Vue2 好?

  • Proxy 替代 Object.defineProperty,深层监听懒执行
  • 模板编译优化:静态节点提升事件缓存
  • 优化的 Diff 算法
  • 更小的打包体积

6. Vue 的 nextTick 原理?

Vue 使用 Promise + MutationObserver + setTimeout 实现异步队列,在 DOM 更新后通过微任务执行回调。

7. keep-alive 的实现原理?

通过缓存 VNode,保存组件实例和状态,切换时复用而非重新创建。activated/deactivated 钩子用于感知缓存状态变化。

8. Vue 的模板编译过程?

解析优化(静态节点提升)→ 代码生成(渲染函数)


总结

Vue 作为一个渐进式框架,在保持易用性的同时不断深化底层技术的实现。Vue3 通过 Composition API、Proxy 响应式系统、优化的 Diff 算法等特性,显著提升了开发体验和运行性能。理解这些底层原理不仅有助于应对面试,更能在实际开发中做出更好的技术决策。

Vue 团队正在探索的 Vapor Mode 未来可能带来更大的性能突破,值得持续关注。

Vue 基础理论 & API 使用

Vue 基础理论 & API 使用

本文主要记录 Vue 的基础理论、核心概念与常用 API 使用方法,面向面试和日常开发参考。


原文地址

墨渊书肆/Vue 基础理论 & API 使用


Vue 简介

Vue 是一个渐进式 JavaScript 框架,由尤雨溪于 2014 年创建。Vue 核心库聚焦于视图层,易于学习和集成,同时能够驱动复杂的单页应用程序(SPA)开发。

核心特点

  • 响应式数据绑定 (MVVM 模式)
  • 组件化开发
  • 虚拟 DOM
  • 指令系统
  • 渐进式架构

安装与项目创建

Vite(推荐)

# 创建 Vue3 项目
npm create vue@latest

# 或使用 Vite 直接创建
npm create vite@latest my-vue-app -- --template vue

Vue CLI

npm install -g @vue/cli
vue create my-project

基础指令

v-model 双向绑定

v-modelVue 中用于表单输入和数据双向绑定的核心指令,本质是 v-bind + v-on语法糖

基本用法

<input v-model="message">
<p>{{ message }}</p>

修饰符

修饰符 说明
.lazy 在 change 事件时更新,而非 input
.number 自动转换为数值
.trim 去除首尾空白

自定义 v-model(Vue 3.4+):

// 子组件
defineProps(['modelValue'])
defineEmits(['update:modelValue'])

// 父组件
<MyInput v-model:title="title" />

v-if / v-show 条件渲染

特性 v-if v-show
DOM 操作 创建/销毁 display: none
初始渲染 惰性 立即渲染
切换性能
适用场景 很少切换 频繁切换
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>C</div>

v-for 列表渲染

<li v-for="(item, index) in items" :key="item.id">
  {{ index }} - {{ item.name }}
</li>

注意事项

  • 必须使用 :key 绑定唯一标识
  • 不建议使用数组索引作为 key
  • Vue2 中 v-for 优先级高于 v-if,Vue3 中相反

v-bind / v-on 属性与事件

<!-- 绑定属性 -->
<img :src="url">

<!-- 绑定多个属性 -->
<img v-bind="attrs">

<!-- 事件监听 -->
<button @click="handleClick">Click</button>

<!-- 事件修饰符 -->
<button @click.stop="handle">阻止冒泡</button>
<button @click.prevent="handle">阻止默认行为</button>

组件选项

data

组件的响应式数据源,必须返回纯对象:

export default {
  data() {
    return {
      count: 0,
      user: { name: 'Vue' }
    }
  }
}

props

父子组件通信的重要方式,支持类型校验默认值

export default {
  props: {
    // 基础类型
    title: String,
    // 多个类型
    age: [Number, String],
    // 带默认值
    size: {
      type: String,
      default: 'medium'
    },
    // 必需
    id: {
      type: Number,
      required: true
    },
    // 自定义校验
    score: {
      validator: (value) => value >= 0 && value <= 100
    }
  }
}

Vue3 组合式 API

const props = defineProps({
  title: String,
  count: { type: Number, default: 0 }
})

computed 计算属性

缓存计算结果,只在依赖变化时重新计算:

export default {
  data() { return { count: 1 } },
  computed: {
    // 只读
    doubled() { return this.count * 2 },
    // 可写
    plusOne: {
      get() { return this.count + 1 },
      set(val) { this.count = val - 1 }
    }
  }
}

methods 方法

处理业务逻辑,每次渲染都会重新创建:

export default {
  methods: {
    handleClick() { /* ... */ }
  }
}

watch 监听器

监听数据变化并执行回调:

export default {
  data() { return { count: 0 } },
  watch: {
    count(newVal, oldVal) {
      console.log(`变化: ${oldVal}${newVal}`)
    },
    // 深度监听
    'obj.data': {
      handler() { /* ... */ },
      deep: true
    },
    // 立即执行
    name: {
      handler() { /* ... */ },
      immediate: true
    }
  }
}

Composition API

Vue3 引入的组合式 API,提供了更灵活的逻辑组织方式。

ref / reactive 响应式

import { ref, reactive } from 'vue'

// ref - 原始类型
const count = ref(0)
count.value++

// reactive - 对象
const state = reactive({
  user: { name: 'Vue' }
})
state.user.name = 'Vue3'

区别

| 特性 | ref | reactive | | ----- -| ----- | ---------- | | 适用类型 | 任意类型 | 对象/数组 | | 访问方式 | .value | 直接属性 | | 重新赋值 | 响应式 | 替换整个对象 |

toRefs / toRef

将 reactive 对象解构为独立的 ref:

import { reactive, toRefs } from 'vue'

const state = reactive({ name: 'Vue', age: 25 })
const { name, age } = toRefs(state)

// 或创建单个 ref
const nameRef = toRef(state, 'name')

computed() 计算属性

import { ref, computed } from 'vue'

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

watch / watchEffect

import { ref, watch, watchEffect } from 'vue'

// watch - 显式监听
watch(count, (newVal, oldVal) => { /* ... */ })
watch(() => state.name, (newVal) => { /* ... */ })

// watchEffect - 自动收集依赖
watchEffect(() => {
  console.log(count.value) // 自动追踪
})

执行时机控制

  • watch:默认同步执行
  • watchEffect:默认 pre(在组件更新前)
  • watchPostEffect:在组件更新后执行
  • watchSyncEffect:同步执行

生命周期钩子

import { 
  onMounted, 
  onUpdated, 
  onUnmounted 
} from 'vue'

export default {
  setup() {
    onMounted(() => { console.log('mounted') })
    onUpdated(() => { console.log('updated') })
    onUnmounted(() => { console.log('unmounted') })
  }
}

组件通信

Props / $emit

// 父组件
<Child :count="count" @update="handleUpdate" />

// 子组件
const props = defineProps({ count: Number })
const emit = defineEmits(['update'])
emit('update', props.count + 1)

Provide / Inject

祖先向后代跨级传值

// 祖先组件
provide('key', 'value')

// 后代组件
const value = inject('key')

响应式

// 祖先
const count = ref(0)
provide('count', count)

// 后代 - 修改会影响所有后代
const count = inject('count')

attrs/attrs / listeners

透传属性和事件:

<!-- 透传所有 -->
<Child v-bind="$attrs" v-on="$listeners" />

Pinia 状态管理

Vue3 推荐的状态管理方案:

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    doubled: (state) => state.count * 2
  },
  actions: {
    increment() { this.count++ }
  }
})

内置组件

Transition

为元素添加过渡动画

<Transition name="fade">
  <div v-if="show">Content</div>
</Transition>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

过渡类名

  • v-enter-from / v-leave-from:起始状态
  • v-enter-active / v-leave-active:过渡中
  • v-enter-to / v-leave-to:结束状态

KeepAlive

缓存组件实例:

<KeepAlive include="A,B" exclude="C">
  <component :is="current" />
</KeepAlive>

生命周期

  • activated:激活时
  • deactivated:停用时

Teleport

渲染到指定 DOM 位置:

<Teleport to="#modal-root">
  <div class="modal">Content</div>
</Teleport>

Suspense

处理异步组件(实验性):

<Suspense>
  <template #default>
    <AsyncComponent />
  </template>
  <template #fallback>
    Loading...
  </template>
</Suspense>

生命周期

Options API

阶段 钩子 说明
初始化 beforeCreate 实例创建前
初始化 created 数据观测完成
挂载 beforeMount 模板编译完成
挂载 mounted DOM 挂载完成
更新 beforeUpdate 数据变化,DOM 未更新
更新 updated DOM 更新完成
销毁 beforeUnmount 实例销毁前
销毁 unmounted 实例已销毁

父子组件执行顺序

挂载:父 created → 子 created → 子 mounted → 父 mounted

更新:父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated

销毁:父 beforeUnmount → 子 beforeUnmount → 子 unmounted → 父 unmounted


常用技巧

动态类名

<div :class="{ active: isActive, 'text-center': isCenter }">
<div :class="[activeClass, errorClass]">

条件类名

<div :class="[isActive && 'active']">

动态绑定 style

<div :style="{ color: textColor, fontSize: fontSize + 'px' }">

函数式组件

export default {
  functional: true,
  props: { msg: String },
  render(h, context) {
    return h('div', context.props.msg)
  }
}

异步组件

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Async.vue'),
  loadingComponent: Loading,
  errorComponent: Error,
  delay: 200,
  timeout: 3000
})

面试常见问题

1. v-model 的原理?

本质是 v-bind:value + @input语法糖,监听 input 事件并更新数据。

2. v-for 中 key 的作用?

帮助 Vue 识别节点身份,实现高效的 DOM 复用。推荐使用数据唯一 ID,避免使用数组索引

3. computed 和 watch 的区别?

  • computed:计算属性,依赖变化自动计算,缓存结果
  • watch:监听器,监听数据变化,执行异步或复杂逻辑

4. Vue2 和 Vue3 的区别?

  • 响应式:Object.defineProperty → Proxy
  • API:Options API → Composition API
  • 多根节点:不支持 → 支持
  • 生命周期:beforeDestroy → beforeUnmount

5. 组件通信方式有哪些?

  • props / $emit:父子
  • provide / inject:祖先-后代
  • attrs/attrs / listeners:透传
  • 事件总线:兄弟/任意
  • Pinia/Vuex:全局状态

6. Vue 的响应式原理?

通过 Proxy/Object.defineProperty 劫持数据访问,在 getter 中收集依赖,setter 中触发更新


总结

Vue 以其简洁的 API 和渐进式的设计理念,成为前端开发的主流框架。掌握 Vue 的基础理论、常用 API 以及组件通信方式,是 Vue 开发者的必备技能。Vue3Composition API 提供了更现代化的开发范式,建议在实际项目中优先使用。

React 底层原理 & 新特性

React 底层原理 & 新特性

本文深入探讨 React 的底层架构演进、核心原理以及最新版本带来的突破性特性。


原文地址

墨渊书肆/React 底层原理 & 新特性


React 版本变动历史

React 自发布以来经历了多个版本的更新,每个主要版本的变动都带来了新的特性和改进,同时也对旧有的API进行了调整或废弃。以下是React几个重要版本的主要变动概述:

React 15 (2016年)

  • 引入Fiber架构:在 React 15后期版本中引入了 Fiber, 提供了更灵活的渲染调度和更换的错误恢复机制。
  • 改进了服务器端渲染:提升了SSR(Server-Side Rendering)的性能 and 稳定性。
  • SVG和MathML的支持增强:更好地支持SVG和MathML元素,使其渲染更加一致和准确。

React 16 (2017年)

  • 全面实施Fiber:Fiber成为了React核心的更新算法,提供了更细粒度的任务调度和更强大的并发模式,使得React应用的性能和响应性有了显著提升。
  • Error Boundaries:引入了错误边界的概念,允许组件捕获其子组件树中的JavaScript错误,并优雅地降级,而不是让整个应用崩溃。
  • Portals:允许将子节点渲染到DOM树的其他位置,为模态框、弹出层等场景提供了更好的解决方案。
  • 支持返回数组的render方法:可以直接从组件的render方法返回多个元素,而不需要额外的包装元素。

React 17 (2020年)

  • 自动批处理更新:默认开启了自动批处理更新,即使开发者没有手动使用 React.startTransitionunstable_batchedUpdates,React也会尝试批处理状态更新,以减少渲染次数。
  • 事件委托改进:改变了事件处理的方式,将事件监听器绑定到 document 上,减少了委托层级,简化了第三方库的继成。
  • 更严格的 JSX 类型检查:增强了对JSX类型的检查,帮助开发者提前发现潜在的类型错误。
  • 无-breaking-change 版本:React 17被设计为一个过渡版本,尽量减少对现有代码的破坏,为未来更大的更新铺路。

React 18 (2022年)

  • 并发模式:进一步深化了Fiber架构的并发特性,通过新的 SuspenseUseTransition API,允许开发者更好地控制组件的加载和更新策略。
  • 自动 hydration:React 18引入了新的渲染模式,包括 Server ComponentsAutomatic Hydration,旨在减少初次加载时间和提高用户体验。
  • 改进的错误处理:增强了错误边界和错误报告的能力,使得调试和问题定位更加容易。
  • StartTransition API:允许开发者标记某些状态更新为低优先级,从而优化UI的响应性和流畅性。

React 19 新特性深度解析 (2024年)

React 19 是一个重大的里程碑,它将许多在 React 18 中处于 Canary/Experimental 阶段的特性正式稳定化,并引入了全新的开发范式。

1. Actions 与异步状态管理

React 19 引入了 Actions 的概念,用于简化异步操作(如表单提交)及其状态管理。

  • useActionState: 自动处理异步函数的 pending 状态和结果。

    function UpdateName({ name, updateName }) {
      const [error, submitAction, isPending] = useActionState(
        async (previousState, formData) => {
          const error = await updateName(formData.get("name"));
          if (error) return error;
          return null;
        },
        null
      );
    
      return (
        <form action={submitAction}>
          <input type="text" name="name" disabled={isPending} />
          <button type="submit" disabled={isPending}>Update</button>
          {error && <p>{error}</p>}
        </form>
      );
    }
    
  • useFormStatus: 子组件无需通过 Props 即可感知父表单的提交状态。

  • useOptimistic: 极致的乐观更新体验。在请求发出时立即更新 UI,请求失败后自动回滚。

2. Server Actions:打通前后端的“虫洞”

Server Actions 允许你在客户端直接调用服务器上的异步函数,是 React 19 的核心特性之一。

  • 指令: 使用 'use server' 标记函数或整个文件。
  • 全链路流程:
    1. 定义: 在服务端定义异步函数。
    2. 序列化: React 自动处理参数的序列化(支持复杂对象、FormData)。
    3. 传输: 客户端调用时,React 发起一个特殊的 POST 请求,将参数序列化后传输。
    4. 执行: 服务器接收请求,反序列化参数,执行逻辑(如操作数据库)。
    5. 响应: 服务器返回执行结果,React 自动刷新相关的客户端数据(通过 Revalidation 机制)。
  • 核心优势:
    • 安全性: 自动包含 CSRF 防护,防止跨站请求伪造。
    • 简化代码: 无需手动编写 API 路由、处理 fetch 和状态更新逻辑。
    • 渐进增强: 在 JS 未加载完成时,表单提交依然可以通过原生的 form action 工作。

3. use API:统一的资源读取

use 是一个全新的运行时 API,可以在渲染时读取 Promises 或 Context。

  • 条件调用: 不同于普通的 Hooks,use 可以在 iffor 循环中调用。
  • 自动 Suspense: 当 use(promise) 还在等待时,React 会自动挂起当前组件并显示最近的 Suspense 占位符。

4. Hook 进阶:useEffectEvent (React 19.2+)

为了解决 useEffect 依赖项过多的问题,React 19.2 引入了 useEffectEvent

  • 设计初衷: 在 useEffect 中,有些逻辑需要读取最新的 propsstate,但不希望这些值的变化触发 Effect 重新运行。

  • 示例:

    function ChatRoom({ roomId, theme }) {
      // 将“纯逻辑事件”抽离
      const onConnected = useEffectEvent(() => {
        showNotification('已连接!', theme); // 始终能拿到最新的 theme
      });
    
      useEffect(() => {
        const connection = createConnection(roomId);
        connection.on('connected', () => {
          onConnected(); // 调用事件
        });
        connection.connect();
        return () => connection.disconnect();
      }, [roomId]); // ✅ theme 变化不再导致重新连接
    }
    
  • 核心逻辑: useEffectEvent 定义的函数具有“反应性”,但它不是“依赖项”。它能捕获最新的闭包值,却不会触发渲染。


5. React Server Components (RSC) 进阶

RSC 不仅仅是服务端渲染,它是一种新的组件架构。

  • 零包体积: 服务端组件的代码不会下载到浏览器,减少了 JS Bundle 大小。
  • 直接访问数据: 可以直接在组件内写 sql 查询或读取文件系统。
  • 混合模式: 通过 'use client' 指令,开发者可以精确定义客户端交互边界。

6. Web Components 原生支持

React 19 终于完美支持了 Web Components,解决了长期以来的“痛点”。

  • 属性与特性的智能映射:
    • 以前: React 总是将属性作为 Attribute 处理,导致无法传递对象或布尔值给 Web Components。
    • 现在: React 会自动检测自定义元素。如果该元素上定义了对应的 Property(属性),React 会优先使用属性赋值;否则使用 setAttribute
  • 原生事件支持:
    • 以前: 开发者需要通过 ref 手动调用 addEventListener
    • 现在: 可以像原生 DOM 一样直接使用 onMyEvent={handleEvent},React 会自动处理事件委托和解绑。
  • 跨团队协作: 这意味着大型企业可以在同一个页面中混合使用 React 组件和基于 Lit、Stencil 开发的 Web Components,而不会产生任何兼容性壁垒。

7. 开发者体验 (DX) 的全面进化

React 19 移除了许多历史包袱,让 API 变得更加直观。

  • 简化 ref 传递:

    • 以前: 必须使用 forwardRef 才能将 ref 传递给子组件。
    • 现在: ref 现在作为一个普通的 prop 传递。你可以直接在函数组件的参数中解构它:
    function MyInput({ placeholder, ref }) {
      return <input placeholder={placeholder} ref={ref} />;
    }
    
  • 文档元数据 (Metadata) 支持:

    • 开发者现在可以直接在组件中渲染 <title>, <meta>, <link>。React 会自动将它们提升(Hoist)到文档的 <head> 部分,并处理去重。
  • 静态资源加载优化:

    • React 19 引入了资源预加载 API,如 preload, preinit
    • 样式表与脚本: 支持在组件中直接声明样式表,React 会确保在组件渲染前样式已加载完成,避免闪烁(FOUC)。

底层原理深度解析

React 的底层设计旨在解决大规模应用中的 UI 响应速度和开发效率问题。其核心逻辑遵循从 “数据描述 (JSX) -> 内存模型 (Fiber) -> 任务调度 (Scheduler) -> 真实渲染 (Commit)” 的流水线。

1. JSX 的本质:声明式描述 UI

JSX(JavaScript XML)是 JavaScript 的语法扩展,本质是 React.createElement 的语法糖。

  • 源码转换JSX 通过 Babel 编译为 _jsx 调用,生成描述 UI 的普通对象(React Element)。
  • 设计初衷
    • 声明式编程:开发者只需关注 UI 的“最终状态”,而非如何操作 DOM。
    • 跨平台一致性React Element 是纯 JSON 对象,不仅能渲染为 DOM,还能渲染为原生应用(React Native)或 Canvas。

2. Fiber 架构:最小工作单元与增量渲染

Fiber 是 React 16 的核心重构,它将渲染过程从不可中断的“递归”变为了可控制的“迭代”。

  • Fiber 节点源码结构

    function FiberNode(tag, pendingProps, key) {
      // 1. 实例属性
      this.tag = tag;                 // 组件类型(Function, Class, Host...)
      this.stateNode = null;          // 对应真实 DOM 或组件实例
      
      // 2. 树结构属性 (单向链表)
      this.return = null;             // 指向父节点
      this.child = null;              // 指向第一个子节点
      this.sibling = null;            // 指向右侧兄弟节点
      
      // 3. 状态属性
      this.memoizedState = null;      // 存储 Hooks 链表
      this.updateQueue = null;        // 存储更新任务 (UpdateQueue)
      
      // 4. 并发与优先级
      this.alternate = null;          // 双缓存指向 (WIP vs Current)
      this.lanes = NoLanes;           // 当前任务优先级
      this.childLanes = NoLanes;      // 子树优先级
    }
    
  • UpdateQueue 内部结构: 每一个 Fiber 节点都有一个 updateQueue,用于存放状态更新。

    const updateQueue = {
      baseState: fiber.memoizedState,
      firstBaseUpdate: null,          // 基础更新链表头
      lastBaseUpdate: null,           // 基础更新链表尾
      shared: {
        pending: null,                // 待处理的循环链表
      },
      effects: null,                  // 存放副作用的数组
    };
    
  • Effect 链表 (副作用清理)

    Commit 阶段,React 会遍历 Effect 链表来执行 DOM 操作、生命周期或 Hooks 的 cleanup

    const effect = {
      tag: tag,                       // Hook 类型 (HookHasEffect | HookPassive)
      create: create,                 // useEffect 的第一个参数
      destroy: destroy,               // useEffect 的返回值 (cleanup)
      deps: deps,                     // 依赖项
      next: null,                     // 指向下一个 Effect
    };
    
  • 核心优势

    • 可中断性:将巨大的更新拆分为细小的 Fiber 任务,主线程可以在任务间隔处理更高优先级的用户输入。
    • 状态持久化:由于 Fiber 节点存储在内存中,即使渲染中断,之前的状态也能被保留,下次继续。

3. Fiber 树的遍历逻辑:深度优先遍历

React 采用“深度优先遍历”算法来处理 Fiber 树,这是一个典型的“递归”转“迭代”的过程。

  • beginWork 阶段:从上往下。

    • 核心逻辑:根据 React Element 的变化,决定是复用现有 Fiber 还是新建。
    • 任务:计算新的 props、计算新的 state、调用生命周期或 Hooks、打上副作用标记(Flags)。
  • completeWork 阶段:从下往上。

    • 核心逻辑
    function completeWork(current, workInProgress, renderLanes) {
      const newProps = workInProgress.pendingProps;
      switch (workInProgress.tag) {
        case HostComponent: // 真实 DOM 节点
          if (current !== null && workInProgress.stateNode != null) {
            // 更新模式:对比 props,记录差异
            updateHostComponent(current, workInProgress, tag, newProps);
          } else {
            // 创建模式:生成真实 DOM,并插入子节点
            const instance = createInstance(type, newProps, ...);
            appendAllChildren(instance, workInProgress);
            workInProgress.stateNode = instance;
          }
          break;
        // ... 其他类型处理
      }
    }
    
    • 任务
      • 构建离屏 DOM 树:在内存中完成 DOM 节点的创建和属性绑定。
      • 副作用冒泡 (Bubble up):将子树的所有 Flags 收集到父节点,这样 Commit 阶段只需遍历根节点的 Flags 链表。
  • 带来的性能体验: 这种双向遍历确保了 React 可以在中途暂停,并在恢复时准确知道当前处理到的位置。通过“副作用冒泡”,Commit 阶段的执行速度得到了极大的提升。

4. 双缓存 (Double Buffering) 机制

React 在内存中维护两棵 Fiber 树:current 树(屏幕显示)和 workInProgress 树(正在构建)。

  • 设计初衷
    • 避免 UI 破碎:如果直接在 current 树上修改,用户可能会看到渲染到一半的页面。
    • 极致性能:构建完成后,只需切换 FiberRoot 指针即可完成整棵树的更新,这种“内存交换”比逐个修改 DOM 节点快得多。

5. Scheduler 与时间切片

Scheduler 是 React 的心脏,负责任务的全局调度。

  • 时间切片 (Time Slicing):React 默认每 5ms 会让出一次主线程。它通过 MessageChannel 模拟宏任务。
  • 设计初衷:即使在执行极其复杂的渲染任务(如万级列表),页面依然能响应用户的点击和输入,彻底解决了 JavaScript 阻塞主线程导致的“卡死”感。

6. Lanes 优先级模型

React 17 引入了基于 31 位位掩码的 Lanes 模型。

  • 设计优势
    • 多任务并行:相比旧的 ExpirationTimeLanes 可以表示“一组”任务优先级。
    • 任务插队:React 可以准确识别出最高优先级任务,优先处理它,并将正在进行的低优先级任务挂起或废弃。

7. 合成事件 (Synthetic Events) 与批处理 (Batching)

React 并不直接使用浏览器的原生事件,而是实现了一套全平台的合成事件机制。

  • 合成事件原理:
    • 事件委派: React 17+ 将事件绑定在 root 容器上,而不是 document
    • 对象池化: (注:React 17 之后已移除池化,改为直接传递)。
    • 跨平台映射: 将不同浏览器的差异(如 transitionend, animationend)封装为统一的 API。
  • 自动批处理 (Automatic Batching):
    • 原理: React 会将多个状态更新合并为一次渲染。
    • React 18/19 的突破: 以前只有在 React 事件处理函数中才有批处理。现在,无论是在 PromisesetTimeout 还是原生事件中,所有的更新都是自动批处理的。
    • 底层实现: 通过 ExecutionContext(执行上下文)标记。当 React 发现处于“更新流程”中时,它不会立即触发渲染,而是将更新放入 UpdateQueue,等待主任务结束后一次性处理。

8. 协调 (Reconciliation) 过程深度拆解

协调是 React 区分“计算”与“渲染”的核心。

  • 阶段拆分:
    1. Render 阶段 (异步/可中断): 生成 Fiber 树,计算差异。
    2. Commit 阶段 (同步/不可中断):
      • BeforeMutation: 处理 DOM 渲染前的逻辑(如 getSnapshotBeforeUpdate)。
      • Mutation: 真正操作 DOM(增删改)。
      • Layout: 渲染后的逻辑(如 useLayoutEffect)。
  • 事务机制 (Transaction):
    • 虽然 React 源码中没有直接命名为 Transaction 的类,但其更新流程遵循典型的事务模式:performSyncWorkOnRoot 开启事务 -> 执行更新 -> commitRoot 结束事务并清理环境。

并发渲染 (Concurrent Rendering) 深度解析

并发渲染是 React 18+ 的核心能力,它改变了 React 处理更新的基础方式。

1. 传统渲染 vs 并发渲染

  • 传统渲染 (Stack Reconciler):渲染过程是同步且不可中断的。如果一个组件树很大,浏览器会一直忙于计算,无法响应用户操作。
  • 并发渲染:React 可以在渲染过程中暂停。如果用户点击了按钮,React 会暂停当前的渲染,处理点击事件,然后再恢复之前的渲染。

2. 并发特性的核心:Transitions

通过 startTransition,开发者可以告诉 React 哪些更新是“不紧急”的。

  • 应用场景:输入框打字是紧急的,下方的搜索结果列表更新是不紧急的。
  • 底层实现startTransition 会将更新标记为低优先级的 Lane,使得紧急更新(输入)可以打断它。

流式 SSR 与 Suspense 架构

React 18+ 彻底重塑了服务端渲染 (SSR) 的工作流程。

1. 传统的 SSR 瓶颈

在 React 18 之前,SSR 必须经历:

  1. 服务器拉取所有数据
  2. 生成整个 HTML
  3. 客户端下载整个 JS
  4. 整个页面进行 Hydration

任何一个环节慢了,用户都会看到白屏或无法交互。

2. 流式 SSR (Streaming SSR)

React 现在支持通过 renderToPipeableStream 将 HTML 分块发送给浏览器。

  • 结合 Suspense: 页面可以先显示外壳,耗时较长的组件(如评论列表)在服务器端准备好后再“流”向客户端,并自动插入到正确位置。
  • 选择性注水 (Selective Hydration): 用户点击了还没注水的组件时,React 会优先为该组件进行注水,提升了交互的实时性。

隐藏的宝藏:Offscreen (Activity) 模式

React 19 引入了 <Activity> 组件(实验性名称为 Offscreen API),开启了“智能预渲染”的大门。

  • 核心原理: 允许 React 在后台渲染组件树,而不将其挂载到真实的 DOM 上。
  • 运行模式:
    • hidden 模式:
      • DOM 隐藏: 组件的 DOM 节点被隐藏或不创建。
      • Effect 卸载: 所有的 useEffect 会执行 cleanup,避免后台任务占用过多资源。
      • 状态保留: 组件内部的 useStateuseReducer 状态会被完整保留。
      • 低优先级更新: 当 React 处理完所有可见任务后,会利用空闲时间悄悄更新 hidden 的树。
    • visible 模式: 组件瞬间恢复可见,useEffect 重新挂载,UI 立即同步到最新状态。
  • 优势与场景:
    • 瞬间回退 (Back Navigation): 用户点击“返回”按钮时,之前的页面可以瞬间重现,无需重新加载数据。
    • 标签页切换 (Tabs): 预先渲染非活跃的 Tab 页面,切换时零延迟。
    • 列表预加载: 当用户滚动列表时,提前渲染屏幕下方的几个节点。

性能优化进阶:Transition Tracing & Profiler

为了帮助开发者量身定制性能方案,React 19 提供了更强大的追踪工具和底层的调度观察能力。

1. Transition Tracing (过渡追踪)

允许开发者监听特定的“过渡任务”的生命周期。

  • onTransitionStart / onTransitionProgress: 可以精准监控 startTransition 开启的任务。

    import { unstable_useTransitionTracing } from 'react';
    
    function SearchPage() {
      unstable_useTransitionTracing('search-results', {
        onTransitionStart: (startTime) => {
          console.log('搜索开始', startTime);
        },
        onTransitionComplete: (endTime) => {
          console.log('搜索渲染完成', endTime);
        }
      });
      // ...
    }
    
  • 核心价值: 帮助开发者识别哪些复杂的渲染导致了 UI 的延迟,从而决定是否需要拆分组件或优化数据结构。

2. DevTools Profiler 增强

现在的 Profiler 可以清晰展示每个任务所属的 Lane(优先级级别)。

  • 优先级可视化: 开发者可以看到哪些任务是 User Blocking(用户阻塞,高优先级),哪些是 Transition(过渡,低优先级)。
  • 任务插队分析: Profiler 会标注出哪些任务是因为被更高优先级的任务“插队”而暂停的,这对于调试复杂的并发逻辑至关重要。

协调过程与 Diff 算法深度解析

协调是计算“变了什么”的过程,而 Diff 是其中的核心算法。

1. 核心策略:O(n) 复杂度

React 的 Diff 算法基于三个预设限制:

  • 同层比较:只比较同级节点,跨层级移动会被视为删除和重新创建。
  • 类型判断:如果节点类型变了,直接销毁旧树,创建新树。
  • Key 标识:通过 key 属性,开发者可以告知 React 哪些元素在不同渲染中是稳定的。

2. 多节点 Diff 的“两次遍历”

  1. 第一轮遍历:从左往右对比新旧节点。如果 keytype 都匹配,复用;否则跳出。
  2. 第二轮遍历:将剩余的旧节点放入 Map。遍历新节点时,尝试从 Map 中通过 key 寻找可复用的节点,从而高效处理位移。

Hooks 底层:基于链表的状态管理

Hooks 的状态存储在 Fiber 节点的 memoizedState 单向链表中。

1. Hook 对象结构

const hook = {
  memoizedState: null, // 存储 useState 的值、useEffect 的 effect 等
  baseState: null,
  baseQueue: null,
  queue: null,         // 状态更新队列
  next: null,          // 下一个 Hook
};

2. 闭包陷阱的本质

当 Hook 在渲染过程中被调用时,它会读取当前 Fiber 的状态。如果异步操作引用了旧的变量,而组件已经重新渲染,就会产生闭包陷阱。这正是 useEffect 依赖项数组存在的原因。


未来展望:React Compiler (React Forget)

为了进一步提升性能,Meta 正在开发 React Compiler

  • 自动记忆化: 目前开发者需要手动使用 useMemouseCallback。编译器将通过静态分析,自动插入这些优化代码。
  • 性能体验: 彻底告别手动性能优化,让 React 应用在默认情况下就拥有极致的运行效率。

总结:Meta 为什么这样设计?

Meta(原 Facebook)之所以设计这套极其复杂的源码结构,其核心目标只有一个:在保证开发者体验(DX)的同时,提供极致的用户体验(UX)。

  1. 响应性优先:通过 FiberScheduler,确保用户操作永远拥有最高优先级。
  2. 内存换速度:通过“虚拟 DOM”和“双缓存”,用内存中的对象运算来换取昂贵的真实 DOM 操作。
  3. 架构的生命力LanesConcurrent Mode 的引入,让 React 从一个简单的 UI 库进化成了一个能处理复杂调度任务的“前端操作系统”。
  4. 全栈融合:React 19 的 Server ActionsRSC 标志着 React 正在从“UI 库”向“全栈框架”迈进,试图统一前后端的开发模型

React 基础理论 & API 使用

React 基础理论 & API 使用

本文主要记录一些关于 React 的基础理论、核心概念以及常用 API 的使用方法,供查漏补缺。


原文地址

墨渊书肆/React 基础理论 & API 使用


React 简介

React 是一个由 Facebook(现在称为 Meta)开发的开源 JavaScript 库,主要用于构建用户界面,特别是单页应用程序(SPA)的开发。React 不仅限于 Web 开发,通过React Native,开发者还可以使用几乎相同的组件化开发方式来构建原生移动应用程序,实现了跨平台的代码复用。由于其灵活性和高效性,React 已成为现代 Web 开发中最受欢迎的前端库之一。

核心特点

  • 组件化编程:React 将页面和功能分解为可复用的组件,每个组件可以管理自己的状态 and 渲染逻辑,大大提高了代码的可维护性和可重用性。
  • Virtual DOM:引入虚拟 DOM 的概念,它是一个树形数据结构,用来表示真实 DOM 的抽象。当状态发生改变时,在Render阶段会先计算VDOM的最小更新,然后在Commit阶段生成真实 DOM,减少了浏览器的重排和重绘,提高了性能。
  • 声明式编程:React 使用声明式的方式定义页面的 UI 和状态逻辑,让代码更容易理解。
  • JSX:允许开发者在 JS 中混写 HTML-like 的语法,这种语法糖被称为JSX,可以更直观和简洁的描述组件的结构。
  • 单向数据流:React 应用遵循单向数据流的原则,父组件向子组件传递状态(props)和回调函数,子组件通过调用这些回调来通知父组件的状态变更,这有助于保持数据流的清晰和可预测性。这点有别于 Vue 的双向绑定。

安装

在搭建 React 框架时,我们现在通常使用目前更主流、构建速度更快的 Vite,它是现代前端开发的优选脚手架。

# 使用 Vite 创建项目
npm create vite@latest my-react-app -- --template react

如果你选择其他框架或工具链,也有对应的安装方式:

  • Next.jsnpx create-next-app@latest
  • UmiJS:使用 create-umi

组件通讯方式

在 React 中,组件间的通信主要有以下几种方式:

  1. 通过 props 向子组件传递数据:父组件通过属性将数据传递给子组件。
  2. 通过回调函数向父组件传递数据:父组件向子组件传递一个函数,子组件调用该函数并传入数据。
  3. 使用 Refs 调用子组件暴露的方法:通过 forwardRefuseImperativeHandle 钩子,父组件可以访问子组件内部定义的方法。
  4. 通过 Context 进行跨组件通信:使用 createContextuseContext 实现跨层级的状态共享。
  5. 使用状态管理库:如 ReduxMobXZustand 等进行全局状态管理。

生命周期

经典生命周期

在React 16.3之后的生命周期可以分为三个阶段:

挂载阶段(Mounting):

  • constructor: 组件实例化时调用,初始化 state 和绑定 this
  • getDerivedStateFromProps: (React 16.3新增)在组件实例被创建后续更新时被调用,用于根据 props 来计算 state
  • render: 根据 state 和 props 渲染UI到虚拟 DOM。
  • componentDidMount: 组件已经被渲染到 DOM 后调用,常用于发起网络请求、设置定时器等。

更新阶段(Updating):

  • getDerivedStateFromProps: 同挂载阶段。
  • shouldComponentUpdate: 判断是否需要更新 DOM,返回 true/false。
  • render: 状态或props改变时再次渲染 UI。
  • getSnapshotBeforeUpdate: (React 16.3新增)在 DOM 更新前调用,可以获取一些信息用于在 componentDidUpdate 中使用。
  • componentDidUpdate: 组件更新后立即调用,可以进行 DOM 操作或网络请求。

卸载阶段(Unmounting):

  • componentWillUnmount: 组件将要卸载时调用,清理工作如取消网络请求、清除定时器等。

从React 16.3开始,componentWillMount, componentWillReceiveProps, 和 componentWillUpdate 被标记为不安全的,并最终在React 17中被废弃。React推荐使用getDerivedStateFromProps和useState、useEffect等Hooks来替代。

Hooks 生命周期模拟

对于 React 函数组件,现在的实践更倾向于使用如下的 Hooks 生命周期:

  • useState: 用于组件内部状态管理。

  • useEffect: 用于处理副作用,可模拟以下生命周期:

    • 模拟挂载阶段 (componentDidMount): 依赖数组传空 []
    useEffect(() => { /* 只在挂载后执行 */ }, []);
    
    • 模拟更新阶段 (componentDidUpdate): 不传依赖数组或传入特定依赖。
    useEffect(() => { /* 每次渲染后都执行 */ });
    useEffect(() => { /* count 变化后执行 */ }, [count]);
    
    • 模拟卸载阶段 (componentWillUnmount): 在 useEffect 中返回一个清理函数。
    useEffect(() => {
      return () => { /* 组件卸载前执行 */ };
    }, []);
    
  • useContext: 用于从上下文中消费值。

  • useRef: 用于持久化一个可变的引用对象,不会引起组件重新渲染。

  • useReducer: 用于有复杂状态逻辑的组件,替代某些 useState 的使用场景。

  • useCallbackuseMemo: 用于优化性能,避免不必要的函数或计算的重新创建。

父子组件生命周期调用顺序

在函数组件中,挂载阶段的执行顺序如下:

  1. 父组件执行函数体(首次渲染)。
  2. 子组件执行函数体(首次渲染)。
  3. 子组件执行 useEffect(挂载完成)。
  4. 父组件执行 useEffect(挂载完成)。

更新阶段:

  1. 父组件重新渲染。
  2. 子组件重新渲染。
  3. 子组件 useEffect 清理函数执行。
  4. 父组件 useEffect 清理函数执行。
  5. 子组件 useEffect 执行。
  6. 父组件 useEffect 执行。

组件类 API

PureComponent

PureComponentComponent 的子类,是基于 shouldComponentUpdate 的一种优化方式。使用 PureComponent 的主要优点在于它自动执行了浅比较来检查 props 和 state 是否有变化,没有变化的时候不会重新渲染,从而提高了性能,减少了不必要的计算和 DOM 操作。

import React, { PureComponent } from 'react';

class MyComponent extends PureComponent {
  render() {
    return (
      <div>
        {this.props.text}
      </div>
    );
  }
}

export default MyComponent;

memo

React.memo 是 React 中用于函数组件的性能优化手段,它是一个高阶函数,用来包装一个函数组件,并利用引用地址比较(浅比较)来决定是否重新渲染该组件。当组件的 props 没有发生变化时(基于浅比较),则跳过重新渲染,从而提高性能。

import React, { memo } from 'react';

const MyComponent = memo((props) => {
  // 组件逻辑...
  return <div>{props.text}</div>;
});

// 自定义比较函数:可以通过传递第二个参数给memo来自定义比较逻辑,这允许你实现深度比较或其他定制化的比较策略。
const MyComponent = memo((props) => {...}, (prevProps, nextProps) => {
  // 自定义比较逻辑
  // 返回true如果 props 没有变化,无需重新渲染
  // 返回false如果 props 有变化,需要重新渲染
  return prevProps.text === nextProps.text;
});

createRef

createRef 是 React 中管理 DOM 元素或组件实例引用的一个现代、灵活的方法,有助于处理表单、动画交互、原生DOM操作等场景。

class MyComponent extends React.Component {
  myInputRef = React.createRef();

  componentDidMount() {
    // 在组件挂载后访问DOM元素
    this.myInputRef?.current?.focus();
  }

  render() {
    return <input type="text" ref={this.myInputRef} />;
  }
}

forwardRef

forwardRef 是 React 中的一个高阶组件(HOC),它允许我们将 React 的 refs 转发到被包裹的组件中,即使这个组件是一个函数组件。这在需要访问子组件的 DOM 节点或者想要从父组件传递一些引用到子组件的场景下非常有用。极大地增强了函数组件的能力,使得它们在处理需要直接操作DOM或传递引用的场景下更加灵活和强大。

import React, { forwardRef } from 'react';

// 第一个参数是React.forwardRef接收的render函数,它接收两个参数:props和ref
const MyForwardedComponent = forwardRef((props, ref) => {
  // 现在你可以在这个函数组件内部使用ref了
  return <input type="text" ref={ref} {...props} />;
});

// 使用forwardRef的组件时,可以像普通组件那样使用ref
class ParentComponent extends React.Component {
  myInputRef = React.createRef();

  focusInput = () => {
    this.myInputRef?.current?.focus();
  };

  render() {
    return (
      <>
        <MyForwardedComponent ref={this.myInputRef} />
        <button onClick={this.focusInput}>Focus Input</button>
      </>
    );
  }
}

createContext

createContext 是 React 中的一个API,用于创建一个“context”对象。Context 提供了一种在组件树中传递数据的方式,而不必显式地通过每一个层级手动传递 props。这使得在不同层级的组件中共享数据变得简单且高效,特别适合管理如主题语言设置认证信息等全局状态。

基本用法:

  • 创建 Context: createContext(defaultValue)
import React from 'react';
// 创建一个context
const MyContext = React.createContext('light');
  • Provider组件: 注入值上下文
class App extends React.Component {
  state = {
    theme: 'light',
  };

  render() {
    return (
      // 通过Provider组件向上下文中注入值
      <MyContext.Provider value={this.state.theme}>
        <ComponentThatNeedsTheContext />
      </MyContext.Provider>
    );
  }
}
  • Consumer组件: 读取上下文的方法
function ComponentThatNeedsTheContext() {
  return (
    <MyContext.Consumer>
      {theme => /* 使用theme值 */}
    </MyContext.Consumer>
  );
}

或者使用 useContext Hook:

import React, { useContext } from 'react';

function ComponentThatNeedsTheContext() {
  const theme = useContext(MyContext);
  // 现在可以使用theme值
  return <div>{theme}</div>;
}

注意事项:

Context 会随着组件树的遍历而传递,无论组件是否使用了这个 Context。因此,应当谨慎使用,避免创建过多的 Context,尤其是嵌套使用时。

createElement

createElement 在我们的平时使用中较少,它用于创建 React 元素,是构成用户界面的基本单位,我们常写的 JSX 就是 createElement 的语法糖,所以还是很有必要了解这个 API 的。

基本语法:

React.createElement(
  type, // 通常是一个字符串(对应HTML标签名)或一个React组件(函数组件或类组件的构造函数)。
  [props], // 一个对象,用于传递给组件的属性。它可以包含事件处理器、样式等。
  [...children] // 代表组件的子元素,可以是一个React元素、字符串或数字,也可以是这些类型的数组。
)

示例:

const element = React.createElement(
  'div',
  { id: 'example', className: 'box' },
  'Hello, world!'
);

这段代码等同于下面的JSX写法:

<div id="example" className="box">
  Hello, world!
</div>

cloneElement

cloneElement 是 React 提供的一个方法,用于克隆并返回一个新的 React 元素,同时可以修改传入元素的 props,甚至可以添加或替换子元素。这个方法常用于在高阶组件中,或者任何需要基于现有元素创建一个具有额外 props 或不同子元素的新元素的场景。

基本语法:

React.cloneElement(
  element, // 要克隆的React元素
  [props], // 一个对象,包含了要添加或覆盖到原始元素props上的新属性
  [...children] // 可选的,用于替换或追加子元素到克隆后的元素中
)

自定义 HOC

高阶组件(Higher-Order Components, HOC)是 React 中用于重用组件逻辑的一种高级技术。HOC 本身不是 React API 的一部分,而是一种从函数式编程原则中借来的模式。一个 HOC 是一个接受组件作为参数并返回一个新的增强组件的函数。

function withEnhancement(WrappedComponent) {
  return function EnhancedComponent(props) {
    // 添加额外的props或逻辑
    const newProps = { ...props, enhancedProp: "Enhanced Value" };
    
    // 渲染被包装的组件,并传递新的props
    return <WrappedComponent {...newProps} />;
  };
}

注意事项:

  • 不要修改传入组件的props: 最好是通过组合新的 props 而不是修改原有的 props来保持纯净性。
  • 命名约定: 通常 HOC 函数名以 with 开头,以表明它是一个 HOC。
  • 文档和测试: 编写清晰的文档说明 HOC 的功能和用法,并确保充分测试,以防止引入bug。

Hooks

React Hooks 是React 16.8版本引入的一个新特性,在不编写类的情况下使用 React 的状态和其他生命周期特性。Hooks 使函数组件的功能更加丰富,使得函数组件逻辑更易于理解和重用。

useState

允许在函数组件中添加状态(state)。它返回一个状态变量和一个用来更新这个状态的函数。

const [count, setCount] = useState(0);

useEffect

useEffect 是 React Hooks 系统中的一个重要成员,它主要用于执行副作用操作,比如数据获取、订阅或者手动修改 DOM 等。此 Hook 允许你同步副作用与 React 组件的生命周期,替代了类组件中的一些生命周期方法,如 componentDidMountcomponentDidUpdatecomponentWillUnmount

// useEffect 接收两个参数:一个包含副作用操作的函数,和一个依赖项数组
useEffect(() => {
  // 副作用操作:订阅或数据获取等
  document.title = `You clicked ${count} times`;

  // 可选的清理函数,用于在下次effect执行前或组件卸载时清理副作用
  return () => {
    // 清理操作,例如取消网络请求或移除事件监听器
  };
}, [count]); // 依赖项数组,当这些值变化时触发effect重新执行

useContext

useContext 是 React Hooks 系统中的一个 API,它使你能够在组件树中无需通过 props 逐层传递,就能访问到全局状态或其他组件上下文中的值。这对于管理如主题、语言、认证信息等跨多个组件共享的数据尤为有用。

import React, { useContext } from 'react';

function ComponentThatNeedsTheContext() {
  const theme = useContext(MyContext);
  // 现在可以使用theme值
  return <div>{theme}</div>;
}

useRef

useRef 是 React Hooks 系统中的一个API,它用于创建一个可变的引用对象(ref),这个对象的.current属性被初始化为传递的参数(initialValue)。useRef的主要用途是在渲染之间持久化一个可变的值,并且可以用来直接访问 DOM 元素或在函数组件之间保持一些状态。

import React, { useRef } from 'react';

function TextInputWithFocusButton() {
  // 初始化一个ref,用来存放input元素的引用
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // 当按钮被点击时,让input元素获取焦点
    inputEl?.current?.focus();
  };

  return (
    <>
      {/* 将input元素的引用赋给useRef返回的对象 */}
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useReducer

useReducer 是React中的一个Hook,它用于管理组件中的状态,特别适用于状态更新逻辑较复杂的场景。

基本用法:

import React, { useReducer } from 'react';

// 定义reducer函数
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
};

// 初始化状态
const initialState = { count: 0 };

function Counter() {
  // 使用useReducer,传入reducer函数和初始状态
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

注意事项:

  • 确保reducer函数是纯函数,即给定相同输入始终产生相同输出,不产生副作用。
  • 选择合适的状态管理方式,对于简单的状态管理,useState可能更直观易用。
  • 利用useCallback来记忆化dispatch函数,避免在每个渲染周期都创建新的函数引用,进而减少不必要的子组件重渲染。

useMemo

useMemo 是React中的一个Hook,用于优化性能,避免在每次渲染时都进行复杂的计算。它让你能够 memoize(记忆化)一个值,这个值是基于某些依赖项计算出来的,只有当这些依赖项改变时,才会重新计算这个值。

基本用法:

import React, { useMemo } from 'react';

function MyComponent({ list }) {
  // 使用useMemo进行性能优化
  const sortedList = useMemo(() => {
    console.log('Sorting list');
    return list.sort((a, b) => a - b);
  }, [list]); // 依赖项数组,当list变化时才重新计算sortedList

  return (
    <div>
      {sortedList.map(item => (
        <div key={item}>{item}</div>
      ))}
    </div>
  );
}

注意事项:

  • 不要过度使用: 虽然useMemo可以帮助优化性能,但是不必要的使用反而可能导致额外的性能开销,特别是在计算简单或频繁变化的值时。
  • 理解其限制: useMemo不会阻止其依赖项内的对象或数组的内部变化触发重渲染。只有当依赖项的引用本身发生变化时,才会触发重计算。
  • 与React.memo区别: React.memo是一个高阶组件,用于记忆化整个组件,防止不必要的渲染,而useMemo是记忆化组件内部的某个值或计算结果。

useCallback

useCallback 是 React中 的另一个性能优化 Hook,它用于记忆化函数。与 useMemo 相似,useCallback 也用于避免在每次渲染时都进行新的函数引用,但它的主要应用场景是当这些函数作为 props 传递给子组件时,帮助子组件避免不必要的重新渲染。

import React, { useCallback, useState } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);

  // 使用useCallback记忆化increment函数
  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count, setCount]); // 依赖项数组,当这些值变化时,才会生成新的increment函数

  return <ChildComponent onClick={increment} />;
}

function ChildComponent({ onClick }) {
  // ...
}

注意事项:

  • 与useMemo的区别: useMemo 适用于记忆化计算值或对象,而 useCallback 专门用于记忆化函数。
  • 避免闭包陷阱: 在使用 useCallback 时,需要注意函数内部引用的外部变量也应包含在依赖项数组中,以确保正确的重渲染逻辑。

ts随笔:面向对象与高级类型

ts随笔:面向对象与高级类型

本篇主要聚焦在类、模块、高级类型以及在常见前端框架中的实践,同时结合生态中新出现的一些特性,如何自然地用上这些新能力。


原文地址

墨渊书肆/ts随笔:面向对象与高级类型


类(Class)

类是面向对象编程的基础,用于创建具有属性(数据成员)和方法(成员函数)的对象的蓝图。TypeScript 中的类支持 继承封装多态 等面向对象特性。

基本语法

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

const person = new Person("Alice", 30);
person.greet();

在较新的 TypeScript 版本中,你也可以结合 ECMAScript 的 私有字段 语法(以 # 开头),在保持类型安全的同时实现更彻底的封装:

class Counter {
  #value = 0;

  increment() {
    this.#value++;
  }

  get value(): number {
    return this.#value;
  }
}

继承

class Student extends Person {
  studentId: string;

  constructor(name: string, age: number, studentId: string) {
    super(name, age);
    this.studentId = studentId;
  }

  study() {
    console.log(`${this.name} is studying.`);
  }
}

const student = new Student("Bob", 20, "S123");
student.greet();
student.study();

借助 TypeScript 的严格类型系统,继承关系中的属性和方法都会得到完整的类型检查支持,在重写方法时也能获得参数和返回值的约束。

模块(Module)

模块是用于组织代码的容器,它允许你将相关联的类、接口、函数等封装在一个单独的文件中,并可以控制它们的可见性(导出/导入)。模块有助于避免命名冲突和促进代码的复用。

导入与导出

// moduleA.ts
export class MyClass {
  // ...
}

// 在其他文件中使用导出的元素
import { MyClass } from "./moduleA";

const myInstance = new MyClass();

默认导出命名导出 可以混合使用,但在一个模块中只能有一个默认导出;命名导出则可以有多个。

命名空间与模块的异同

在早期版本的 TypeScript 中,命名空间(Namespace)是另一种组织代码的方式,它类似于 C# 或 Java 中的包,提供了一种分层次的方式来组织代码。虽然模块现在是推荐的做法,但命名空间仍然可用,特别是在需要合并多个文件定义的命名空间时。

面向未来的模块特性:JSON 模块import defer

从 ES2025 开始,JSON 模块 等特性有望在主流环境中稳定可用,你可以直接以模块的方式导入 JSON 文件,并配合 TypeScript 的类型系统进行约束:

// config.json
// {
//   "apiBaseUrl": "https://api.example.com",
//   "featureFlags": {
//     "newUI": true
//   }
// }

interface FeatureFlags {
  newUI: boolean;
}

interface AppConfig {
  apiBaseUrl: string;
  featureFlags: FeatureFlags;
}

// 在支持 JSON 模块的环境下
import configJson from "./config.json" with { type: "json" };

const config = configJson as AppConfig;

在 ES2026 及之后,import defer 等语法提案逐步成熟时,可以在保持语义清晰的前提下延迟加载非关键模块,而 TypeScript 依然会对导入的符号进行完整的类型检查:

// 伪代码示意:具体语法以最终标准为准
// import defer "./heavy-analytics.js";

// TypeScript 关注的是导出的类型本身,只要声明文件同步更新,
// 即使底层加载时机发生变化,类型系统仍然保持稳定。

和上一篇中提到的声明文件一样,这些新的模块特性最终都会通过 .d.ts 的方式落地到 TypeScript 生态中。

高级类型探索

泛型 Generics

泛型(Generics)是 TypeScript 中一个强大的特性,它允许你在定义函数、接口或类的时候不预先指定具体的类型,而是将类型作为参数传递。

基本概念

泛型的核心在于使用类型变量(通常用大写字母表示,如 T、U 等)来代表一些未知的类型。当使用这个组件时,你再指定这些类型变量的具体类型。

泛型函数
function identity<T>(arg: T): T {
  return arg;
}

let output = identity<string>("hello");
console.log(output);

let numberOutput = identity<number>(123);
console.log(numberOutput);
泛型接口
interface Pair<T> {
  first: T;
  second: T;
}

let pairStr: Pair<string> = { first: "hello", second: "world" };
let pairNum: Pair<number> = { first: 1, second: 2 };
泛型类
class Box<T> {
  private containedValue: T;

  set(value: T) {
    this.containedValue = value;
  }

  get(): T {
    return this.containedValue;
  }
}

let boxStr = new Box<string>();
boxStr.set("hello");
console.log(boxStr.get());

let boxNum = new Box<number>();
boxNum.set(123);
console.log(boxNum.get());
泛型约束

有时候,你可能需要限制可以作为类型参数的具体类型,这时候可以使用泛型约束。泛型约束通过接口来定义,要求传入的类型必须满足该接口定义的条件。

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity({ length: 10, value: "test" });
// loggingIdentity(123); // 错误:number 没有 length 属性

联合类型 Union Types

联合类型 允许一个变量可能是多种类型之一。例如,你可以定义一个变量既可能是字符串也可能是数字:

let myValue: string | number;
myValue = "Hello";
myValue = 42;

类型守卫 Type Guards

当你在操作联合类型的变量时,TypeScript 可能无法确定变量的具体类型,这会影响到你能够调用的方法或访问的属性。类型守卫 就是用来缩小类型范围,确保在运行时变量属于某种特定类型。

typeof 类型守卫
if (typeof myValue === "string") {
  console.log(myValue.toUpperCase());
} else {
  console.log(myValue.toFixed(2));
}
instanceof 类型守卫
class Animal {}

class Dog extends Animal {
  bark() {
    console.log("Woof!");
  }
}

function isDog(animal: Animal): animal is Dog {
  return animal instanceof Dog;
}

let pet = new Dog();
if (isDog(pet)) {
  pet.bark();
}
in 操作符
interface Cat {
  meow: () => void;
}

function makeSound(animal: Animal | Cat) {
  if ("meow" in animal) {
    animal.meow();
  } else {
    console.log(animal.toString());
  }
}

Iterator Helpers 与 Set 扩展下的类型推断

在 ES2025、ES2026 相关提案中,Iterator HelpersSet 扩展 是非常值得关注的一类特性:它们让各种可迭代对象(包括数组、Set、Map 的键值迭代器等)拥有类似链式操作的能力。

当对应的类型定义进入 TypeScript 之后,可以配合泛型和类型守卫写出既简洁又安全的代码。例如,以 Set 扩展为例:

// 假设运行时与 TypeScript lib 均已支持 Set 的扩展方法
const ids = new Set([1, 2, 3, 4, 5]);

// filter 返回的仍然是 Set<number>,类型信息由泛型推断而来
// const evenIds = ids.filter((id) => id % 2 === 0);

// map 等其他 Iterator Helpers 也同理可以得到明确的类型
// const idStrings = ids.map((id) => `id-${id}`);

虽然上面的代码在当前某些环境中还处于“提案阶段”,但可以预期的是,未来在 TypeScript 中使用这些 API 时,你同样能获得完整的泛型推断和类型守卫支持。

日期时间与本地化:TemporalIntl.Locale

时间与本地化一直是前端开发中的老大难问题。Temporal 和 Intl.Locale 等提案正是为了解决 Date 语义不清、Intl 配置复杂等问题。

Temporal 定稿并进入主流运行时时,你可以在 TypeScript 中这样书写代码:

// 假设 lib 已经包含 Temporal 与最新的 Intl 声明
// const now: Temporal.ZonedDateTime = Temporal.Now.zonedDateTimeISO();
// const locale = new Intl.Locale("zh-CN", { calendar: "gregory" });

// console.log(now.toLocaleString(locale.toString()));

这些 API 本身是 JavaScript 语言层面的特性,但它们的类型声明会第一时间进入 TypeScript 官方声明文件,从而让我们在使用它们时也能享受完整的类型推断、自动补全和错误检查。

ts 在 React 中的使用

新项目使用 create-react-app 接入

npx create-react-app my-app --template typescript

React 老项目接入

首先安装 @types/react@types/react-dom 这些 React 的类型定义文件:

npm install --save-dev @types/react @types/react-dom

然后将 .js 文件逐步转换为 .tsx(TypeScript 支持 JSX 的文件扩展名)并添加类型注释。

React 代码编写

import React, { useState } from "react";

interface Props {
  name: string;
}

const Hello: React.FC<Props> = ({ name }) => {
  const [message, setMessage] = useState<string>("Hello");

  return (
    <div>
      <h1>{`${message}, ${name}!`}</h1>
      <button onClick={() => setMessage("Welcome")}>Change Message</button>
    </div>
  );
};

export default Hello;

在较新的 TypeScript 与 React 生态中,配合前面提到的 JSON 模块Iterator HelpersTemporal 等能力,你可以更放心地在组件中使用这些新特性——只要升级依赖并确保声明文件同步更新,编辑器就会用类型系统帮你“兜住”大部分错误。

ts 在 Vue 3 中的使用

新项目使用 Vue CLI 接入

vue create my-vue3-project --preset typescript

Vue 老项目接入

vue add typescript

Vue 代码编写

<script lang="ts">
import { defineComponent, ref, reactive } from "vue";

interface Props {
  msg: string;
}

export default defineComponent({
  props: {
    msg: String,
  },
  setup(props: Props) {
    const count = ref(0);
    const state = reactive({ status: "active" });

    // 在这里同样可以安心地使用前文提到的高级类型、
    // Iterator Helpers 或 Temporal 等能力,TypeScript
    // 会在编译阶段帮你把控类型安全。

    return {
      count,
      state,
    };
  },
});
</script>

无论是 React 还是 Vue,TypeScript 都会继续扮演“粘合剂”的角色:只要按需升级依赖、合理配置 tsconfig,就能够在习惯的写法下自然享受到这些新特性带来的收益。

AI辅助开发实战:会问问题比会写代码更重要

AI辅助开发实战:会问问题比会写代码更重要

系列第二篇。我想聊聊怎么用好 AI 这个工具。不是教你怎么敲代码,而是教你,怎么真正用好AI辅助开发工具。


原文地址

墨渊书肆/AI辅助开发实战:会问问题比会写代码更重要


你有没有过这样的经历?

打开Cursor(或者TraeCopilot),对着空白编辑器发了半天呆,不知道该让AI帮你干什么。

或者你问了一句「帮我写个登录功能」,AI 噼里啪啦写了一大堆代码,你看都看不懂,最后只能硬着头皮复制粘贴。

再或者,你问 AI:「这个报错是什么意思?」它回了一堆你看不懂的术语,你更迷茫了。

如果你有以上任何一种经历,这篇文章就是写给你的。


会问问题,比会写代码更重要

这是我最近一年用 AI 辅助开发最大的感悟。

以前我觉得,AI 嘛,就是个更聪明的搜索引擎。我不会的代码问它,它告诉我怎么写呗。

后来发现不是这么回事。

同样一个问题,不同的问法,AI 给出的答案质量可以差十倍。

AI 不会读心术。你得把自己的需求翻译成 AI 能理解的语言。

举两个例子感受一下:

第一种问法:「帮我写个登录功能。」

AI给你一个标准答案:用户名密码输入框、提交按钮、后端接口、数据库查询。看起来很全,但放到你的项目里可能完全不适用。你要改吧,改到猴年马月。不要吧,扔掉又可惜。

第二种问法:「我的项目用Next.jsPrisma,用户表字段是 email 和 passwordHash。请帮我写一个登录API,要支持邮箱密码登录,密码用 bcrypt 加密,返回 JWT token,7天有效期。」

AI给你的代码,直接就能用。稍微调一下就能跑。

这就是差距。好的 Prompt 不是更长的Prompt,而是更精确的 Prompt。


几个基本概念

在开始讲技巧之前,先简单说几个你经常会遇到的术语:

LLM:Large Language Model,大语言模型。你可以把LLM理解为"大脑",GPT、Claude、DeepSeek 都是 LLM。ChatGPT、Cursor背后的 AI 都是LLM在驱动。

Prompt:提示词,你给AI说的话。「帮我写个登录功能」就是一个Prompt。

Agent:你可以理解为"能自己干活"的AI。传统AI是你问一句它答一句,Agent 是你告诉它一个目标,它自己规划步骤去执行。Cursor 的 Agent 模式就是这个原理。

MCP:Model Context Protocol,模型上下文协议。这是 2024 年出来的一个标准,让 AI 能统一地访问外部工具和数据。比如 AI 可以通过 MCP 直接读取你电脑上的文件、查询你的数据库、控制浏览器。2026年的 Cursor 已经支持 MCP,用起来很方便。

Token:你可以理解为 AI 处理文字的"计量单位"。英文约4个字符=1个 Token,中文约1-2个汉字=1个 Token。

为什么要注意 Token?因为 AI API 是按 Token 收费的。你输入的文字要花钱,AI输出的文字也要花钱。知道这些就够了,继续往下看。


我的AI辅助开发经验

2026年了,AI辅助开发工具已经成为程序员的标配。CursorTraeCopilotOpenCode……不管你用哪个,核心技巧都是互通的。

我用了一年多,从一开始的「这有啥」到现在的「真香」,总结了一些真正有用的经验。

1. 搞清楚什么时候用什么模式

Cursor 有两个核心模式:AgentChat。用对了,效率翻倍;用错了,就是折磨。

Chat模式:你问一句,它答一句。像跟人聊天一样。

我一般用来:

  • 问具体问题:「这个报错是什么意思?」
  • 查知识点:「PostgreSQL的索引类型有哪些?」
  • 解释代码:「这个函数做了什么?」
  • 帮我想名字:「帮我给这个函数起个名字」

Agent模式:你描述一个任务,它自己去分析和改代码。威力更大,但需要把需求说清楚。

我一般用来:

  • 帮我重构整个模块:「把这个登录从JWT改成Session」
  • 帮我修bug:「登录一直返回401,帮我看看是什么原因」
  • 帮我转换代码:「把这个JavaScript文件改成TypeScript」
  • 帮我实现一个功能:「帮我实现用户注册功能,包含表单验证、数据库存储、发送欢迎邮件」

简单说:小问题用Chat,大任务用Agent。

2. 喂上下文是有技巧的

Cursor 最强的地方是它能理解你的整个项目。你打开一个文件,问它这个组件是做什么的,它能根据文件名、代码内容、项目结构给你答案。

但有时候它也会犯傻——给你一些牛头不对马嘴的回答。

这时候,你得学会喂上下文

我犯过的错误:

「怎么优化这个查询?」

AI回了半天,什么加索引、分页、缓存讲了一套,我根本不知道它说的是什么,因为我连我的表结构都没告诉它。

后来我学乖了:

「我的Prisma查询是这样的:prisma const users = await prisma.user.findMany() 数据量大概10万条,现在查询要3秒,请问怎么优化?」

这次AI直接告诉我:1. 加索引 2. 用select只查需要的字段 3. 考虑分页。

我的习惯是:至少告诉AI三件事

  1. 我用的技术栈是什么(Next.js + Prisma + PostgreSQL)
  2. 当前代码长什么样(贴上代码)
  3. 遇到了什么问题(查询慢、报错、不知道怎么做)

3. Tab键补全真的好用

大部分 AI 辅助开发工具都有代码补全功能,会预测你下一行要写什么。按 Tab 键直接采纳预测。

刚开始我还不太信这个功能,觉得 AI 哪有那么聪明。后来真香了。

我经常这样用:

  • 写TypeScript类型定义,AI能猜到我要的类型
  • 写React组件props,AI能帮我补全大部分
  • 写数据库schema,AI知道我想要什么字段
  • 写import语句,AI知道我要导入什么

10次有8次是准的,能省很多打字的时间。

4. 选中代码让AI帮你改(核心技巧)

选中一段代码,让AI帮你修改。这是一个通用技巧,大部分工具都支持,只是快捷键不太一样。

这是我最常用的功能,没有之一。

比如我选中一个函数,这样用:

「请帮我添加错误处理和类型定义」

AI直接在原代码基础上帮我改好了,我只需要确认一下就行。

比让它生成一段新代码然后我再替换,效率高很多。

再举几个我常用的场景:

  • 选中一段面条式代码:「请帮我重构这段代码」
  • 选中一个API接口:「请帮我添加参数校验」
  • 选中一个组件:「请把这个组件改成响应式」

5. 打开对话窗口做复杂任务

有时候你想让AI帮你做比较复杂的任务,比如生成一个完整的组件。

选中代码后打开对话窗口,可以详细描述你的需求。

我经常这样用:

  1. 选中一段代码
  2. 打开对话窗口
  3. 详细描述我要做什么
  4. AI生成代码,我可以逐行确认

这个功能特别适合:

  • 生成一个新组件
  • 实现一个复杂功能
  • 写测试用例

6. @符号引用文件

@符号引用特定内容。

  • @File :引用当前打开的文件
  • @components/UserCard.tsx :直接引用某个文件
  • @Folder :引用整个文件夹
  • @Docs :引用官方文档
  • @Search :搜索项目内的代码

最常用的场景:

@components/UserCard.tsx 请帮我在这个组件里添加一个编辑用户信息的功能

AI直接读取文件内容,在正确的位置帮我添加代码。

@Docs 请帮我查一下Next.js的metadata怎么用来做SEO

AI直接读官方文档,给我准确的答案。

7. 设置好项目规范

我在每个项目都会设置Rules。这是Cursor的一个特色功能,其他工具类似功能还在发展中。

在项目根目录创建.cursor/rules/目录,放.mdc文件:

---
name: 项目规范
description: Next.js 15 App Router 项目规范
---

# 技术栈
- 框架:Next.js 15 App Router
- 语言:TypeScript strict
- 样式:Tailwind CSS
- 数据库:PostgreSQL + Prisma

# 目录结构
- app/:页面
- components/:组件
- lib/:工具函数
- prisma/:数据库schema

# 代码规范
1. 默认使用 Server Components
2. 客户端用 'use client' 标记
3. API错误格式:{ success: boolean, error?: string }

设置好之后,Cursor每次生成代码都会自动遵循这些规范。

举个例子:我不用每次都说「API错误要返回success和error字段」,Cursor自己就知道。

而且Rules是可以复用的。我做了几个模板:

  • Next.js项目规范
  • NestJS项目规范
  • React组件规范

每次建新项目,直接复制过来改一下就行。

8. 用好Skills,让AI更专业

如果说Rules是「项目规范」,那Skills就是「专业能力」。

你可以在.cursor/skills/目录放一些专业技能文件:

# .cursor/skills/database.md

你是一个数据库专家,精通 PostgreSQL 和 Prisma。

在回答数据库相关问题时:
1. 优先考虑查询性能,避免 N+1 问题
2. 合理使用索引,解释为什么加这个索引
3. 更新用 update,删除用 delete,别用 updateMany

回答时先解释原理,再给代码示例。

用的时候告诉AI:「请用数据库专家的角度,帮我审查以下Prisma查询...」

它回答的专业度明显比普通模式高。

我目前积累了几个Skills:

  • database.md:数据库专家
  • security.md:安全专家
  • performance.md:性能优化专家
  • typescript.md:TypeScript专家

9. MCP让AI更强大

前面提到了MCP,这是2026年特别值得关注的特性。

简单说,MCP 让 AI 能从"只懂训练数据"变成"能操作真实世界":

  • MCP + 文件系统:AI 可以直接读取、修改你本地项目的代码
  • MCP + 数据库:AI 可以直接查询你的数据库
  • MCP + 浏览器:AI 可以控制浏览器,帮你填表单、截图
  • MCP + 搜索:AI 可以帮你搜Google、搜文档

Cursor、Trae 等新一代工具已经开始支持 MCP,装好对应的插件就能用。

装好 MCP 插件后,我可以直接问AI:「帮我查询数据库里最近注册的10个用户」

AI真的会去查数据库,然后给我结果。

这个功能还在快速进化中,未来能做的事情会越来越多。

10. 节省Token是有技巧的

前面提到了 Token 的概念。Token 是 AI 处理文字的计量单位,AI API 是按 Token 收费的。

这是我总结的节省 Token 经验:

  1. 别一上来就贴全栈代码:只贴和问题相关的代码片段,AI不需要看你的整个项目才能回答问题。

  2. 问完一个问题可以开新会话:如果新问题和上一个问题不相关,别在同一个会话里继续聊。AI需要记住之前对话的内容,这些也会算Token。

  3. 让AI一次性完成:比如你要写一个组件,别分开问「先帮我写HTML」「再帮我写样式」「再加个交互」。直接说「帮我写一个登录组件,包含表单验证、错误提示、暗色模式支持」,一次搞定。

  4. 精简你的Prompt:Prompt不是越长越好,是越精确越好。把无关的废话去掉,AI能更专注,Token也花得值。

  5. 用@引用代替复制粘贴:用@File引用文件,AI会自己去读,比你复制粘贴一长串代码省Token。


这些场景我天天用AI

1. 读报错信息

以前遇到报错,我要把错误信息复制到Google搜半天。

现在直接问Cursor:「这个报错是什么意思?TypeError: Cannot read properties of undefined (reading 'map')」

它会告诉我:错误原因是什么、最可能出在哪个地方、怎么修复。

80%的情况下,它能帮我省掉搜索的时间。

有时候我甚至直接截图给它看,它也能分析个大概。

2. 代码Review

以前代码Review都是同事做。现在我先让AI Review一遍,发现低级问题,再交给同事。

效率高很多,而且有些话AI说得,我作为开发者反而不好开口。

「请审查以下代码,指出:1. 潜在安全问题 2. 性能问题 3. 代码规范问题」

它会从安全性、性能、代码规范等角度帮我分析一遍。

3. 重构代码

觉得某段代码写得烂,但不知道怎么改?

问AI:「请帮我重构以下代码,要求:1. 使用TypeScript类型 2. 提取可复用逻辑 3. 增加错误处理」

AI会给一个全新的版本,我可以参考它的思路自己改,也能学到东西。

有时候我还会让它用不同的方式重构,让我对比学习。

4. 帮我想名字

我经常让AI帮我给变量、函数起名字。

「我有一个函数,接受用户ID,返回用户名、邮箱、头像、最后登录时间。请帮我想一个合适的函数名」

AI会给三四个建议:

  • getUserById
  • fetchUserDetails
  • getUserProfile

我会选一个最合适的。

比自己想半天强多了。而且AI起的名字通常都比较规范,符合命名习惯。

5. 写测试

写测试很枯燥,但很重要。

我会让AI帮我:

「请为以下函数编写单元测试,覆盖:正常情况、空输入、错误输入」

AI生成测试代码,我再根据需要调整。能省不少时间。

有时候我还会让它帮我补充边界情况的测试。

6. 查文档

以前遇到问题,我先去 Google 搜,然后看 Stack Overflow,最后实在不行才去翻文档。

现在我直接问 Cursor:

`@Docs 请帮我查一下Next.js 15怎么做密码重置」

或者

`@Docs Vercel AI SDK怎么实现流式响应」

AI直接从文档里给我准确的答案,比我自己搜快多了。

7. 帮我写SQL

有时候我需要写一些复杂的SQL查询,直接问AI:

「帮我写一个SQL,查询过去7天每天的新增用户数,按日期排序」

AI会给我SQL,我稍微改改就能用。

8. 帮我理解别人的代码

接手别人的项目,看不懂代码怎么办?

问AI:

`@components/OldCode.tsx 请帮我解释这个组件做了什么」

AI会把代码逻辑梳理一遍,比我自己看快多了。


积累自己的Prompt模板库

这是我想聊的最后一个话题。

有些 Prompt 我会反复使用,慢慢就积累了一套模板:

// 解释代码
请用三句话解释以下代码做了什么

// 解释报错
这个报错是什么意思?{报错信息}

// 生成类型
请为以下接口生成 TypeScript 类型定义

// 代码审查
请审查以下代码,指出:1. 潜在问题 2. 性能优化点 3. 代码规范问题

// 重构
请重构以下代码,要求:{你的要求}

// 写测试
请为以下函数编写测试用例,覆盖:{场景1}、{场景2}、{场景3}

// 查文档
@Docs {你的问题}

我保存在一个markdown文件里,用的时候直接复制粘贴,稍微改改就能用。

我的经验总结

用多了,你会发现有些规律:

  • 1. 模板要简单通用

    我的模板都很简单,就是一个开头。比如「请用三句话解释」,这个模板可以用在任何代码上。

    不要把模板写得太具体,比如「帮我写一个登录表单要包含用户名、密码、验证码」。这样反而不好复用。

  • 2. 遇到好的Prompt就保存下来

    有时候你会发现,同样的问题,不同的问法,AI回答的质量差很多。

    遇到好的Prompt,就把它保存下来。下次遇到类似的问题,直接用或者改改再用。

  • 3. Rules模板可以复用

    Rules模板也是一样的道理。

    我做了几个模板:

    • Next.js项目规范
    • NestJS项目规范
    • React组件规范

    每次建新项目,直接复制过来改一下就能用。做到第三个项目,你会发现很多规范是可以复用的。

  • 4. 定期整理和迭代

    我的模板库每个月会整理一次。把不用的删掉,好的留下来。

    有时候会发现之前写的模板不够好,就改改。

    这是一个持续迭代的过程,不用急。


写在最后

回到开头的问题:会问问题,为什么比会写代码更重要?

因为 AI 时代,写代码的门槛会越来越低。但提问的能力——把模糊的需求翻译成精确的描述——这个能力反而越来越值钱。

你能不能清楚地描述你想要什么?能不能给 AI 足够的上下文?能不能判断 AI 给出的答案对不对?

这些才是 AI 时代真正的核心竞争力。

AI 辅助开发工具会越来越好用,Cursor、Trae、Copilot、OpenCode……不管你用哪个,核心技巧都是互通的。用好工具的人,永远是那些懂得思考的人。

下一篇文章,我们会开始真正的技术内容:《全栈开发环境搭建:Git + monorepo + 开发工具链》。

感兴趣的话,下一篇见。

为什么2026年还要学全栈?

为什么2026年还要学全栈?

系列开篇,写给想要真正做事的人。


原文地址

墨渊书肆/为什么2026年还要学全栈


你有没有过这样的经历?

做了一套很酷的前端界面,发到群里求赞。朋友问:「能线上访问吗?」你愣了一下:「还在本地跑着呢。」

搭建了一个API接口,测试数据跑得好好的。放到线上就开始报错,你对着日志看了半天,不知道是数据库连接问题还是CORS没配好。

买了个云服务器,SSH连上后对着黑屏发呆——接下来该干什么?域名怎么绑定?HTTPS怎么配置?

如果你有过类似的经历,说明你和我一样,曾经被困在某个技术边界里。

前端会一点,后端也懂一点,但真的要把一个想法变成线上能用的东西,总是差了那么一口气。

我想聊聊这件事。


全栈这件事,被误解了很多年

一提到「全栈工程师」,很多人脑海里浮现的是这样一个形象:什么框架都会,什么语言都能写,数据库也能碰,服务器也能捣鼓。

换句话说,「什么都会一点」。

这种理解,在五年前或许还能成立。那时候做Web开发,确实需要前后端都懂一点才能混得下去。

但2026年了,这种理解该过时了。

真正的全栈,不是「什么都会一点」,而是「能独立交付一个完整的、可运行的互联网产品」。

这两个定义有本质区别。

「什么都会一点」说的是技术广度,你掌握了ABCDE各种技术。 「能交付完整产品」说的是能力深度,你能够从0到1,把想法变成现实。

前者是堆砌,后者是整合。

这十年,全栈经历了什么

让我简单回顾一下这段历史,你可能会更有感触。

  • 2010-2015年:全栈的黄金时代

那时候,一个创业者想要做个网站,真的需要一个人搞定所有事情。PHP就是最典型的全栈语言——一个文件,从数据库到HTML全写了。

没有选择,只能全栈。

  • 2015-2020年:前后端分离,全栈「衰落」

前端技术越来越复杂,React、Vue、Angular各自一套生态。后端技术也在深化,微服务、容器化、云原生,一个领域比一个领域深。

很多人开始专注于一个方向。全栈这个词渐渐变成了「什么都会一点,什么都不精」的代名词。

我见过很多前端工程师,后端代码一行都不敢改。也见过很多后端工程师,写个表单样式就头皮发麻。

技术栈在变宽,人在变窄。

  • 2020年至今:AI时代,全栈复兴

转折来自两个力量:

一是Serverless和全栈框架的成熟。Next.jsSupabase让一个人能覆盖的场景越来越广。

二是AI的爆发。代码可以自动生成了,一个人能做的事情边界再次扩大。

但这次不一样。

这次的全栈,不是回到过去那种「什么都会一点」的状态,而是有了AI的辅助,你可以更专注于「整合」而非「实现」

你不需要记住每个API的用法,AI可以帮你查。但你需要知道一个系统需要哪些模块、它们怎么配合。

这才是2026年「全栈」的真正含义。


我见过太多「会技术」但「做不出东西」的人

我自己也是这么走过来的。

刚学编程的那几年,我痴迷于学新东西。React出来了,学React。Vue火了,学Vue。Node.js流行,学Node。Docker热门,学Docker。

感觉自己越来越厉害,简历上技术栈越来越长。

但有一次,我做一个个人博客系统,前前后后做了俩个月。

不是技术难,而是我在每个环节都卡住:

  • 前端写到一半,发现后端API设计不合理,推倒重来
  • 数据库表结构改了三版,每次都要改前端对应的字段
  • 好不容易做完了,部署上线又折腾了一周
  • 刚上线就被别人注册了一堆垃圾数据,才发现自己没做接口限流

一个看似简单的博客系统,真正从零做到上线,才发现之前学的那些技术都是散的,根本连不起来。

后来我反思:不是我技术不够,而是我从来没有站在「完整产品」的角度去规划一个系统。

这就是问题所在。

但现在,在春节前,我使用 AI 辅助开发和腾讯云的轻量服务器,3天就成功上线了我的个人博客站。

————墨渊书肆

后面,也会根据这个博客站,和我在开发的另一个出海产品,分享我的实战经验。


全栈到底学什么?

说了这么多,你可能想问:所以全栈到底要学什么?

我的回答是:不是学更多技术,而是理解技术之间的关系。

举两个例子。

第一个例子。

你想实现「用户登录后可以评论」这个功能。你需要懂:

  • 前端表单验证
  • 后端接口设计
  • 数据库表结构
  • 密码怎么加密存储
  • Token怎么验证
  • HTTPS怎么配
  • Rate Limiting怎么加

每一项单拎出来都不难。但如果你不懂它们之间的关系,就会出现:前端验证了后端没验证、密码存明文了、Token没过期时间、接口被人刷爆等各种问题。

第二个例子。

你做一个博客系统。要发文章、要看文章、要评论、要搜索、要做SEO、要做推荐。

每个功能你都能找到对应的技术方案。但关键问题是:

  • 先做哪个后做哪个?
  • 数据库表之间怎么关联?
  • 哪些数据要缓存哪些不用?
  • 搜索要做全文检索还是简单like查询?

这些问题没有标准答案,需要你根据实际需求去权衡去决策。

全栈的核心能力,就是理解这些技术怎么配合,然后做出合理的决策。


2026年的全栈技术图谱

既然说到全栈,我把一个现代 Web 应用涉及到的技术领域整理一下。不用全部记住,但需要知道大概有哪些东西,以及每个部分是干嘛的。


前端部分 —— 用户能看到的一切

前端就是用户打开浏览器能看到的所有东西。按钮能不能点、页面好不好看、表单能不能提交,这些都归前端管。

框架:用来构建用户界面。React是现在最主流的选择,Vue在国内用得也比较多,Next.js比较特殊,它既是前端框架,又自带后端能力,属于「全栈框架」。

样式:让界面好看。Tailwind CSS是现在的主流,因为它不用写单独的CSS文件,直接在HTML里写样式,很方便。

状态管理:管理页面数据。比如用户登录了,他的信息存在哪里?购物车有几件商品?这些数据的变化需要统一管理,Zustandmobx是轻量级的选择,Redux功能更全但也 更重。

UI组件:现成的界面零件。shadcn/ui现在特别火,它不是传统意义上的组件库,而是提供代码让你自己修改,这样你可以完全控制样式。


后端部分 —— 用户看不到但每天在用的

后端是服务器上运行的代码,你看不见它,但它在默默处理各种请求。用户登录、提交订单、查询数据——这些都需要后端来处理。

运行时:JS 可以在服务器上运行了,这就是Node.js,目前最成熟。Bun更快,Deno更现代(Node.js的原作者重新写的)。

框架:写后端代码的工具。Next.js API Routes是前后端一起写的方式,适合小项目。Hono非常轻量,而且天然支持 Edge 部署(边缘计算,后面会讲)。NestJS是企业级的,结构更严谨,适合大项目。

数据库:存数据的地方。PostgreSQL是目前最强悍的关系型数据库,MySQL是老牌稳定选手。简单理解:重要数据放数据库。

ORM:数据库和代码之间的翻译官。Prisma用起来很舒服,Drizzle更快且更轻, typeORM 功能更全。它们让你用 JS 的语法去操作数据库,不用写原始SQL。


基础设施 —— 让你的应用能跑起来

这部分是很多前端开发者最头疼的——代码写完了,怎么让它能被所有人访问?这就是基础设施要解决的问题。

服务器:一台24小时开机的电脑。国内的阿里云腾讯云,国外的VercelNetlify,都是提供服务器的服务商。

容器:把应用和它依赖的所有东西打包,这样在任何环境下都能跑。Docker是标配,Docker Compose用来在本机编排多个服务。

CDN:让用户访问更快。CDN就是一堆分布在世界各地的服务器,用户访问时从最近的服务器拿资源,速度会快很多。国际首选Cloudflare,国内用阿里云CDN

域名和SSL:域名是网站的地址,SSL是让访问变成https://的那个加密协议。Let's Encrypt提供免费SSL,Cloudflare可以自动帮你处理HTTPS。


运维监控 —— 保障服务稳定运行

应用上线了,怎么知道用户访问快不快?出错了怎么知道?这些就是运维监控要做的。

日志:记录系统发生了什么。ELK(Elasticsearch + Logstash + Kibana)是经典方案,Loki更轻量。现在很多云服务也自带日志功能。

监控:看系统健康不健康。Sentry专门追踪错误,谁的代码出错了第一时间知道。Prometheus + Grafana是看指标的,比如服务器CPU用了多少、数据库响应多快。

CI/CD:自动化部署。代码提交后自动测试、自动部署到服务器。GitHub Actions最常用,国内有阿里云效腾讯云CODING


安全 —— 保护你的应用

不做安全防护的应用,就像没装门的房子,谁都能进来。

前端安全:XSS是别人在你的页面里注入恶意脚本,CSRF是别人伪造你的身份发请求,CSP是限制页面能加载哪些资源。

后端安全:SQL注入是通过输入框往数据库里塞恶意代码,参数校验是确保用户传的数据是你期望的,Rate Limiting是限制一个人1分钟内只能发10次请求,防止被刷。

数据安全:HTTPS加密传输是最基本的,敏感数据(比如密码)要加密存储,密钥不要写在代码里。


AI能力 —— 新时代的必备技能

2026年了,如果你说自己是全栈但不懂 AI 用法,就像做前端不会用Git一样说不过去。

集成框架Vercel AI SDK是最流行的AI功能集成框架,支持流式响应(就是 ChatGPT 那种一个字一个字蹦出来的效果),对接各种模型很方便。

模型提供商:国外用OpenAI(GPT)、Anthropic(Claude),国内用硅基流动DeepSeek。国内外使用体验和成本差异很大,后面实战会分别讲。

向量数据库:AI场景专用。传统数据库存文字,向量数据库存「意思」。比如你搜「苹果」,它不仅能匹配到「苹果」,还能匹配到「iPhone」、「水果」,因为它理解「苹果」的含义。PineconeMilvus是代表。


这就是现代全栈的完整图谱。你不需要每样都精通,但需要知道它们各自负责什么,以及什么时候该用什么。


AI时代,全栈反而更重要了

我知道你可能会有疑问:现在AI这么强,Cursor敲几下代码就出来了,我还需要学全栈吗?

我的答案是:恰恰相反。

AI可以帮你写一个登录API,但它不知道:

  • 你的产品需不需要短信验证码登录
  • 你的用户数据存储在哪里
  • 你要不要支持微信登录
  • 登录失败几次要锁号
  • Token过期时间设多长

AI可以帮你写一个数据库查询,但它不知道:

  • 你的数据量级需要什么索引
  • 哪些查询需要加缓存
  • 读写分离怎么做

AI可以帮你部署上线,但它不知道:

  • 选择Vercel还是阿里云
  • 国内用户访问慢怎么办
  • 怎么做成本优化

AI擅长的是「点」,你需要的是「面」。

你告诉AI「帮我写个用户登录」,它会给你写一个标准答案。但具体怎么设计,这是你需要决策的事情。

而且,只有当你真正理解一个系统是怎么运转的,你才能:

  • 准确描述你想要什么(而不是永远在改需求)
  • 发现AI写的代码哪里有问题(而不是全盘接受)
  • 把不同模块组合在一起(而不是拼都拼不起来)

这才是整合能力的价值。

AI不是取代你,而是放大你。你原本只能做前端,AI帮你写了后端,你就能做全栈。但前提是,你本来就具备全栈思维,知道一个完整的产品需要什么。


怎么学?T型发展

说了这么多,到底怎么学?我的建议是「T型发展」:

先广度,后深度。

首先,对全栈技术有个整体认知。前端、后端、数据库、运维、安全……每个领域都了解一下,知道它们各自负责什么、解决什么问题。

这个阶段不需要深入,掌握概念就够了。

然后,选择一个方向深挖。

如果你对前端感兴趣,就深入React/Next.js。如果你对后端感兴趣,就深入Node.js/PostgreSQL。深入到能独立完成一个完整项目的程度。

最后,按需补充。

在实际项目中遇到什么问题,就去学什么。需要做支付,就去学Stripe。需要做搜索,就去学Elasticsearch。需要做 AI 功能,就去学Vercel AI SDK

这种「实战驱动」的学习方式,效率最高。


这个系列想带你做什么

市面上不缺技术教程。React入门、Node.js实战、Docker部署——这种内容一搜一大把。

但我发现很多人学完这些教程,还是做不出东西。

因为技术是散的,需要一条线把它们串起来。

这个系列我想带你做的事情很简单:从零开始,构建一个真正能上线的产品。

不是demo,不是练习,而是真实的、可访问的、能在生产环境跑的系统。

我会分成这几个阶段:

  • 第一阶段:认知重建

先理解全栈到底要学什么,怎么学(就是这篇)。

  • 第二阶段:基础设施

服务器、域名、CDN、Docker、日志、监控——那些「不太技术」但非常重要的东西。

  • 第三阶段:前端开发

React、Next.js、TypeScript、UI体系。

  • 第四阶段:后端开发

API设计、数据库、认证、缓存。

  • 第五阶段:AI集成

Vercel AI SDK、流式响应。

  • 第六阶段:部署上线

国内(阿里云)和国外(Vercel)两套方案。

  • 第七阶段:安全与性能

生产环境必须注意的那些事。

  • 第八阶段:实战

两个完整项目,从0到上线的全过程。

在这个过程中,你会看到我踩过的坑、做过的错误决策、总结出的经验。我不是为了告诉你「这个技术怎么用」,而是告诉你「这个系统该怎么搭」。


写在最后

回到开头的问题。

你是不是经常感觉学了很多技术,但真正要用的时候还是不知道从哪里开始?

这很正常。

技术本身不是目的,产品才是。

2026年了,AI 可以帮你写代码,但不能帮你交付产品。能做到这一点的人,永远有市场。

而这,就是我们这个系列要一起做的事情。

下一篇文章,我会讲讲AI辅助开发这件事——怎么用好CursorTraeOpenCode,以及一个更重要的道理:会问问题比会写代码更重要。

感兴趣的话,下一篇见。

❌