我把Vue2响应式源码从头到尾啃了一遍,这是整理笔记

Vue 2 响应式源码精读:从 initState 到 defineReactive

之前看 Vue 源码的时候,状态初始化这块一直是一知半解的状态,后来硬着头皮一行行啃下来,发现其实逻辑很清晰。这篇就把 initState、initProps、initData、proxy、observe、Observer、defineReactive 这几个核心函数串起来讲,争取让读完的人都能在脑子里画出整条链路。


一、initState ------ 所有状态的"总调度"

initState 这个函数做的事情说白了就是:把 Vue 实例上的 props、methods、data、computed、watch 统统初始化一遍,变成响应式数据。

ts 复制代码
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options

  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)

  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }

  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

拆开看:

  • vm._watchers = [] ------ 先准备一个数组,后面所有 Watcher(computed、watch、渲染 watcher)都会塞进去
  • const opts = vm.$options ------ 就是你 new Vue({ ... }) 传进来的配置对象,取出来方便后面用
  • 后面就是按顺序依次初始化:props → methods → data → computed → watch

这个顺序不是随便排的。 props 先初始化,所以 data 里能访问 props;methods 第二,所以 data 里能调 methods;computed 第四,所以它能依赖 data 和 props;watch 最后,所以它能监听前面所有的数据。谁在前谁在后,是有依赖关系的。

data 那块有个细节:如果用户没写 data,Vue 会给一个空对象 {} 并调 observe,保证根实例一定有响应式数据。


二、initProps ------ 处理父组件传进来的数据

initProps 要干的事情:拿到父组件传的值 → 校验类型和默认值 → 变成响应式 → 代理到 this 上。

ts 复制代码
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent

  if (!isRoot) {
    toggleObserving(false)
  }

  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)

    // 省略了开发环境警告逻辑...

    defineReactive(props, key, value, () => {
      if (vm.$parent && !isUpdatingChildComponent) {
        warn(`Avoid mutating a prop directly...`)
      }
    })

    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }

  toggleObserving(true)
}

几个关键点:

1. propsData vs propsOptions

  • propsData 是父组件实际传过来的值,比如 <Child msg="hello"/> 中的 { msg: 'hello' }
  • propsOptions 是子组件声明的 props 配置,props: { msg: { type: String } }

2. toggleObserving(false) 是干嘛的?

非根组件会先关掉响应式转换开关。因为 props 的值来自父组件,父组件那边已经做过响应式处理了,子组件不需要再 observe 一遍,避免重复。

3. validateProp

这个函数负责校验:取父组件传入的值,没传就用默认值,检查类型对不对,执行自定义校验函数,最后返回合法值。

4. defineReactive 里的第四个参数

ts 复制代码
defineReactive(props, key, value, () => {
  if (vm.$parent && !isUpdatingChildComponent) {
    warn(`Avoid mutating a prop directly...`)
  }
})

这个箭头函数是自定义 setter,当你在子组件里直接改 props(this.msg = 'xxx')的时候会触发警告。这就是为什么 Vue 一直强调"不要在子组件里直接修改 props"------源码层面就给你拦着了。

5. proxy(vm, '_props', key)

让你能直接写 this.msg 而不是 this._props.msg,后面会单独讲 proxy 函数。


三、initData ------ 处理组件自身的数据

initData 的流程:拿到 data → 处理函数/对象 → 挂载到 vm._data → 校验重名 → 代理到 this → observe 变响应式。

ts 复制代码
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object...',
      vm
    )
  }

  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length

  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    if (props && hasOwn(props, key)) {
      warn(`The data property "${key}" is already declared as a prop.`, vm)
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }

  observe(data, true /* asRootData */)
}

几个要注意的地方:

1. 组件的 data 为什么必须是函数?

ts 复制代码
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}

这行就是答案。组件会被复用创建多个实例,如果 data 是对象,所有实例共享同一块内存,一个改了全跟着变。用函数的话每次 getData 都返回新对象,实例之间数据隔离。

2. 校验很严格

遍历 data 的每个 key,检查三件事:

  • 不能和 methods 重名(否则 this.xxx 不知道是取数据还是调方法)
  • 不能和 props 重名(props 优先级更高,重名会被覆盖)
  • 不能是 $_ 开头的保留字(Vue 内部属性用的)

