普通视图

发现新文章,点击刷新页面。
昨天 — 2025年10月13日首页
昨天以前首页

Vue Mixin 全解析:概念、使用与源码

作者 excel
2025年10月11日 08:23

一、Mixin 的概念

Mixin(混入)来源于面向对象编程(OOP)中的概念,它是一类提供方法实现的类,其他类可以访问 Mixin 中的方法而不必继承它。

在 Vue 中,Mixin 提供了一种灵活的方式来复用组件功能,本质上就是一个 JavaScript 对象,它可以包含组件的任意选项,如 datamethodscomputed、生命周期钩子等。

组件使用 Mixin 时,Mixin 对象中的选项会“混入”到组件自身选项中,实现代码复用。

1. 局部混入(Local Mixin)

定义一个 Mixin 对象:

var myMixin = {
  created() {
    this.hello()
  },
  methods: {
    hello() {
      console.log('hello from mixin!')
    }
  }
}

在组件中使用:

Vue.component('componentA', {
  mixins: [myMixin]
})

运行时,组件会执行 Mixin 的 created 钩子,调用 hello 方法。

2. 全局混入(Global Mixin)

通过 Vue.mixin() 方法实现全局混入:

Vue.mixin({
  created() {
    console.log('全局混入')
  }
})

注意:全局混入会影响每一个组件实例,包括第三方组件。常用于插件开发,但要谨慎使用。

3. 合并规则与注意事项

  • 普通选项(data、methods 等) :组件的同名选项会覆盖 Mixin 中的选项。
  • 生命周期钩子:会合并为数组,先执行 Mixin 的钩子,再执行组件自身的钩子。

二、Mixin 的使用场景

在实际开发中,我们经常遇到多个组件有重复逻辑的情况。这些逻辑独立且可复用,此时 Mixin 就非常实用。

举例:有两个组件都需要控制显示状态 isShowing

// Modal 弹窗组件
const Modal = {
  template: '#modal',
  data() { return { isShowing: false } },
  methods: {
    toggleShow() { this.isShowing = !this.isShowing }
  }
}

// Tooltip 提示框组件
const Tooltip = {
  template: '#tooltip',
  data() { return { isShowing: false } },
  methods: {
    toggleShow() { this.isShowing = !this.isShowing }
  }
}

两者逻辑重复,可提取为 Mixin:

const toggle = {
  data() { return { isShowing: false } },
  methods: {
    toggleShow() { this.isShowing = !this.isShowing }
  }
}

const Modal = { template: '#modal', mixins: [toggle] }
const Tooltip = { template: '#tooltip', mixins: [toggle] }

通过 Mixin,我们实现了逻辑复用,代码更简洁。


三、Vue Mixin 源码解析

Vue Mixin 的核心实现分为两部分:全局 API 注册选项合并策略

1. 全局注册

源码位置:/src/core/global-api/mixin.js

export function initMixin(Vue) {
  Vue.mixin = function(mixin) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

核心是调用 mergeOptions 将 Mixin 与组件选项合并。

2. mergeOptions 方法

源码位置:/src/core/util/options.js

export function mergeOptions(parent, child, vm) {
  if (child.mixins) {
    child.mixins.forEach(m => parent = mergeOptions(parent, m, vm))
  }

  const options = {}
  for (let key in parent) mergeField(key)
  for (let key in child) if (!hasOwn(parent, key)) mergeField(key)

  function mergeField(key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }

  return options
}

合并策略分类:

策略类型 代表选项 合并规则
替换型 propsmethodsinjectcomputed 同名属性直接替换
合并型 data 通过 mergeData 递归合并对象属性
队列型 生命周期钩子、watch 以数组形式合并,正序执行
叠加型 componentsdirectivesfilters 利用原型链叠加,保留父级属性

a. 替换型

strats.methods = function(parentVal, childVal) {
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}

b. 合并型(data)

function mergeData(to, from) {
  if (!from) return to
  Object.keys(from).forEach(key => {
    if (!to.hasOwnProperty(key)) set(to, key, from[key])
    else if (typeof to[key] === 'object' && typeof from[key] === 'object')
      mergeData(to[key], from[key])
  })
  return to
}

c. 队列型(生命周期钩子、watch)

function mergeHook(parentVal, childVal) {
  return parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal]
}

d. 叠加型(组件、指令、过滤器)

function mergeAssets(parentVal, childVal) {
  const res = Object.create(parentVal || null)
  if (childVal) Object.keys(childVal).forEach(key => res[key] = childVal[key])
  return res
}

四、小结

  • Mixin 本质:JavaScript 对象,包含可复用组件选项。

  • 使用方式:局部混入(组件级)和全局混入(全局影响)。

  • 合并规则

    • 替换型 → 同名直接替换
    • 合并型 → 对象递归合并
    • 队列型 → 函数数组,正序执行
    • 叠加型 → 原型链叠加
  • 使用场景:复用独立逻辑、封装公共功能,减少代码重复。

通过源码分析,我们不仅理解了 Mixin 的使用,还了解了其底层选项合并机制,为更高级的插件开发和组件复用打下基础。


本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。

深入解析 Vue 3 源码:computed 的底层实现原理

作者 excel
2025年10月8日 22:46

在 Vue 3 的响应式系统中,computed 是一个非常重要的功能,它用于创建基于依赖自动更新的计算属性。本文将通过分析源码,理解 computed 的底层实现逻辑,帮助你从源码层面掌握它的原理。


一、computed 的基本使用

在使用层面上,computed 有两种常见用法:

1. 只读计算属性

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 输出 2
plusOne.value++ // 报错,因只读

2. 可写计算属性

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => { count.value = val - 1 }
})

plusOne.value = 3
console.log(count.value) // 输出 2

这两种形式在底层源码中都会通过一个统一的类 ComputedRefImpl 来实现。


二、核心类:ComputedRefImpl

ComputedRefImpl 是 Vue 3 中计算属性的核心实现类,它实现了响应式依赖追踪和懒执行机制。

1. 成员属性解析

export class ComputedRefImpl<T = any> implements Subscriber {
  _value: any = undefined          // 缓存计算后的值
  dep: Dep = new Dep(this)         // 依赖收集容器
  readonly __v_isRef = true        // 标记为 ref 类型
  readonly __v_isReadonly: boolean // 是否只读
  deps?: Link                      // 依赖链表(订阅者)
  flags: EffectFlags = EffectFlags.DIRTY // 标志位,初始为脏值
  globalVersion: number = globalVersion - 1 // 全局版本号
  isSSR: boolean                   // 是否在服务端渲染环境中
  next?: Subscriber = undefined    // 下一个订阅者

  // 调试钩子
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: ComputedGetter<T>,
    private readonly setter: ComputedSetter<T> | undefined,
    isSSR: boolean,
  ) {
    this[ReactiveFlags.IS_READONLY] = !setter
    this.isSSR = isSSR
  }
}

