普通视图

发现新文章,点击刷新页面。
昨天 — 2026年5月23日技术

React Hook采用环形链表的原因

作者 卷帘依旧
2026年5月22日 15:02

React Hooks 更新采用环形链表的原因

React Hooks 的更新队列采用环形链表结构,这是一个精心设计的决策。让我从源码层面解释为什么。

1. 环形链表的核心优势

优势一:O(1) 时间的合并操作

// 环形链表结构
type UpdateQueue<T> = {
  pending: Update<T> | null,  // 指向最后一个更新
}

type Update<T> = {
  action: T | ((T) => T),
  next: Update<T> | null,
}

// 添加新更新 - O(1) 时间复杂度
function appendUpdate(queue, update) {
  const pending = queue.pending
  
  if (pending === null) {
    // 第一个更新,指向自己形成环
    update.next = update
  } else {
    // 插入到环形链表的头部
    // pending 指向最后一个节点
    // pending.next 指向第一个节点
    update.next = pending.next  // 新节点的 next 指向第一个节点
    pending.next = update       // 最后一个节点的 next 指向新节点
  }
  queue.pending = update  // 更新 pending 指向新节点(新的最后一个)
}

// 如果是单向链表(非环形)
function appendUpdateLinear(head, update) {
  // 需要遍历到末尾才能添加 - O(n) 时间复杂度
  if (head === null) {
    return update
  }
  let current = head
  while (current.next !== null) {  // 遍历!
    current = current.next
  }
  current.next = update
  return head
}

实际性能对比

// React 中频繁的批量更新场景
function handleClick() {
  // 同一个状态连续更新多次
  setCount(1)
  setCount(2)
  setCount(3)
  setCount(4)
  setCount(5)
}

// 环形链表:每次 O(1),5次操作 = 5个单位时间
// 单向链表:第1次 O(1),第2次 O(2),第3次 O(3)... 总计 O(n²)

优势二:高效的双向遍历能力

// React 处理更新时的遍历
function processUpdateQueue(queue) {
  const pending = queue.pending
  
  if (pending !== null) {
    // 关键:通过 pending.next 获取第一个更新
    const first = pending.next  // O(1) 获取头部
    let newState = currentState
    
    // 正向遍历所有更新
    let update = first
    do {
      newState = applyUpdate(newState, update.action)
      update = update.next
    } while (update !== first)  // 回到起点,遍历完成
    
    // 如果需要反向遍历(比如优先级调度)
    // 也可以轻松实现
    let last = pending
    let prev = first
    while (prev.next !== last) {
      // 反向遍历逻辑
    }
  }
}

2. 解决并发渲染中的问题

问题场景:高优先级更新打断

// 环形链表在并发渲染中的优势
function concurrentUpdateExample() {
  const [count, setCount] = useState(0)
  
  // 场景:用户快速点击,产生多个更新
  setCount(1)  // 低优先级更新 A
  setCount(2)  // 高优先级更新 B(打断 A)
  setCount(3)  // 低优先级更新 C
  
  // 环形链表的处理方式:
  // pending → [C] → [B] → [A] → (回到 [C])
  //            ↑_______________|
  // 
  // 渲染时可以从任意节点开始,灵活调整优先级顺序
}

React 的实际实现

// React 源码中的环形链表实现(简化)
function dispatchSetState(fiber, queue, action) {
  const update = {
    action,
    next: null,
    priority: getCurrentPriorityLevel(),
  }
  
  // 获取当前待处理的更新环
  const pending = queue.pending
  
  if (pending === null) {
    // 第一个更新,形成环
    update.next = update
  } else {
    // 插入到环中
    update.next = pending.next
    pending.next = update
  }
  
  queue.pending = update
  
  // 并发渲染时可以安全地 fork 更新队列
  if (fiber.lanes !== NoLanes) {
    // 如果正在进行渲染,创建 interleaved 队列
    const interleaved = queue.interleaved
    if (interleaved === null) {
      queue.interleaved = update
    } else {
      update.next = interleaved.next
      interleaved.next = update
    }
    queue.interleaved = update
  }
  
  scheduleUpdateOnFiber(fiber)
}