3. 最后一步 observe(data, true)

把整个 data 对象递归地变成响应式,这是响应式的入口,后面会细讲。

对比一下 initProps 和 initData:

initProps initData
数据存哪 vm._props vm._data
怎么访问 this.xxx(代理) this.xxx(代理)
响应式方式 defineReactive 逐个属性 observe 整体递归
数据来源 父组件传入 组件自己定义
能不能改 子组件不能改 可以改

四、proxy ------ this.xxx 背后的"中间商"

这个函数特别短,但特别关键。它做的事情就一件:让你写 this.xxx 的时候,实际去访问 this._data.xxxthis._props.xxx

ts 复制代码
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

逻辑很直白:

  1. 先定义一个公用的属性描述符模板 sharedPropertyDefinition,不用每次都 new 一个,省内存
  2. 动态设置 getter:读 this.msg → 实际读 this._data.msg(或 this._props.msg
  3. 动态设置 setter:写 this.msg = 'hi' → 实际写 this._data.msg = 'hi'
  4. Object.defineProperty 把这个属性挂到 Vue 实例上

所以 this.xxx 本身不存任何数据,它就是一个"门把手",拧开之后通向 _data_props

Vue 这么设计有几个好处:

  • 写法简洁,不用到处写 this._data.xxx
  • 真实数据藏在内部,外部只暴露代理接口,内部怎么优化不影响用户代码
  • 不管是 data、props 还是 computed,用户都只需要 this.xxx 一种写法

五、observe ------ 响应式的"门卫"

observe 是响应式系统的入口函数,负责判断一个值需不需要、能不能变成响应式。

ts 复制代码
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }

  let ob: Observer | void

  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }

  if (asRootData && ob) {
    ob.vmCount++
  }

  return ob
}

分三步看:

第一步:过滤掉不需要处理的值

不是对象或者数组?直接 return。是 VNode(虚拟 DOM)?也 return。简单类型(string、number、boolean)不需要劫持。

第二步:检查是不是已经处理过了

__ob__ 是 Vue 给响应式对象加的隐藏标记。如果对象上已经有 __ob__,说明已经被 observe 过了,直接复用,不重复创建。这是个重要的性能优化。

第三步:满足五个条件才创建 Observer

ts 复制代码
shouldObserve &&              // 响应式开关是开着的
!isServerRendering() &&       // 不是服务端渲染
(Array.isArray(value) || isPlainObject(value)) && // 是对象或数组
Object.isExtensible(value) && // 没被 Object.freeze() 冻结
!value._isVue                 // 不是 Vue 实例本身

五个条件全满足,才会 new Observer(value),真正给数据穿上响应式外套。

最后 ob.vmCount++ 是给根数据打标记,后面组件销毁的时候会用到,跟内存回收有关。


六、Observer ------ 真正给数据装监控的"工程师"

observe 只是门卫,Observer 才是干活的人。

ts 复制代码
export class Observer {
  value: any
  dep: Dep
  vmCount: number

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0

    def(value, '__ob__', this)

    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

构造函数做了这些事:

1. this.dep = new Dep()

每个被监控的对象都有一个 Dep(依赖管理器),可以理解成一个"通讯录",记录哪些 Watcher 用了这个对象的数据。数据变了就翻通讯录通知。

2. def(value, '__ob__', this)

给数据打上 __ob__ 标记,值就是 Observer 实例本身。用了 def 函数(后面讲),所以这个属性是不可枚举的,for...in 遍历不到,不会污染用户数据。

3. 对象和数组走不同路线

这是 Vue 响应式里最容易考的点:

  • 对象 :调 walk,遍历所有属性,逐个调 defineReactive 给每个属性加 getter/setter
  • 数组 :重写原型上的 7 个变异方法(pushpopshiftunshiftsplicesortreverse),然后 observeArray 递归处理数组里的每一项

为什么数组要特殊处理?因为 Object.defineProperty 劫持不到数组下标的赋值操作(arr[0] = xxx 不会触发 setter),所以 Vue 只能通过重写那几个会修改数组的方法来"曲线救国"。

这也解释了两个经典面试题:

  • 为什么对象新增属性不响应? 因为 walk 只在初始化时遍历一次,后面加的属性没经过 defineReactive,没有 getter/setter。用 Vue.setthis.$set 就行。
  • 为什么数组下标赋值不响应? 因为 Observer 没有劫持数组下标,只有那 7 个重写方法能触发更新。用 spliceVue.set 替代。

七、def ------ 一个极简的工具函数

顺带提一下 def,因为上面用到了:

ts 复制代码
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

就是对 Object.defineProperty 的封装,默认不可枚举。Vue 内部用它来给对象加隐藏属性(比如 __ob__),不会出现在 for...inObject.keys() 里。


八、defineReactive ------ 响应式的核心加工厂

最后也是最核心的一个函数。defineReactive 的使命:给对象的某个属性劫持 get 和 set,实现"读的时候收集依赖,写的时候派发更新"。

ts 复制代码
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,

    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

这段代码值得拆细了看。

Getter:读数据的时候发生了什么

ts 复制代码
get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
    dep.depend()
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
  }
  return value
}

当你渲染模板、执行 computed 或 watch 的时候,会读到 this.xxx,就会触发这个 getter。

关键在 Dep.target。它指向当前正在执行的 Watcher(可能是渲染 Watcher、computed Watcher 或 watch Watcher)。如果 Dep.target 存在,说明"有人正在用这个数据",就调 dep.depend() 把这个 Watcher 记录下来。

如果值本身是对象或数组,还要递归地对子对象也收集依赖(childOb.dep.depend()),数组还要额外处理(dependArray)。

一句话:getter 负责"记住谁在用我"。

Setter:改数据的时候发生了什么

ts 复制代码
set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }
  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
  }
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal)
  dep.notify()
}

当你执行 this.xxx = 新值,触发 setter:

  1. 先拿旧值,跟新值比一下,一样就直接 returnNaN !== NaN 的特殊情况也处理了),这是性能优化
  2. 开发环境下如果有 customSetter 就调一下(比如 initProps 里传的那个"不要直接改 props"的警告)
  3. 赋新值
  4. 新值如果是对象/数组,也要 observe,保证新数据也是响应式的
  5. dep.notify() ------ 遍历之前收集的 Watcher 列表,逐个通知更新

一句话:setter 负责"通知所有用我的人,我变了"。

整个响应式闭环

画个简单的流程:


整条链路串起来

到这里,Vue 2 响应式初始化的完整链路就清楚了:

scss 复制代码
new Vue()
  → initState()
    → initProps()  → validateProp + defineReactive + proxy
    → initMethods()
    → initData()   → getData + 校验 + proxy + observe
    → initComputed()
    → initWatch()

proxy: this.xxx → this._data.xxx / this._props.xxx

observe: 判断要不要响应式 → new Observer()
  Observer:
    对象 → walk → defineReactive(给每个属性加 getter/setter)
    数组 → 重写 7 个变异方法 + observeArray 递归

defineReactive:
  get → dep.depend()(收集依赖)
  set → dep.notify()(派发更新)

每个函数各司其职,代码量不大但设计得很精巧。建议感兴趣的话对着源码自己走一遍,比看任何文章都管用。

相关推荐
吴声子夜歌3 小时前
Vue3——新语法
前端·javascript·vue.js
jiayong233 小时前
第 36 课:任务详情抽屉快捷改状态
开发语言·前端·javascript·vue.js·学习
琢磨先生TT3 小时前
为什么很多后台系统功能不少,看起来却还是很廉价?
前端·vue.js·设计
一 乐5 小时前
交通感知与车路协同系统|基于springboot + vue交通感知与车路协同系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·交通感知与车路协同系统
踩着两条虫6 小时前
强强联合!VTJ.PRO 正式接入 DeepSeek V4,AI 编码能力再跃升
前端·vue.js·ai编程
Beginner x_u6 小时前
前端八股整理(Vue 02)|组件通信、生命周期、v-if 与 v-show
前端·javascript·vue.js
万物得其道者成7 小时前
Vue3 使用 Notification 浏览器通知,解决页面关闭后旧通知点击无法跳转问题
前端·vue.js·edge浏览器
一條狗7 小时前
学习日报 20260423|[特殊字符] 深度解析:Vue 3 SPA 部署到 Spring Boot 的 404/500 错误排查与完美解决方案-2
vue.js·spring boot·学习
LIO8 小时前
Vue 3 实战——搜索框检索高亮的优雅实现
前端·vue.js