关键点:

  • flags:使用 EffectFlags 管理状态,如 DIRTY 表示需要重新计算。
  • dep:内部维护依赖列表,供其他响应式对象追踪。
  • _value:缓存上次计算的值,实现懒计算。
  • __v_isReadonly:若没有传入 setter,则为只读计算属性。

三、value 的访问逻辑

get value(): T {
  const link = this.dep.track() // 依赖追踪
  refreshComputed(this)         // 若脏则重新计算
  if (link) {
    link.version = this.dep.version
  }
  return this._value
}

这里是 computed 的核心机制:

  1. 依赖追踪:通过 dep.track() 让当前计算属性被其他响应式数据订阅。
  2. 惰性求值:只有在访问 .value 时,才会执行 getter 重新计算。
  3. 版本同步link.version 确保依赖的版本号一致,以判断是否需要重新计算。

四、value 的设置逻辑

set value(newValue) {
  if (this.setter) {
    this.setter(newValue)
  } else if (__DEV__) {
    warn('Write operation failed: computed value is readonly')
  }
}
  • 若定义了 setter,则允许外部修改计算属性值;
  • 否则在开发环境中发出警告,提示为只读属性。

五、依赖更新与通知机制

当依赖的响应式数据发生变化时,notify() 会被调用:

notify(): true | void {
  this.flags |= EffectFlags.DIRTY // 标记为脏值
  if (!(this.flags & EffectFlags.NOTIFIED) && activeSub !== this) {
    batch(this, true) // 批量更新依赖
    return true
  }
}

这里的关键是:

  • 标记为 “脏”,下次访问时会重新计算;
  • 通过 batch 批量更新,避免重复通知。

六、computed 函数入口

外层的 computed 函数是一个工厂方法:

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false,
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T> | undefined

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.onTrack = debugOptions.onTrack
    cRef.onTrigger = debugOptions.onTrigger
  }

  return cRef as any
}

这里做了两件事:

  1. 根据参数判断是只读还是可写计算属性;
  2. 创建 ComputedRefImpl 实例;
  3. 如果处于开发模式,还会绑定调试钩子(onTrackonTrigger)。

七、总结

Vue 3 中的 computed 实现基于以下关键机制:

机制 作用
惰性求值 (Lazy Evaluation) 仅在访问时计算结果
缓存结果 (Caching) 若依赖未变则返回缓存值
依赖追踪 (Dependency Tracking) 自动感知依赖变化
脏标记 (Dirty Flag) 控制何时重新计算
批量更新 (Batching) 提高性能,减少重复通知

从源码可以看到,computed 实际上是一个特殊的 ref,但拥有更多的依赖追踪与缓存机制,是 Vue 响应式系统中非常精妙的一环。


本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。

一文读懂 Vue 组件间通信机制(含 Vue2 / Vue3 区别)

作者 excel
2025年10月8日 07:22

一、组件间通信的概念

在 Vue 中,组件(Component) 是最核心的概念之一。每个 .vue 文件都可以视为一个独立的组件。
通信(Communication) 是指一个组件如何将信息传递给另一个组件。

通俗地说:

组件间通信,就是不同组件之间如何共享数据、触发行为、进行信息交互的过程。

例如:当我们使用 UI 框架中的 table 组件时,需要向它传入 data 数据,这个“传值”的过程本质上就是一种组件通信。


二、组件间通信解决了什么问题?

在实际开发中,每个组件都有自己的作用域,数据默认是相互隔离的。但我们经常希望:

  • 父组件能把数据传给子组件;
  • 子组件能向父组件反馈事件或数据;
  • 两个兄弟组件能共享状态;
  • 甚至是跨层级(祖孙、非亲缘)组件之间的数据共享。

这正是组件通信的意义所在

让组件之间能够协作、共享数据,从而构成一个有机、完整的应用系统。


三、组件间通信的分类

按照组件关系,通信可分为以下几种:

  1. 父子组件通信
  2. 兄弟组件通信
  3. 祖孙组件通信
  4. 非关系组件通信

简单关系图如下:

Parent
 ├── Child A
 └── Child B
      └── GrandChild

四、Vue 中 8 种常见通信方案

下面是 Vue2 / Vue3 通用的 8 种通信方式,并在最后补充两者的区别。


1. props 传递数据(父 → 子)

适用场景: 父组件向子组件传值。

子组件定义:

export default {
  props: {
    name: String,
    age: {
      type: Number,
      default: 18,
      required: true
    }
  }
}

父组件使用:

<Child name="Jack" :age="18" />

📘 Vue3 区别:
Vue3 支持在 <script setup> 中使用 defineProps

const props = defineProps({
  name: String,
  age: Number
})

2. $emit 触发自定义事件(子 → 父)

适用场景: 子组件向父组件传递数据或事件。

子组件:

this.$emit('add', good)

父组件:

<Child @add="cartAdd($event)" />

📘 Vue3 区别:
Vue3 使用 defineEmits

const emit = defineEmits(['add'])
emit('add', good)

3. ref 获取子组件实例(父 → 子)

适用场景: 父组件想直接访问子组件实例或方法。

父组件:

<Child ref="foo" />
this.$refs.foo.someMethod()

📘 Vue3 区别:
Vue3 的 ref 通过 setup 中的 ref() 引用,访问方式为 childRef.value


4. EventBus 事件总线(兄弟组件)

适用场景: 兄弟组件之间传值。

Bus.js:

import Vue from 'vue'
export const Bus = new Vue()

发送方:

Bus.$emit('foo', data)

接收方:

Bus.$on('foo', (data) => { console.log(data) })

📘 Vue3 区别:
Vue3 没有 $on/$off,可用第三方库(如 mitt)实现轻量事件总线:

import mitt from 'mitt'
export const bus = mitt()

bus.emit('foo', data)
bus.on('foo', handler)

5. parent/parent / root(祖辈 → 后代)

适用场景: 通过组件实例树访问父组件或根组件方法。

this.$parent.someMethod()
this.$root.globalFn()

📘 Vue3 区别:
仍可用,但在 Composition API 下不推荐,应优先使用 provide/inject


6. attrsattrs 与 listeners(祖先 → 子孙)

适用场景: 批量向下传递属性与事件。

<!-- Parent.vue -->
<Child2 msg="hello" @some-event="handleEvent" />

<!-- Child2.vue -->
<Grandson v-bind="$attrs" v-on="$listeners" />

<!-- Grandson.vue -->
<div @click="$emit('some-event', 'from grandson')">
  {{ msg }}
</div>

📘 Vue3 区别:
Vue3 将 $listeners 合并入 $attrs,事件监听器自动包含其中:

<Grandson v-bind="$attrs" />

7. Provide / Inject(祖孙通信)

适用场景: 祖先组件向任意深度的后代组件传递数据。

祖先组件:

provide() {
  return {
    foo: 'foo'
  }
}

后代组件:

inject: ['foo']

📘 Vue3 区别:
Vue3 使用 Composition API:

// 祖先
import { provide } from 'vue'
provide('foo', 'foo')

// 后代
import { inject } from 'vue'
const foo = inject('foo')