// 处理并发更新时的队列合并
function mergeQueues(baseQueue, interleavedQueue) {
  if (baseQueue === null) {
    return interleavedQueue
  }
  if (interleavedQueue === null) {
    return baseQueue
  }
  
  // 环形链表的合并:O(1) 完成
  // baseQueue: ... → last1 → first1 → ...
  // interleavedQueue: ... → last2 → first2 → ...
  
  const first1 = baseQueue.next
  const last1 = baseQueue
  const first2 = interleavedQueue.next
  const last2 = interleavedQueue
  
  // 将两个环连接成一个环
  last1.next = first2
  last2.next = first1
  
  return interleavedQueue  // 返回新的尾部
}

3. 批量更新与状态计算

批量更新机制

// React 18 的自动批处理
function batchUpdate() {
  // 所有更新被收集到环形链表
  setCount(1)  // update1
  setCount(2)  // update2
  setCount(3)  // update3
  setName('John')  // 另一个 Hook 的更新
  
  // 环形链表结构:
  // pending → update3 → update2 → update1 → (回到 update3)
  //            ↑____________________|
  
  // 一次渲染处理所有更新
  // 遍历环形链表只需 O(n) 时间
}

// 处理环形链表的代码
function processUpdateQueue(workInProgress, queue) {
  let pending = queue.pending
  
  if (pending !== null) {
    // 关键:解除环形,变成单向链表方便处理
    const first = pending.next
    let last = pending
    let newState = currentState
    
    // 断开环形
    last.next = null
    
    // 现在变成了单向链表,可以安全遍历
    let update = first
    while (update !== null) {
      newState = applyUpdate(newState, update.action)
      update = update.next
    }
    
    return newState
  }
}

4. 与单向链表的对比

// 性能对比测试
function benchmark() {
  const updates = Array(1000).fill().map((_, i) => ({ action: i }))
  
  // 环形链表插入
  console.time('Circular Linked List')
  let circularQueue = null
  for (let update of updates) {
    if (circularQueue === null) {
      update.next = update
      circularQueue = update
    } else {
      update.next = circularQueue.next
      circularQueue.next = update
      circularQueue = update
    }
  }
  console.timeEnd('Circular Linked List')  // ~0.1ms
  
  // 单向链表插入
  console.time('Singly Linked List')
  let linearHead = null
  let linearTail = null
  for (let update of updates) {
    if (linearHead === null) {
      linearHead = update
      linearTail = update
    } else {
      linearTail.next = update
      linearTail = update
    }
  }
  console.timeEnd('Singly Linked List')  // ~0.15ms(略慢)
  
  // 但环形链表在特定操作上优势明显
  // 比如:获取第一个和最后一个元素都是 O(1)
}

5. 实际应用场景

场景一:优先级提升

// React 中的优先级提升机制
function promoteUpdatePriority(queue, targetPriority) {
  const pending = queue.pending
  if (pending === null) return
  
  // 环形链表可以轻松调整顺序
  let update = pending.next
  let highestPriorityUpdate = null
  
  do {
    if (update.priority > targetPriority) {
      // 找到高优先级更新,提升它
      highestPriorityUpdate = update
      break
    }
    update = update.next
  } while (update !== pending.next)
  
  if (highestPriorityUpdate) {
    // 将高优先级更新移到环的头部
    // 这样渲染时会优先处理
    queue.pending = highestPriorityUpdate
  }
}

场景二:状态回滚

// 时间切片中的状态回滚
function rollbackUpdates(queue, rollbackPoint) {
  const pending = queue.pending
  if (pending === null) return
  
  // 找到回滚点
  let update = pending.next
  let found = false
  
  do {
    if (update === rollbackPoint) {
      found = true
      break
    }
    update = update.next
  } while (update !== pending.next)
  
  if (found) {
    // 截断环形链表,丢弃回滚点之后的更新
    queue.pending = rollbackPoint
    rollbackPoint.next = rollbackPoint  // 重新形成环
  }
}

