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.xxx 或 this._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)
}
逻辑很直白:
- 先定义一个公用的属性描述符模板
sharedPropertyDefinition,不用每次都 new 一个,省内存 - 动态设置 getter:读
this.msg→ 实际读this._data.msg(或this._props.msg) - 动态设置 setter:写
this.msg = 'hi'→ 实际写this._data.msg = 'hi' - 用
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 个变异方法(
push、pop、shift、unshift、splice、sort、reverse),然后observeArray递归处理数组里的每一项
为什么数组要特殊处理?因为 Object.defineProperty 劫持不到数组下标的赋值操作(arr[0] = xxx 不会触发 setter),所以 Vue 只能通过重写那几个会修改数组的方法来"曲线救国"。
这也解释了两个经典面试题:
- 为什么对象新增属性不响应? 因为
walk只在初始化时遍历一次,后面加的属性没经过defineReactive,没有 getter/setter。用Vue.set或this.$set就行。 - 为什么数组下标赋值不响应? 因为 Observer 没有劫持数组下标,只有那 7 个重写方法能触发更新。用
splice或Vue.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...in 和 Object.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:
- 先拿旧值,跟新值比一下,一样就直接 return (
NaN !== NaN的特殊情况也处理了),这是性能优化 - 开发环境下如果有 customSetter 就调一下(比如 initProps 里传的那个"不要直接改 props"的警告)
- 赋新值
- 新值如果是对象/数组,也要 observe,保证新数据也是响应式的
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()(派发更新)
每个函数各司其职,代码量不大但设计得很精巧。建议感兴趣的话对着源码自己走一遍,比看任何文章都管用。