8. Vuex(或 Pinia)(全局状态管理)

适用场景: 复杂组件关系、跨层级状态共享。

Vuex 核心概念:

  • state:存放共享状态;
  • getters:相当于计算属性;
  • mutations:同步修改 state;
  • actions:支持异步逻辑;
  • modules:模块化管理。

📘 Vue3 推荐:
Vue 官方推荐使用 Pinia,API 更简洁、类型友好。


五、通信方式选择建议

关系类型 推荐通信方式 说明
父 → 子 props 简单直接
子 → 父 $emit 常用于回调
父 ↔ 子 ref 调用方法或取值
兄弟组件 EventBus / mitt 中央事件总线
祖孙组件 provide / inject 优雅高效
非关系组件 Vuex / Pinia 全局状态共享
属性透传 $attrs / $listeners 批量下传属性
简单共享状态 Composition API + reactive() Vue3 场景

六、小结

  • 父子通信: 使用 props$emit 最简洁,也可用 ref
  • 兄弟通信: 推荐 EventBusmitt
  • 祖孙通信:provide / inject 最优雅。
  • 全局通信: 使用 Vuex(Vue2)或 Pinia(Vue3)。
  • Vue3 提升点: Composition API 让通信更灵活,逻辑更集中。

总结一句话:

在 Vue 中,组件间通信的本质就是——让数据在不同组件间可控地流动


本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。

dep.ts 逐行解读

作者 excel
2025年10月7日 23:29

简化归纳

一、导入与上下文说明(开头几行)

import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
import type { ComputedRefImpl } from './computed'
import { type TrackOpTypes, TriggerOpTypes } from './constants'
import {
  type DebuggerEventExtraInfo,
  EffectFlags,
  type Subscriber,
  activeSub,
  endBatch,
  shouldTrack,
  startBatch,
} from './effect'

解释:

  • 这些导入给出当前模块依赖的工具函数、类型以及响应式运行时的状态/函数。
  • activeSub:当前正在执行或收集依赖的 Subscriber(副作用/计算)。
  • shouldTrack:是否允许在当前上下文下收集依赖(例如在某些内部读取时关闭追踪)。
  • startBatch / endBatch:用于批处理通知(合并多次触发执行)。
  • EffectFlags:位掩码枚举,用于标识 effect/computed 的状态(如 TRACKING、DIRTY、NOTIFIED 等)。
  • 这些都在后文会反复用到以实现精细控制。

二、全局版本号

export let globalVersion = 0

解释:

  • 每当有 reactive 改变发生,全局版本 globalVersion 随之增长。
  • 作用:给 computed 等提供一个“快速路径”判断,避免在内容未变时重复计算;也可以作为调试或一致性检查的基础。

三、Link:dep ↔ subscriber 的连接节点

export class Link {
  version: number
  nextDep?: Link
  prevDep?: Link
  nextSub?: Link
  prevSub?: Link
  prevActiveLink?: Link

  constructor(
    public sub: Subscriber,
    public dep: Dep,
  ) {
    this.version = dep.version
    this.nextDep =
      this.prevDep =
      this.nextSub =
      this.prevSub =
      this.prevActiveLink =
        undefined
  }
}

解释(要点)

  • Link 表示 单一的 dep (某对象的某个 key) 与 单一的 subscriber(effect / computed)的绑定(many-to-many 的一条边)。

  • 之所以用 Link 而不是直接 Set,是为了:

    • 使用双向链表能在 O(1) 插入/移除节点(减少内存与时间开销)。
    • 允许在 effect 的 deps 列表与 dep 的 subs 列表之间建立双向导航(便于清理,维护访问顺序等)。
  • 字段解释:

    • version:记录 link 与 dep 的版本同步状态(用于清理/重用判定)。
    • nextDep/prevDep:在 effect 的依赖链表 中的双向指针(一个 effect 可能依赖多个 dep)。
    • nextSub/prevSub:在 dep 的订阅者链表 中的双向指针(一个 dep 可能有多个 subscriber)。
    • prevActiveLink:用于在 effect 重新收集依赖时的临时链表(实现细节相关,用于高效重排序/回收)。

四、Dep 类:单个响应式“属性”的依赖容器

export class Dep {
  version = 0
  activeLink?: Link = undefined
  subs?: Link = undefined
  subsHead?: Link
  map?: KeyToDepMap = undefined
  key?: unknown = undefined
  sc: number = 0
  readonly __v_skip = true

  constructor(public computed?: ComputedRefImpl | undefined) {
    if (__DEV__) {
      this.subsHead = undefined
    }
  }

  track(debugInfo?: DebuggerEventExtraInfo): Link | undefined { ... }

  trigger(debugInfo?: DebuggerEventExtraInfo): void { ... }

  notify(debugInfo?: DebuggerEventExtraInfo): void { ... }
}

字段逐一解释:

  • version:dep 自身的版本(每次 trigger() 增加)。用于和 link.version 协调,判断 link 是否在当前运行中被访问到。
  • activeLink:针对当前 activeSub 的一个快速指针(优化)。当 activeSub 连续多次访问同一个 dep 时,比遍历 subs 更快地找到已有 Link
  • subs(尾指针)和 subsHead(头指针,仅 DEV 用于触发顺序):表示订阅此 dep 的 subscribers 链表。注意实现中以尾为主(方便 append)。
  • map / key:在创建 dep 时,记录回指向所属 target 的 depsMap(用于 debug 或清理)与对应的 key。
  • sc:subscriber counter,dep 的订阅者计数(用于统计/优化)。
  • __v_skip:内部标志(略)——告诉响应式系统此对象在某些流程里要被跳过(实现细节)。

构造参数:

  • computed:如果这个 dep 是由某个 computed 拥有的(computed 内部有自己的 dep),则记录 computed 引用(这将在 computed 首次被订阅时有特殊处理)。

五、Dep.track(debugInfo?) 的完整逻辑(依赖收集)

源码(节选并注释核心流程):

track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
  if (!activeSub || !shouldTrack || activeSub === this.computed) {
    return
  }

  let link = this.activeLink
  if (link === undefined || link.sub !== activeSub) {
    link = this.activeLink = new Link(activeSub, this)
    // 将 link 添加到 activeSub.deps(作为尾部)
    if (!activeSub.deps) {
      activeSub.deps = activeSub.depsTail = link
    } else {
      link.prevDep = activeSub.depsTail
      activeSub.depsTail!.nextDep = link
      activeSub.depsTail = link
    }
    addSub(link)
  } else if (link.version === -1) {
    // 重用上次运行中保留的 link:需要将 link.version 同步为当前 dep.version
    link.version = this.version
    // 如果该 link 不是 tail,则把它移动到 tail(维护访问顺序)
    if (link.nextDep) {
      const next = link.nextDep
      next.prevDep = link.prevDep
      if (link.prevDep) {
        link.prevDep.nextDep = next
      }

      link.prevDep = activeSub.depsTail
      link.nextDep = undefined
      activeSub.depsTail!.nextDep = link
      activeSub.depsTail = link

      if (activeSub.deps === link) {
        activeSub.deps = next
      }
    }
  }

  if (__DEV__ && activeSub.onTrack) {
    activeSub.onTrack( extend({ effect: activeSub }, debugInfo) )
  }

  return link
}