6. 内存和 GC 优势

// 环形链表的垃圾回收优势
function cleanupQueue(queue) {
  // 断开环形引用,帮助 GC
  const pending = queue.pending
  
  if (pending !== null) {
    // 打破循环引用
    const first = pending.next
    pending.next = null  // 断开环
    
    // 现在可以安全地清理
    let update = first
    while (update !== null) {
      const next = update.next
      update.next = null  // 帮助 GC
      update = next
    }
  }
  
  queue.pending = null
}

// 单向链表需要更多遍历才能完全清理

总结

React Hooks 采用环形链表的核心原因:

  1. 性能优化:O(1) 的头部和尾部访问,O(1) 的合并操作
  2. 并发安全:便于 fork 和合并更新队列,支持优先级调度
  3. 灵活性:可以从任意节点开始遍历,方便实现各种调度策略
  4. 内存效率:无需维护额外的头尾指针,单个指针就能定位整个队列
  5. 批量更新:天然支持环形遍历,适合处理批量状态更新

这种设计是 React 团队在性能和功能之间做出的最优权衡,既满足了并发渲染的需求,又保持了良好的性能特性。

为什么React Hooks不能用在if/for等条件/循环语句中

作者 卷帘依旧
2026年5月22日 14:52

在 React 中,Hooks 不能直接用在 iffor 或其他条件/循环语句中

原因

React 要求 Hooks 必须在组件的顶层被调用,不能在条件语句、循环或嵌套函数中使用。这背后有两个关键原因:

1. 保证 Hooks 的调用顺序一致

React 内部通过调用顺序来追踪每个 Hook 对应的 state。例如:

// 第一次渲染
useState('name')  // 第1个Hook → name state
useState(0)       // 第2个Hook → age state
useEffect(() => {}) // 第3个Hook → effect

// 第二次渲染时,React 按顺序分配:
// 第1个调用 → name state
// 第2个调用 → age state  
// 第3个调用 → effect

如果在 if 中使用 Hook:

// ❌ 错误示例
if (someCondition) {
  useState('name')  // 条件为 false 时,这个 Hook 不执行
}
useState(0)         // 原本的第2个 Hook 变成了第1个

这会导致 Hook 顺序错乱,React 无法正确匹配 state。

2. 避免状态混乱

// ❌ 错误示例
function MyComponent({ id }) {
  if (id) {
    useEffect(() => {
      fetchData(id)
    }, [id])  // 危险!Hook 在条件中
  }
  
  const [count, setCount] = useState(0)  // 顺序可能改变
  // ...
}

解决方案

在条件中使用 Hooks 的正确方式

// ✅ 将条件写在 Hook 内部
function MyComponent({ id }) {
  useEffect(() => {
    if (id) {
      fetchData(id)
    }
  }, [id])
  
  // 或者在条件中使用 Hook 的返回值
  const [count, setCount] = useState(0)
  
  if (count > 5) {
    return <div>超过限制</div>  // 可以在条件中提前 return
  }
  
  return <div>{count}</div>
}

在循环中使用

// ✅ 不能这样,但可以:
// 错误:在循环中调用 Hook
for (let i = 0; i < 5; i++) {
  useState(i)  // ❌ 错误
}

// 正确:将 Hook 的结果用于循环
const [items, setItems] = useState([])  // ✅ Hook 在顶层
const renderedItems = items.map(item => <Item key={item.id} />)

总结

  • 可以做的:在 Hooks 内部写条件判断
  • 不能做的:在条件语句或循环中调用 Hooks

这个规则是 React 的设计原则,目的是保持 Hooks 调用顺序的一致性,确保状态正确管理。


React Hooks 的完整管理方式

从源码层面详细讲解 React 如何管理 Hooks,包括创建、更新、销毁的完整流程。

1. 三层数据结构

React 使用三层结构管理 Hooks:

// 第一层:Fiber 节点(每个组件实例)
type Fiber = {
  memoizedState: Hook | null,        // 指向第一个 Hook
  updateQueue: UpdateQueue | null,    // 更新队列
  stateNode: any,                     // 真实 DOM 节点
  alternate: Fiber | null,            // 双缓冲副本
  // ... 其他 Fiber 属性
}

// 第二层:Hook 节点(链表结构)
type Hook = {
  memoizedState: any,          // 当前 state/effect
  baseState: any,              // 基础 state(用于计算)
  baseQueue: Update | null,    // 基础更新队列
  queue: UpdateQueue | null,    // 更新队列
  next: Hook | null,           // 指向下一个 Hook
}

// 第三层:更新队列(环形链表)
type UpdateQueue<S, A> = {
  pending: Update<A> | null,   // 待处理的更新(环形链表)
  interleaved: Update<A> | null,
  lanes: Lanes,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
}

type Update<A> = {
  action: A,                   // setState 传入的值
  next: Update<A> | null,      // 下一个更新
  priority: number,            // 优先级
  // ...
}

2. Hooks 的完整生命周期

阶段一:挂载阶段(Mount)

// React 内部实现(简化版)
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  }

  if (workInProgressHook === null) {
    // 第一个 Hook
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    // 后续 Hook,添加到链表末尾
    workInProgressHook = workInProgressHook.next = hook
  }
  
  return workInProgressHook
}

// mountState 实现
function mountState(initialState) {
  // 1. 创建 Hook 节点
  const hook = mountWorkInProgressHook()
  
  // 2. 初始化 state
  if (typeof initialState === 'function') {
    hook.memoizedState = initialState()
  } else {
    hook.memoizedState = initialState
  }
  
  // 3. 创建更新队列
  const queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  }
  hook.queue = queue
  
  // 4. 创建 dispatch 函数
  const dispatch = (queue.dispatch = dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ))
  
  return [hook.memoizedState, dispatch]
}

阶段二:更新阶段(Update)

// updateWorkInProgressHook - 复用旧 Hook
function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: Hook | null
  
  if (currentHook === null) {
    // 获取 Fiber 对应的旧 Hook 链表的头节点
    const current = currentlyRenderingFiber.alternate
    if (current !== null) {
      nextCurrentHook = current.memoizedState
    } else {
      nextCurrentHook = null
    }
  } else {
    // 获取下一个旧 Hook
    nextCurrentHook = currentHook.next
  }
  
  // 关键检测:Hook 数量不一致
  if (nextCurrentHook === null) {
    throw new Error(
      'Rendered more hooks than during the previous render.'
    )
  }
  
  currentHook = nextCurrentHook
  
  // 创建新的 Hook 节点,复用旧数据
  const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,
    next: null,
  }
  
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook
  } else {
    workInProgressHook = workInProgressHook.next = newHook
  }
  
  return workInProgressHook
}

// updateState 实现
function updateState(initialState) {
  const hook = updateWorkInProgressHook()
  const queue = hook.queue
  const pending = queue.pending
  
  if (pending !== null) {
    // 处理所有待处理的更新
    let firstUpdate = pending.next
    let newState = hook.memoizedState
    
    // 遍历环形链表,应用所有更新
    let update = firstUpdate
    do {
      const action = update.action
      newState = action(newState)  // 或者直接赋值
      update = update.next
    } while (update !== null && update !== firstUpdate)
    
    hook.memoizedState = newState
    queue.pending = null
  }
  
  return [hook.memoizedState, queue.dispatch]
}

3. 双缓冲机制(Fiber 双缓冲)

React 使用双缓冲技术管理 Hooks 状态:

// 双缓冲工作原理
function renderWithHooks(current, workInProgress, Component, props) {
  // current: 当前屏幕显示的 Fiber 树(旧)
  // workInProgress: 正在构建的 Fiber 树(新)
  
  currentlyRenderingFiber = workInProgress
  
  // 重置 Hooks 状态
  workInProgressHook = null
  currentHook = null
  
  // 将 current 树的 Hooks 复制到 workInProgress
  if (current !== null) {
    currentHook = current.memoizedState  // 从旧树获取 Hooks
  }
  
  // 执行组件,内部会调用各种 Hooks
  let children = Component(props)
  
  // 渲染完成后,workInProgress 树成为新的 current
  finishRendering()
  
  return children
}

4. 状态更新的调度流程

// dispatchAction - setState 的实现
function dispatchAction(fiber, queue, action) {
  // 1. 创建更新对象
  const update = {
    action,
    next: null,
    priority: getCurrentPriorityLevel(),
  }
  
  // 2. 将更新添加到环形链表
  const pending = queue.pending
  if (pending === null) {
    // 第一个更新,指向自己形成环形
    update.next = update
  } else {
    update.next = pending.next
    pending.next = update
  }
  queue.pending = update
  
  // 3. 调度更新
  scheduleUpdateOnFiber(fiber)
}

// 处理更新队列
function processUpdateQueue(workInProgress) {
  let queue = workInProgress.updateQueue
  let pending = queue.pending
  
  if (pending !== null) {
    // 移除环形链表的循环引用
    const first = pending.next
    let last = pending
    let newState = workInProgress.memoizedState
    
    // 处理所有更新
    let update = first
    do {
      const action = update.action
      if (typeof action === 'function') {
        newState = action(newState)
      } else {
        newState = action
      }
      update = update.next
    } while (update !== null && update !== first)
    
    workInProgress.memoizedState = newState
    queue.pending = null  // 清空队列
  }
}

5. 不同类型 Hooks 的管理

useState 管理

function useState(initialState) {
  const dispatcher = resolveDispatcher()
  return dispatcher.useState(initialState)
}

// 不同阶段的 dispatcher
const HooksDispatcherOnMount = {
  useState: mountState,
  useEffect: mountEffect,
  useContext: readContext,
  // ...
}

const HooksDispatcherOnUpdate = {
  useState: updateState,
  useEffect: updateEffect,
  useContext: readContext,
  // ...
}

useEffect 管理

function mountEffect(create, deps) {
  const hook = mountWorkInProgressHook()
  const nextDeps = deps === undefined ? null : deps
  
  // Effect 有特殊的存储结构
  hook.memoizedState = {
    create,     // effect 函数
    destroy: null,  // cleanup 函数
    deps: nextDeps,
  }
  
  // 将 effect 添加到 fiber 的 updateQueue 中
  pushEffect(
    HookPassive | HookHasEffect,
    create,
    undefined,
    nextDeps,
  )
  
  return
}

function updateEffect(create, deps) {
  const hook = updateWorkInProgressHook()
  const nextDeps = deps === undefined ? null : deps
  const prevState = hook.memoizedState
  
  // 比较依赖是否变化
  if (prevState !== null && nextDeps !== null) {
    const prevDeps = prevState.deps
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      // 依赖没变,跳过执行
      pushEffect(HookPassive, create, undefined, nextDeps)
      return
    }
  }
  
  // 依赖变化,需要执行
  hook.memoizedState = {
    create,
    destroy: prevState?.destroy,
    deps: nextDeps,
  }
  pushEffect(HookPassive | HookHasEffect, create, undefined, nextDeps)
}

6. 完整的执行示例

// 示例组件
function App() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('John')
  
  useEffect(() => {
    console.log('Count changed:', count)
  }, [count])
  
  const handleClick = () => {
    setCount(count + 1)
    setCount(c => c + 1)  // 函数式更新
  }
  
  return <button onClick={handleClick}>{count}</button>
}

// 第一次渲染(Mount):
// 1. useState(0) → 创建 Hook1,memoizedState = 0
// 2. useState('John') → 创建 Hook2,memoizedState = 'John'  
// 3. useEffect → 创建 Hook3,添加到 effect 队列
// 链表结构:Hook1 → Hook2 → Hook3