逐件解释(关键点):

  1. 早退条件

    • !activeSub:没有活跃的副作用则不收集。
    • !shouldTrack:当前上下文禁止收集。
    • activeSub === this.computed:如果当前正在运行的 effect 就是这个 dep 所属的 computed(避免自己追自己)——不收集(防止循环/冗余)。
  2. activeLink 快速路径

    • activeLink 用作快速比较:如果保存的 last activeLink.sub 就是当前 activeSub,说明当前 effect 与此 dep 曾经有过绑定,可以直接复用 Link 而不用重新创建或查找整个 subs 链表。
  3. 创建新 Link 并把它 append 到 activeSub.deps 的尾部

    • activeSub.deps / depsTail:effect 自身维护一个依赖链表,收集完成后用于清理那些不再使用的依赖(优化回收)。
  4. addSub(link)

    • 负责把 link 添加到 dep 的 subs 链表(只在 subscriber 处于 TRACKING 时才真正加入,详见 addSub)。
  5. 重用 link(link.version === -1)

    • 在 effect 的重新运行中,开始时会把之前的所有 link 的 version 置为 -1(表示尚未在本次运行被访问)。
    • 当某个 dep 再次被访问到,会把 link.version 同步为 dep.version;如果此 link 在 effect 的 deps 列表不是尾部,会把它移动到尾部以保持“访问顺序”。访问顺序用于在后续清理时将未访问到(仍为 -1)的 link 高效剪除。
  6. 开发者工具 hook

    • 如果存在 onTrack 钩子(开发时),会把调试信息传给用户。

六、Dep.trigger 与 Dep.notify(触发更新)

trigger(debugInfo?: DebuggerEventExtraInfo): void {
  this.version++
  globalVersion++
  this.notify(debugInfo)
}

触发要点:

  • 每次 trigger() 时:

    • this.version++:dep 自增版本,之后在下一次 track() 时,link.version 会与之对齐(用于判断 link 是否在新一轮中被访问),从而实现依赖清理与缓存失效。
    • globalVersion++:全局版本也增加,供 computed 的快速路径判断等使用。
    • 然后调用 notify() 真正通知订阅者。

notify 的实现要点(节选)

notify(debugInfo?: DebuggerEventExtraInfo): void {
  startBatch()
  try {
    if (__DEV__) {
      for (let head = this.subsHead; head; head = head.nextSub) {
        if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
          head.sub.onTrigger( extend({ effect: head.sub }, debugInfo) )
        }
      }
    }
    for (let link = this.subs; link; link = link.prevSub) {
      if (link.sub.notify()) {
        (link.sub as ComputedRefImpl).dep.notify()
      }
    }
  } finally {
    endBatch()
  }
}

解释(重要行为与原因):

  1. 批处理包裹

    • startBatch() / endBatch():确保在一次操作中多次触发会合并为一次批次更新(减少重复渲染/计算)。
  2. DEV 模式的 onTrigger

    • 为了调试与 devtools:按 原始顺序(从 head 向 nextSub)调度 onTrigger 钩子(跟后面实际的通知顺序不同),且跳过已经被标记为 NOTIFIED 的 subscriber(避免重复回调)。
  3. 实际通知顺序

    • 实际通知是从 subs 尾部向前(prevSub)遍历。为什么?

      • 这样能保证按 反序 收集到的依赖被先通知,然后在 batch 结束时以原始顺序执行具体回调(实现上可减少冲突)。
  4. computed 的特别处理

    • if (link.sub.notify())notify() 返回 true 表明该 subscriber 是 computed(返回表示 computed 需要特殊处理)。
    • 当 computed 被标记为需要更新时,会触发其 own dep 的 notify(),将 computed 的变更进一步传播给依赖于 computed 的其他 subscribers。这么做的原因是为了减少调用栈深度:先把 computed 的 change 标记好并在这里触发它的 dep 通知,而不是在 computed 内部深层递归去触发,避免过深的 JS 调用栈。
  5. finally 保证

    • finally { endBatch() }:即便内部抛出异常也要正确结束批处理,保持系统状态一致。

七、addSub(link) 函数(把 link 加入 dep 的 subscribers 链表)

function addSub(link: Link) {
  link.dep.sc++
  if (link.sub.flags & EffectFlags.TRACKING) {
    const computed = link.dep.computed
    // computed getting its first subscriber
    if (computed && !link.dep.subs) {
      computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
      for (let l = computed.deps; l; l = l.nextDep) {
        addSub(l)
      }
    }

    const currentTail = link.dep.subs
    if (currentTail !== link) {
      link.prevSub = currentTail
      if (currentTail) currentTail.nextSub = link
    }

    if (__DEV__ && link.dep.subsHead === undefined) {
      link.dep.subsHead = link
    }

    link.dep.subs = link
  }
}

解释(关键点):

  1. link.dep.sc++:订阅者计数增加(统计用)。

  2. if (link.sub.flags & EffectFlags.TRACKING):只有当 subscriber 正确处于 TRACKING 状态时,才真的把它加入 dep 的 subscribers 列表(某些 effect 在某时刻可能被禁用 tracking)。

  3. Computed 的延迟订阅(lazy subscription)

    • link.dep.computed 存在,且当前 dep 还没有 subscribers(!link.dep.subs),说明这是 computed 的第一个订阅者:

      • 给 computed 标记 TRACKING & DIRTY(开启 tracking 并标记为脏),然后延迟地把 computed 自身所依赖的那些 dep(computed.deps)逐个通过 addSub(l) 添加上去,使计算属性在以后依赖项变化时能被正确通知。
      • 这一步非常关键:computed 在没人订阅时通常不建立自己与底层 deps 的双向引用(节省内存、计算),当第一次有外界订阅 computed 时,computed 才会真正订阅它内部依赖。
  4. 把 link 插入到 dep.subs 的尾部

    • 以尾部为主方便追加,并且保留 subsHead(DEV)用于按正确顺序触发 onTrigger 钩子。
  5. 结论:addSub 是把 effect/computed 正式注册为依赖项的机制,同时处理 computed 的第一次订阅的延迟绑定行为。


八、全局依赖表与迭代相关 key

type KeyToDepMap = Map<any, Dep>
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()

export const ITERATE_KEY: unique symbol = Symbol(__DEV__ ? 'Object iterate' : '')
export const MAP_KEY_ITERATE_KEY: unique symbol = Symbol(__DEV__ ? 'Map keys iterate' : '')
export const ARRAY_ITERATE_KEY: unique symbol = Symbol(__DEV__ ? 'Array iterate' : '')

解释:

  • targetMap:顶层 WeakMap,把每个 reactive 对象 target 映射到它的 Map(key -> Dep)

    • 使用 WeakMap 的原因:当 target 对象没有外部引用时,GC 能自动回收相关依赖表,避免内存泄漏。
  • KeyToDepMap:对一个 target 来说,每个属性 key 对应一个 Dep 实例。

  • ITERATE_KEY / MAP_KEY_ITERATE_KEY / ARRAY_ITERATE_KEY

    • 这些是用于“迭代依赖”的特殊 key(Symbol),用于处理 for..in / Object.keys / Map.keys / 数组遍历等操作的依赖追踪。
    • 例如:对对象进行 for..in 的副作用应在属性被 ADDDELETE 时触发,而不是只在某个具体 key 变化时触发 —— 所以使用特殊的 iterate-key。

九、track(target, type, key):外部入口(在 proxy 的 getter 中被调用)

export function track(target: object, type: TrackOpTypes, key: unknown): void {
  if (shouldTrack && activeSub) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Dep()))
      dep.map = depsMap
      dep.key = key
    }
    if (__DEV__) {
      dep.track({ target, type, key })
    } else {
      dep.track()
    }
  }
}

解释:

  • track 是数据访问时调用的入口(常在 Proxy 的 get 钩子里)。

  • 流程:

    1. 仅在允许追踪且有活跃副作用时继续。(避免在读取内部字段或初始化时收集无关依赖)
    2. targetMap 拿到 depsMap,若无则创建。
    3. depsMap 根据 key 拿 Dep,若无则新建并把 map/key 回指上去(便于 debug / 清理)。
    4. 调用 dep.track()(传 debugInfo 时为 dev 模式)。
  • 特别注意:这个函数不会直接把 activeSub 添加到 dep;而是通过 Dep.track() 完成(且 Dep.track 内做了许多优化分支)。


十、trigger(...):外部入口(在 proxy 的 setter/delete/collection 操作中被调用)

完整签名(你给出的):

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
): void { ... }

逻辑要点(详解)

  1. 先拿 depsMap

    const depsMap = targetMap.get(target)
    if (!depsMap) {
      globalVersion++
      return
    }
    
    • 如果没有记录任何依赖(从未被 track),直接增加 globalVersion(保持版本一致性),然后返回——没有订阅者就不需要做任何通知工作。
  2. 封装 run helper

    • run(dep) 会根据 DEV 与非 DEV 分别调用 dep.trigger(debugInfo?)dep.trigger(),这是为了统一触发单个 dep。
  3. 开始批处理

    • startBatch() 包裹后续通知操作。
  4. 类型为 CLEAR 的处理

    • 如果是 CLEAR(集合被清空),就对 depsMap 的每个 dep 都执行 run(dep)(因为任何依赖集合结构的 effect 都应被触发)。
  5. 其他情况(SET/ADD/DELETE/普通 SET)

    • 先判断 targetIsArrayisArrayIndex(索引更新)。

    • 数组特殊:长度变化

      • key === 'length':当 length 被设为更短值时,需要触发索引 >= 新长度 的那些 deps(因为元素被移除)。
      • 代码里 if (key === 'length') { depsMap.forEach((dep, key) => { if (key === 'length' || key === ARRAY_ITERATE_KEY || (!isSymbol(key) && key >= newLength)) run(dep) }}) }
      • 解释:触发 length,触发数组迭代依赖(ARRAY_ITERATE_KEY),并且触发所有索引 >= newLength 的键对应的 dep。
    • 普通属性或集合键变化

      • 如果 key !== void 0 || depsMap.has(void 0)run(depsMap.get(key))。注意 depsMap.has(void 0) 是为了处理一些内部用 undefined 作为 key 的场景(实现细节)。
      • isArrayIndex 则还要触发 ARRAY_ITERATE_KEY(因为改变数组中某个索引也会影响到依赖数组遍历的 effect)。
    • 依据 TriggerOpTypes 做额外触发

      • ADD

        • 若目标不是数组:触发 ITERATE_KEY(对象属性新增影响 for..in 等)。
        • 若是 Map:同时触发 MAP_KEY_ITERATE_KEY(Map keys 迭代依赖)。
        • 若是数组且添加的是新索引:触发 'length' 的 dep(新索引使 length 改变)。
      • DELETE

        • 若目标不是数组:触发 ITERATE_KEY,Map 同时触发 MAP_KEY_ITERATE_KEY
      • SET

        • 若是 Map 的 set:触发 ITERATE_KEY(因为 Map 值更新也可能影响某些迭代逻辑)。
    • 结尾 endBatch() 关闭批处理。

目的与设计思想:

  • trigger 的复杂分支是为了准确触发受影响的副作用,避免过度触发(减少渲染/计算),同时确保对集合类型(数组、Map、Set、对象)不同操作语义的正确映射(例如数组 length、迭代器依赖等)。

十一、辅助方法:getDepFromReactive

export function getDepFromReactive(
  object: any,
  key: string | number | symbol,
): Dep | undefined {
  const depMap = targetMap.get(object)
  return depMap && depMap.get(key)
}

解释:

  • 简单的调试/工具用的便捷函数:直接从 targetMap 取出某对象 key 对应的 Dep(若存在)。
  • 在调试工具或开发时会用到该接口来检查当前依赖图。

十二、设计上的若干深入说明(总结与边界细节)

  1. 为什么要用 Link 而不是 Set?

    • Link 允许在 effectdep 两侧分别维护链表(effect 的 deps 列表与 dep 的 subs 列表)。
    • 这样能在 effect 重新执行后把没有被再次访问的依赖(link.version 仍为 -1)快速从链表断开,避免在 GC 或 clear 时遍历大型集合。
    • 双链表便于 O(1) 的插入与移除(不需要在 Set 中搜索并分配大量内存)。
  2. version / link.version 的工作机制

    • 在每次 effect 执行前,effect 会把自己所有的 link.version 标成 -1(表示“未在本次执行被访问”)。
    • 在执行时访问某个 dep->track,会把对应 link.version 同步为 dep.version(标记为已访问)。
    • 执行结束后,仍然为 -1 的 link 表示这个依赖已不再需要,可以清理(在 effect 的收集结束清理流程中实现,这段代码不在你给出的片段中,但 link.version 的语义正是为此设计)。
  3. computed 的懒订阅策略

    • computed 在无人订阅时通常处于惰性(lazy)状态:内部不会把自己与底层依赖建立双向引用,数据只在被读取时计算并缓存。
    • 当 computed 第一次被外部 subscriber 订阅时(在 addSub 里发现 dep 没有 subs),需要把 computed 标记为 TRACKING,并把 computed 已知的依赖通过 addSub 逐个注册到底层 deps 中,这样底层 deps 变更时才能通知到 computed。
    • 这是一种折中的策略:节约内存(无人订阅的 computed 不需占用订阅链表)并在有需要时建立完整订阅链。
  4. 通知顺序与 onTrigger 的差异

    • 源码在 notify 中先按 subsHead 顺序触发 onTrigger(用于 dev 品质的回调),但真正对 subscribers 的通知是按 subs 的反序(从尾到头)。这样设计一方面保证 dev hooks 能看到“原始顺序”的调用(对调试更友好),另一方面允许 runtime 在最终 batch 执行时以另一种顺序处理(通常是为了减少副作用冲突与保证某些顺序语义)。
  5. 对数组的特殊处理

    • 数组是既有索引访问又有 length 语义的特殊容器:改变某个索引可能触发遍历依赖;改变 length 可能截断元素,需要触发被截断元素对应的 deps。
    • 所以代码中对 key === 'length' 的判断和 isArrayIndex 的判断非常重要,能避免在数组操作时无意义或漏掉通知。
  6. 批处理(startBatch / endBatch)

    • 批处理通常会收集本轮触发的 effect,延迟实际的执行到 endBatch 时再按合并策略触发。好处是当一次操作内部改变多处 reactive 时不会重复渲染或重复计算(例如在一个 setter 里做多个属性改动)。
  7. 性能考量

    • 大量的设计选择(弱引用 WeakMap、Link 链表、activeLink 快速路径、computed 延迟订阅、批处理)都是为在现代大型应用中既保证正确性又尽量降低内存与 CPU 开销而设计的。