// 点击按钮后:
// 1. setCount(1) 创建 update1
// 2. setCount(c => c + 1) 创建 update2
// 更新队列(环形链表):update1 → update2 → update1

// 第二次渲染(Update):
// 1. useState(0) → 复用 Hook1,应用两个更新,memoizedState = 2
// 2. useState('John') → 复用 Hook2,memoizedState = 'John'
// 3. useEffect → 比较依赖,count 变化,执行 effect

7. 内存管理与清理

// Fiber 删除时的 Hook 清理
function safelyCallDestroy(current, nearestMountedAncestor, destroy) {
  try {
    destroy()  // 执行 useEffect 返回的 cleanup 函数
  } catch (error) {
    // 错误处理
  }
}

// 组件卸载时清理所有 Hooks
function commitUnmount(fiber) {
  // 遍历 Hook 链表,执行所有 cleanup
  let nextEffect = fiber.memoizedState
  while (nextEffect !== null) {
    const effect = nextEffect.memoizedState
    if (effect !== null && effect.destroy !== undefined) {
      safelyCallDestroy(fiber, null, effect.destroy)
    }
    nextEffect = nextEffect.next
  }
}

总结

React Hooks 的管理方式核心特点:

  1. 链表结构:使用单向链表存储 Hook 节点,依赖固定的调用顺序
  2. 双缓冲:通过 Fiber 双缓冲机制支持并发渲染
  3. 环形更新队列:高效处理状态更新的批量操作
  4. 分阶段调度:区分 mount 和 update 阶段,使用不同的 dispatcher
  5. 优先级机制:支持高优先级更新打断低优先级更新

这种设计使 React 能够在保证性能的同时,提供简单易用的 Hooks API。

昨天以前技术

Vue2中defineProperty缺陷

作者 卷帘依旧
2026年5月19日 16:21

vue2监听不到数组修改索引和长度的变化,以及监听不到Map、Set的变化!Vue 2 监听不到这些变化的根本原因在于 Object.defineProperty 的设计目标和 JavaScript 引擎的限制

核心原因分析

1. 为什么监听不到数组索引修改?

// Object.defineProperty 的设计目标是为对象属性服务的
const arr = [1, 2, 3]

// 理论上,数组索引也是属性名
Object.getOwnPropertyDescriptor(arr, '0')
// { value: 1, writable: true, enumerable: true, configurable: true }

// 但问题在于:
// 1. 性能考虑:为每个索引都添加 getter/setter 成本太高
// 2. 数组长度可能动态变化,无法预先为所有索引添加监听
// 3. JavaScript 引擎对数组有特殊优化(快数组/慢数组)

具体技术原因:

// Vue 2 的简化实现
function observeArray(arr) {
  // 理论上可以这样做,但 Vue 没有采用
  for(let i = 0; i < arr.length; i++) {
    defineReactive(arr, i, arr[i])  // 为每个索引添加 getter/setter
  }
  // 问题:
  // 1. 性能极差(大数组遍历很慢)
  // 2. 新增索引无法监听
  // 3. 破坏了 JavaScript 引擎对数组的优化
}

2. 数组元素的特殊处理机制

// Vue 2 的实际做法:重写数组变异方法
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push', 'pop', 'shift', 'unshift', 'splice',
  'sort', 'reverse'
]