十二、开发者(DEV)模式的辅助特性

  • onTrack / onTrigger:开发时工具 hook,帮助 devtools 展示依赖关系与触发的上下文信息。
  • subsHead:仅在 DEV 中用于保证 onTrigger 的原始调用顺序。
  • key/map 在 dep 中保存,是为了在 devtools 或 debug 输出中能把 dep 回溯到源对象与属性,便于诊断。

结束语与可扩展建议

  • 上述解释覆盖了你给出的源码中所有逻辑路径、字段和关键分支(包括 computed 交互、数组/集合/迭代的特例、dev hooks、版本/重用机制等)。

  • 如果你愿意,我可以继续:

    • effect 模块中与 activeSubstartBatch/endBatchEffectFlags 相关的实现也贴出来并逐行解释,或者
    • 给出一个图示(依赖图、链表关系、触发顺序)来帮助可视化理解,或
    • 针对某一段(例如 link 重排序、computed 延迟绑定)做性能与复杂度分析并提供改进建议。
  • 现在我已经把文件里你贴出来的所有代码路径与语义做了完整详解;若还想把周边模块(effectcomputed 的实现)也纳入“全部”解释,请直接告诉我要展开哪个模块,我会在同一回复中继续展开(不做异步等待)。

本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。

Vue3 响应式核心源码全解析:Dep、Link 与 track/trigger 完整执行机制详解

作者 excel
2025年10月7日 23:27

逐行解读

Vue3 的响应式系统是整个框架的灵魂,它让开发者能够在不显式调用更新的情况下自动响应数据变化。本文将带你深入阅读 Vue3 的核心响应式模块源码,重点讲解 DepLinktracktrigger 等关键机制,并用通俗的语言串联其工作流程,让你真正理解 Vue3 响应式系统的运行原理。


一、响应式系统的设计思路

Vue3 的响应式系统基于 依赖收集(track)派发更新(trigger) 两大过程:

  • track:在读取响应式数据时记录依赖,建立「谁依赖了谁」的关系;
  • trigger:当依赖的数据发生变化时,通知对应的副作用函数(effect)重新执行。

核心问题是:如何高效、准确地追踪依赖关系并在更新时精准触发?

Vue3 使用 Dep(依赖容器)和 Link(依赖连接)这两个类,配合 targetMap(全局依赖映射表),构建出一个双向的依赖追踪结构。


二、Dep:依赖容器

Dep 是每个响应式属性的依赖中心,负责维护所有订阅它的副作用函数。

export class Dep {
  version = 0
  activeLink?: Link
  subs?: Link
  subsHead?: Link
  sc: number = 0

  constructor(public computed?: ComputedRefImpl | undefined) {}
}

主要属性:

  • version:当前依赖的版本号,每次触发时递增;
  • subs / subsHead:双向链表,记录所有订阅者(副作用函数);
  • activeLink:当前活跃的依赖连接;
  • sc:订阅者数量计数。

Dep 是响应式系统的“核心神经节点”,它知道有哪些副作用依赖自己,也能在变化时精确通知它们。


三、Link:Dep 与 Effect 的连接纽带

DepEffect(副作用函数)是多对多关系。
Link 就是这两者之间的桥梁,用于在它们之间建立和维护依赖。

export class Link {
  version: number
  nextDep?: Link
  prevDep?: Link
  nextSub?: Link
  prevSub?: Link

  constructor(public sub: Subscriber, public dep: Dep) {
    this.version = dep.version
  }
}

可以理解为:

  • 每个 Link 同时存在于两条链表中:

    • 一条属于 Effect,记录它依赖的所有 Dep
    • 一条属于 Dep,记录它的所有订阅者。
  • Effect 执行时,Link 会根据访问的属性动态更新依赖关系。

这种 双向链表结构 能快速定位和清理依赖,避免重复依赖和内存泄漏。


四、track:依赖收集

track() 是读取响应式属性时调用的核心函数。它的任务是:
在读取属性时记录当前活跃的副作用函数(activeSub),让它与属性对应的 Dep 建立联系。

export function track(target: object, type: TrackOpTypes, key: unknown): void {
  if (shouldTrack && activeSub) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Dep()))
      dep.map = depsMap
      dep.key = key
    }
    dep.track({ target, type, key })
  }
}

执行流程如下:

  1. 查找当前对象在 targetMap 中的依赖映射;
  2. 如果没有就创建一个新的;
  3. 获取对应 key 的 Dep
  4. 调用 dep.track() 将当前的 activeSub(副作用函数)与此属性关联。

简单来说,track() 就是在“我读取了这个属性”时,登记“我依赖它”。


五、trigger:派发更新

trigger() 是响应式系统的另一端,它在属性修改时触发依赖更新。

export function trigger(target, type, key?, newValue?, oldValue?) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    globalVersion++
    return
  }

  const run = (dep: Dep | undefined) => dep && dep.trigger()

  if (type === TriggerOpTypes.CLEAR) {
    depsMap.forEach(run)
  } else {
    const targetIsArray = isArray(target)
    const isArrayIndex = targetIsArray && isIntegerKey(key)

    if (targetIsArray && key === 'length') {
      // 数组长度变化时触发相关依赖
      const newLength = Number(newValue)
      depsMap.forEach((dep, key) => {
        if (key === 'length' || key >= newLength) run(dep)
      })
    } else {
      run(depsMap.get(key))
      if (isArrayIndex) run(depsMap.get(ARRAY_ITERATE_KEY))
      switch (type) {
        case TriggerOpTypes.ADD:
          run(depsMap.get(ITERATE_KEY))
          break
        case TriggerOpTypes.DELETE:
          run(depsMap.get(ITERATE_KEY))
          break
      }
    }
  }
}

核心思路:

  • 修改属性 → 找到对应的 Dep
  • 调用 dep.trigger() 通知所有订阅者;
  • 依赖对应的 Effect(或 Computed)重新执行。