methodsToPatch.forEach(method => {
  arrayMethods[method] = function(...args) {
    // 执行原始方法
    const result = arrayProto[method].apply(this, args)
    
    // 获取新增的元素
    let inserted
    switch(method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    
    // 对新元素进行响应式处理
    if(inserted) this.__ob__.observeArray(inserted)
    
    // 触发更新
    this.__ob__.dep.notify()
    
    return result
  }
})

// 但直接索引修改无法被拦截
arr[0] = 'new value'  // 这个方法没有被重写,所以监听不到

3. 为什么监听不到 length 变化?

// length 属性比较特殊
const arr = [1, 2, 3]

// length 属性的描述符
Object.getOwnPropertyDescriptor(arr, 'length')
// {
//   value: 3,
//   writable: true,
//   enumerable: false,  // 不可枚举
//   configurable: false  // 不可配置,不能重新定义
// }

// Vue 无法重新定义 length 属性
try {
  Object.defineProperty(arr, 'length', {
    get() { return this._length },
    set(val) { this._length = val }
  })
} catch(e) {
  console.log('Cannot redefine length property')
  // TypeError: Cannot redefine property: length
}

// 所以 arr.length = 2 无法被 Vue 拦截

监听不到 Map/Set 的根本原因

1. Map/Set 不是普通对象

// Map 和 Set 是 ES6 新增的集合类型
const map = new Map()
const set = new Set()

// 它们的内部存储机制不同
// Map 使用 [[MapData]] 内部槽存储数据,不是对象属性
console.log(Object.keys(map))  // [],没有可枚举属性

// Object.defineProperty 只能拦截属性访问
// 无法拦截 map.set()、map.get() 等方法调用

2. 方法调用 vs 属性访问

// Vue 2 的响应式基于属性访问拦截
const obj = { name: '张三' }
// 读取 obj.name 会触发 getter
// 设置 obj.name = '李四' 会触发 setter

// Map 的操作是通过方法完成的
map.set('key', 'value')  // 这不是属性赋值,是方法调用
// Vue 无法拦截方法调用

// 如果要监听 Map,需要这样做(但 Vue 没有实现)
class ReactiveMap extends Map {
  set(key, value) {
    super.set(key, value)
    // 触发更新
    this._dep.notify()
  }
}

深入的技术限制

1. Proxy 与 Object.defineProperty 的本质区别

// Object.defineProperty - 只能拦截属性操作
Object.defineProperty(obj, 'name', {
  get() { return this._name },
  set(val) { 
    this._name = val
    // 只能知道属性被赋值
    // 不知道是新增属性还是修改
  }
})

// Proxy - 可以拦截更多操作
const proxy = new Proxy(obj, {
  get(target, key) { /* 拦截读取 */ },
  set(target, key, value) { /* 拦截设置 */ },
  deleteProperty(target, key) { /* 拦截删除 */ },
  has(target, key) { /* 拦截 in 操作符 */ },
  ownKeys(target) { /* 拦截 Object.keys() */ },
  // 还能拦截 Map/Set 的方法调用?不能直接拦截
  // 但可以拦截方法调用前的 get 操作
})

// 对于 Map:
const reactiveMap = new Proxy(map, {
  get(target, key) {
    const value = target[key]
    if(key === 'set') {
      // 包装 set 方法
      return function(...args) {
        const result = value.apply(target, args)
        console.log('Map set 被调用', args)
        // 触发更新
        return result
      }
    }
    return value
  }
})

2. V8 引擎对数组的优化

// V8 引擎的数组有两种存储模式

// 1. 快数组 (Fast Elements)
// - 连续内存存储
// - 通过索引直接访问
// - 性能高
const fastArray = [1, 2, 3]

// 2. 慢数组 (Dictionary Elements)
// - 使用哈希表存储
// - 适合稀疏数组
// - 性能较低
const slowArray = []
slowArray[10000] = 1  // 转为慢数组

// Vue 如果为每个索引添加 getter/setter
// 会导致快数组转为慢数组
// 性能大幅下降

Vue 2 的妥协方案和原因

// Vue 2 团队面临的选择:

// 方案1:为所有数组索引添加 getter/setter
Object.defineProperty(arr, 0, { get, set })
Object.defineProperty(arr, 1, { get, set })
// ...
// 问题:
// - 初始化性能差
// - 内存占用大
// - 破坏引擎优化
// - 无法处理动态长度

// 方案2:使用 Proxy(当时不可用)
// - ES6 Proxy 在 Vue 2 发布时(2016)兼容性不好
// - 无法在旧浏览器 polyfill

// 方案3:妥协方案 - 重写变异方法(Vue 2 的选择)
// 优点:
// - 性能好
// - 兼容性好
// - 覆盖大部分场景
// 缺点:
// - 监听不到索引修改和 length 修改
// - 需要开发者遵守约定

实际示例:为什么必须用 $set

// Vue 2 的响应式实现简化版
function defineReactive(obj, key, val) {
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      if(newVal !== val) {
        val = newVal
        dep.notify()  // 触发更新
      }
    }
  })
}