Vue3 针对不同类型(对象、数组、Map、Set)做了细致优化,比如:

  • 数组修改 length 会触发超出新长度的索引依赖;
  • Map 的 ADDDELETE 会额外触发迭代器依赖。

六、globalVersion 与批处理机制

每次响应式变更都会触发:

globalVersion++

它用于快速判断计算属性(computed)是否需要重新计算。

此外,Vue3 在触发更新时通过 startBatch()endBatch() 包裹通知过程,实现了批量触发、延迟执行,从而显著减少重复计算和性能浪费。


七、依赖结构图示

为了更直观地理解整个机制,可以把依赖关系抽象为如下结构:

Reactive Target
   └── key -> Dep
            ├── Link -> Effect A
            ├── Link -> Computed B
            └── Link -> Effect C
  • track():在访问属性时建立这些箭头;
  • trigger():当属性变化时,沿箭头依次通知所有订阅者。

八、总结

Vue3 的响应式系统在性能和结构上都做了精细设计:

  • 使用 Dep 管理依赖;
  • Link 构建高效的双向链表;
  • 借助 targetMap 构建全局依赖关系;
  • 通过 tracktrigger 完成依赖收集与派发更新;
  • 引入 globalVersion 与批处理优化机制,避免多余计算。

当你理解了这些底层逻辑,再回头看 Vue 的 reactive()computed()watch() 等 API,就会发现——它们都是基于这套机制自然生长出来的。


本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。

Vue3 EffectScope 源码解析与理解

作者 excel
2025年10月6日 21:13

1. 全局变量与基础类定义

  • activeEffectScope:表示当前正在运行的 effect 作用域。
  • EffectScope 类:用来管理一组副作用(ReactiveEffect),提供生命周期控制。
import type { ReactiveEffect } from './effect'
import { warn } from './warning'

// 当前全局正在运行的作用域
export let activeEffectScope: EffectScope | undefined

export class EffectScope {
  private _active = true              // 是否活跃
  private _on = 0                     // on() 调用计数
  effects: ReactiveEffect[] = []      // 收集的副作用
  cleanups: (() => void)[] = []       // 清理回调
  private _isPaused = false           // 是否暂停

  parent: EffectScope | undefined     // 父作用域
  scopes: EffectScope[] | undefined   // 子作用域
  private index: number | undefined   // 在父作用域数组中的索引

2. 构造函数与层级关系

  • 构造时会判断是否 detached(独立作用域)。
  • detached 时,会自动挂到当前 activeEffectScopescopes 中,形成父子层级。
  constructor(public detached = false) {
    this.parent = activeEffectScope
    if (!detached && activeEffectScope) {
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(this) - 1
    }
  }

  get active(): boolean {
    return this._active
  }

3. 暂停与恢复

  • pause():暂停本作用域及子作用域的所有副作用。
  • resume():恢复运行。
  pause(): void {
    if (this._active) {
      this._isPaused = true
      if (this.scopes) {
        for (let i = 0; i < this.scopes.length; i++) {
          this.scopes[i].pause()
        }
      }
      for (let i = 0; i < this.effects.length; i++) {
        this.effects[i].pause()
      }
    }
  }

  resume(): void {
    if (this._active && this._isPaused) {
      this._isPaused = false
      if (this.scopes) {
        for (let i = 0; i < this.scopes.length; i++) {
          this.scopes[i].resume()
        }
      }
      for (let i = 0; i < this.effects.length; i++) {
        this.effects[i].resume()
      }
    }
  }

4. 执行函数上下文

  • run(fn):在当前作用域环境下执行函数。
  • 内部会切换 activeEffectScope,保证新副作用挂到正确的 scope。
  run<T>(fn: () => T): T | undefined {
    if (this._active) {
      const currentEffectScope = activeEffectScope
      try {
        activeEffectScope = this
        return fn()
      } finally {
        activeEffectScope = currentEffectScope
      }
    } else if (__DEV__) {
      warn(`cannot run an inactive effect scope.`)
    }
  }

5. 手动 on/off 控制

  • on():进入 scope,记录之前的 scope。
  • off():退出 scope,还原到上一个 scope。
  prevScope: EffectScope | undefined

  on(): void {
    if (++this._on === 1) {
      this.prevScope = activeEffectScope
      activeEffectScope = this
    }
  }

  off(): void {
    if (this._on > 0 && --this._on === 0) {
      activeEffectScope = this.prevScope
      this.prevScope = undefined
    }
  }

6. 停止(销毁)

  • stop():彻底销毁本 scope。

    • 停止所有副作用。
    • 调用清理回调。
    • 停止所有子 scope。
    • 从父作用域中移除自己,避免内存泄漏。
  stop(fromParent?: boolean): void {
    if (this._active) {
      this._active = false
      for (let i = 0; i < this.effects.length; i++) {
        this.effects[i].stop()
      }
      this.effects.length = 0

      for (let i = 0; i < this.cleanups.length; i++) {
        this.cleanups[i]()
      }
      this.cleanups.length = 0

      if (this.scopes) {
        for (let i = 0; i < this.scopes.length; i++) {
          this.scopes[i].stop(true)
        }
        this.scopes.length = 0
      }

      if (!this.detached && this.parent && !fromParent) {
        const last = this.parent.scopes!.pop()
        if (last && last !== this) {
          this.parent.scopes![this.index!] = last
          last.index = this.index!
        }
      }
      this.parent = undefined
    }
  }
}

7. 工具函数

  • effectScope(detached):创建新的作用域。
  • getCurrentScope():获取当前活跃作用域。
  • onScopeDispose(fn):在当前作用域注册清理函数。
export function effectScope(detached?: boolean): EffectScope {
  return new EffectScope(detached)
}

export function getCurrentScope(): EffectScope | undefined {
  return activeEffectScope
}

export function onScopeDispose(fn: () => void, failSilently = false): void {
  if (activeEffectScope) {
    activeEffectScope.cleanups.push(fn)
  } else if (__DEV__ && !failSilently) {
    warn(
      `onScopeDispose() is called when there is no active effect scope` +
        ` to be associated with.`,
    )
  }
}

使用示例:简化版 effectScope

在 Vue3 中,setup() 里创建的副作用(watch/computed)会自动挂到组件的 scope 上。这里我们用手动的 effectScope 来演示:

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

const scope = effectScope()

scope.run(() => {
  const count = ref(0)

  // 这个 watch 会被 scope 收集
  watch(count, (newVal) => {
    console.log('count changed:', newVal)
  })

  // 模拟修改
  count.value++
  count.value++
})

// 当我们不需要这个 scope 里的副作用时
scope.stop()
// 此时 watch 会被自动清理,不会再触发

✅ 这样一篇文章逻辑清晰,最后有示例,能从源码理解到实际应用。

本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。

Vue2 动态添加属性导致页面不更新的原因与解决方案

作者 excel
2025年10月6日 07:20

在 Vue2 开发中,经常会遇到这样一个问题:对象新增属性后,数据虽然更新了,但页面并没有随之更新。本文将通过一个例子来说明原因,并给出解决方案。


一、问题示例

我们先来看一个简单的例子:

<div id="app">
  <p v-for="(value, key) in item" :key="key">
    {{ value }}
  </p>
  <button @click="addProperty">动态添加新属性</button>
</div>

Vue 实例代码如下:

const app = new Vue({
  el: "#app",
  data: () => ({
    item: {
      oldProperty: "旧属性"
    }
  }),
  methods: {
    addProperty() {
      this.item.newProperty = "新属性"; // 动态添加新属性
      console.log(this.item); // 数据确实更新了
    }
  }
});

点击按钮后,console 能打印出带有 newProperty 的对象,但页面上并没有新增一行。


二、原理分析

为什么会出现这种情况?

Vue2 的响应式系统是基于 Object.defineProperty 实现的。

const obj = {};
let val = "初始值";

Object.defineProperty(obj, "foo", {
  get() {
    console.log(`get foo: ${val}`);
    return val;
  },
  set(newVal) {
    if (newVal !== val) {
      console.log(`set foo: ${newVal}`);
      val = newVal;
    }
  }
});

当访问或修改 foo 时,会触发 getter/setter。但如果直接新增属性:

obj.bar = "新属性";

此时 bar 并没有通过 Object.defineProperty 设置拦截,因此不是响应式属性。
这正是 Vue2 无法检测到新属性的原因。


三、解决方案

Vue2 不允许在已创建的实例上直接新增响应式属性,但我们有几种方式来解决:

1. 使用 Vue.set()

Vue.set(this.item, "newProperty", "新属性");

这样 Vue 会调用内部的 defineReactive,为新属性建立响应式绑定,并触发视图更新。

简化源码逻辑如下:

function set(target, key, val) {
  defineReactive(target, key, val);
  dep.notify(); // 通知视图更新
  return val;
}

2. 使用 Object.assign()

直接使用 Object.assign() 并不能触发更新,但如果创建一个新对象赋值给原对象,就可以:

this.item = Object.assign({}, this.item, { newProperty: "新属性" });

这种方式适合一次性添加多个属性。


3. 使用 $forceUpdate()

强制让 Vue 实例重新渲染:

this.item.newProperty = "新属性";
this.$forceUpdate();

不过,这种方式不推荐,因为大多数情况下说明代码结构存在问题。


四、小结

  • 少量新增属性 → 使用 Vue.set()
  • 大量新增属性 → 使用 Object.assign() 创建新对象。
  • 临时解决方案 → 使用 $forceUpdate() 强制刷新(不建议)。

需要注意的是:Vue3 使用 Proxy 实现响应式,可以直接动态添加属性,依然能触发更新,不再存在这个问题。

前端读取文件夹并通过 SSH 上传:完整实现方案 ✅

作者 excel
2025年10月5日 22:15

在 Web 应用中,除了单文件上传,很多时候我们还需要用户直接选择整个文件夹,并批量上传到远程服务器。典型场景包括:静态资源部署、文档归档、远程备份等。本文整合了 前端文件夹选择方案(webkitdirectory + File System Access API)Node.js + node-ssh 后端上传,实现端到端的完整流程。


前端部分:选择文件夹并上传

前端的目标是让用户选择目录,遍历其中所有文件,并逐一上传到后端。

方案一:webkitdirectory

这是目前兼容度最好的方式,Chrome、Edge、Safari 均支持。

<input type="file" id="folder" webkitdirectory multiple>
<script>
  document.getElementById('folder').addEventListener('change', async (event) => {
    const files = event.target.files;

    for (const file of files) {
      console.log(file.webkitRelativePath, file.name);

      const formData = new FormData();
      formData.append("file", file, file.webkitRelativePath);

      await fetch("http://localhost:3000/upload", {
        method: "POST",
        body: formData
      });
    }
  });
</script>

优点

  • ✅ 兼容度较好,支持主流浏览器。
  • ✅ 能保留相对路径,方便后端还原目录结构。

缺点

  • ❌ Firefox 支持度差。
  • ❌ 必须通过 <input> 触发,不够灵活。

方案二:File System Access API

这是现代浏览器支持的新特性,可以通过 JS 调用目录选择器。

<script>
async function readDir() {
  const dirHandle = await window.showDirectoryPicker();

  for await (const entry of dirHandle.values()) {
    if (entry.kind === "file") {
      const file = await entry.getFile();
      console.log(entry.name);

      const formData = new FormData();
      formData.append("file", file, entry.name);

      await fetch("http://localhost:3000/upload", {
        method: "POST",
        body: formData
      });
    }
  }
}
</script>

<button onclick="readDir()">选择文件夹并上传</button>

优点

  • ✅ 灵活,可结合按钮、拖拽交互。
  • ✅ 支持读写权限,可操作文件。

缺点

  • ❌ 兼容性差,仅 Chromium 内核浏览器支持(Chrome、Edge)。
  • ❌ 需要用户手动授权目录访问。

Node.js 后端:接收并通过 SSH 上传

后端主要任务:接收前端传输的文件 → 缓存到本地 → 通过 SSH 上传到远程服务器 → 删除缓存。

安装依赖

npm install express multer node-ssh

完整后端代码

const express = require('express');
const multer = require('multer');
const { NodeSSH } = require('node-ssh');
const path = require('path');
const fs = require('fs');

const app = express();
const upload = multer({ dest: 'uploads/' });
const ssh = new NodeSSH();

// SSH 配置
const sshConfig = {
  host: 'your-server-ip',
  username: 'your-username',
  privateKey: '/path/to/private/key' // 或 password
};

// 接收文件上传
app.post('/upload', upload.single('file'), async (req, res) => {
  try {
    const localPath = req.file.path;
    const remotePath = path.join('/remote/dir', req.file.originalname);

    await ssh.connect(sshConfig);
    await ssh.putFile(localPath, remotePath);

    fs.unlinkSync(localPath); // 删除本地缓存文件

    res.send('上传成功');
  } catch (err) {
    console.error(err);
    res.status(500).send('上传失败');
  }
});

app.listen(3000, () => {
  console.log('服务已启动: http://localhost:3000');
});

工作流程

  1. 前端选择目录,逐个文件打包成 FormData
  2. 文件 POST 到后端 /upload
  3. 后端存储临时文件,再通过 node-ssh 传输到远程服务器。
  4. 删除临时文件,完成上传。

优缺点对比

前端

  • webkitdirectory:兼容更好,但交互有限。
  • File System Access API:交互灵活,但兼容性差。

后端

  • 采用 express + multer,简单易用。
  • 借助 node-ssh,实现自动化远程部署。

总结

  • 如果项目需要支持更多浏览器,推荐 webkitdirectory
  • 如果项目环境可控,推荐 File System Access API,更灵活。
  • 后端使用 Node.js + node-ssh,能快速实现从前端到服务器的自动化传输。

该方案特别适合 批量文件上传、静态资源部署、远程备份 等场景。

❌
❌