// 对于数组
const arr = [1, 2, 3]

// Vue 2 初始化时
arr.forEach((item, index) => {
  defineReactive(arr, index, item)  // 假设这样做了
})

// 问题1: 新增索引
arr[3] = 4  // 索引3没有 getter/setter,无法触发更新

// 问题2: 数组长度变化
arr.length = 2  // length 属性不可重新定义,无法拦截

// 问题3: 性能问题
const bigArray = new Array(1000000)
// 如果为每个索引添加 getter/setter
// 初始化会非常慢,内存暴增

总结

Vue 2 监听不到这些变化的原因是:

  1. 设计限制Object.defineProperty 只能拦截属性操作,不能拦截方法调用
  2. 性能考虑:为数组每个索引添加监听会破坏 V8 优化,导致性能问题
  3. 技术限制length 属性不可配置,无法重新定义
  4. 规范限制:Map/Set 的内部存储机制不是对象属性,无法通过属性拦截实现
  5. 兼容性考虑:发布时 Proxy 兼容性不够好

这也是为什么 Vue 3 必须用 Proxy 重写响应式系统的根本原因。

Promise链式调用原理

作者 卷帘依旧
2026年5月8日 15:39

Promise链式调用原理,then在不同情况下的返回值

在JavaScript中,Promise是一种用于处理异步操作的对象。它可以让你以同步的方式来编写异步代码,从而提高代码的可读性和可维护性。Promise对象代表了异步操作的最终完成(或失败)及其结果值。

Promise的基本用法

一个Promise对象有三种状态:

  1. Pending(等待) ‌ - 初始状态,既不是成功,也不是失败。
  2. Fulfilled(已成功) ‌ - 操作成功完成。
  3. Rejected(已失败) ‌ - 操作失败。

创建Promise

你可以使用new Promise()构造函数来创建一个Promise对象。这个构造函数接受一个执行器函数作为参数,执行器函数有两个参数,分别是resolvereject

let promise = new Promise(function(resolve, reject) {
    // 异步操作
    if (/* 成功条件 */) {
        resolve(value); // 操作成功,将Promise的状态改为Fulfilled,并传递结果值
    } else {
        reject(error); // 操作失败,将Promise的状态改为Rejected,并传递错误信息
    }
});

Promise链式调用

Promise的链式调用是通过.then().catch()方法实现的。.then()方法返回一个新的Promise实例,并且可以接受两个参数:第一个参数是操作成功时的回调函数,第二个参数是操作失败时的回调函数(可选)。.catch()方法用于指定发生错误时的回调函数。

let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("成功"), 1000); // 模拟异步操作成功
});

promise.then(result => {
    console.log(result); // "成功"
    return "下一个Promise"; // 这里返回的结果会传递给下一个.then()的回调函数
}).then(result => {
    console.log(result); // "下一个Promise"
}).catch(error => {
    console.log(error); // 错误处理
});

使用async/await进行链式调用

async/await是建立在Promise之上的语法糖,让异步代码的书写和阅读更加直观。async函数可以包含await表达式,await关键字用于等待一个Promise对象解析完成。

async function asyncCall() {
    try {
        let result = await firstPromise(); // 等待第一个Promise解析完成
        let nextResult = await secondPromise(result); // 使用第一个Promise的结果作为第二个Promise的输入,并等待解析完成
        console.log(nextResult); // 处理结果
    } catch (error) {
        console.error(error); // 错误处理
    }
}

总结

通过.then().catch()方法或者使用async/await,你可以很容易地实现Promise的链式调用,使得异步代码的管理变得更加容易和清晰。

❌
❌