Vue的响应式原理
Vue数据响应式原理的解读有很多文章,此文只分享我对于Observer,Dep,Watcher的实现理解,希望此文对刚接触Vue的开发者有所帮助。
Observer、Dep、Watcher简介
- Observer:数据的观察者,让数据对象的读写操作都处于自己的监管之下。当初始化实例的时候,会递归遍历data,用Object.defineProperty来拦截数据。
- Dep:数据更新的发布者,get数据的时候,收集订阅者,触发Watcher的依赖收集,set数据时发布更新,通知Watcher。
- Watcher:数据更新的订阅者,get数据的时候,收集发布者,订阅的数据改变时执行相应的回调函数。
Observer
在Vue实例化的过程中会执行initState()和initData(),initData()中会执行observe(data, true /* asRootData */)。点击此处查看initData代码。此处为observer代码.
/**
* 执行var vm = new Vue() 时,会执行observe(data)
* 没有被监听的data需要New Observer()观察;
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果存在__ob__属性,说明对象不是observer类,需要new Observer()
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
// vmCount是用来记录此Vue实例被使用的次数
ob.vmCount++
}
return ob
}
// observe执行后,不是observer类的执行new Observer
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data 记录被使用的次数
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// data是数组执行observeArray,是对象执行walk
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
// 获取对象的属性数组
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 执行 defineReactive(); params 依次是 data,key,value
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/**
* Observe a list of Array items.
* 对于数组继续执行observe(),对数组深入Observe.此时数组有对应的dep
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
/**
* 执行observer后,value是对象在执行defineReactive(data,key,value)
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 指定对象上一个自有属性对应的属性描述符 (value,get,set,writable[属性值是否可改],configurable[属性描述是否可改],enumerable[对象属性是否可枚举])
const property = Object.getOwnPropertyDescriptor(obj, key)
// 对象的属性描述不可更改时,返回
if (property && property.configurable === false) {
return
}
// 获取getter和setter
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
// 属性值是对象或数组会进一步 defineReactive,不停深入建立observe
let childOb = !shallow && observe(val)
// 修改对象属性的属性描述符
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// get 主要是订阅者和发布者互相收集的过程
get: function reactiveGetter () {
// get 触发执行
const value = getter ? getter.call(obj) : val
// 此时Dep.target指向触发get的watch,下文中描述
if (Dep.target) {
// 触发get的watch收集dep发布者
dep.depend()
// 属性值是数组或对象时,会执行observe,执行observe会有dep属性,其实就是使触发get的watch收集child的dep
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// set就比较好理解了
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 属性值修改后,可能修改为对象或数组,如果是就要建立observe观察者,如果相反的话,就取消observe观察者
childOb = !shallow && observe(newVal)
// 发布者通知收集的订阅者更新数据
dep.notify()
}
})
}
上述代码执行 var vm = new Vue() --> observe(data) --> observer() --> defineReactive() 过程中可以分析出data不停的深入遍历,直到不能建立观察者为之,给data和每个深入遍历的属性和对应属性值为对象或数组的建立观察者。从set可以看出属性值改变会触发dep.notify()通知订阅者更新数据。数据改变除了属性值改变还有对象增删属性和数组修改,这就是为什么要给每个对象和数组建立观察者了.请看下述代码:
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any {
//对于数组的处理,调用变异方法splice,这个时候数组的Dep会发布更新消息
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 下述链接的代码是给数组的常用操作添加数组Dep发布通知
// https://github.com/vuejs/vue/blob/v2.5.13/src/core/observer/array.js
target.splice(key, 1, val)
return val
}
// 改变的是已经存在的属性的属性值
if (key in target && !(key in Object.prototype)) {
// 触发该属性对应的dep发布订阅者更新数据
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
// 给新增属性创建观察者,用于收集订阅者
defineReactive(ob.value, key, val)
// 对象的发布者通知订阅者更新数据,新增属性的发布者收集订阅者
ob.dep.notify()
return val
}
/**
* Delete a property and trigger change if necessary.
*/
export function del (target: Array<any> | Object, key: any) {
//对于数组的处理,调用变异方法splice,这个时候数组的Dep会发布更新消息
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
// 删除对象的属性
delete target[key]
if (!ob) {
return
}
// 对象的发布者通知订阅者更新数据
ob.dep.notify()
}
/**
* Collect dependencies on array elements when the array is touched, since
* we cannot intercept array element access like property getters.
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
1. 对象新增或删除属性时,是监控不到的。因为开始oberve data的时候已经建立了getter,setter和发布者,后面属性新增和删除是无法检测到的。所以Vue给出了Vue.set和Vue.delete API。 当增删属性时或数组增删元素时,就会触发ob.dep.notify()通知订阅者更新数据
2. 对于数组也可以通过Vue.set和Vue.delete API来修改,也可以调用数组的变异方法(push(),pop(),shift(),unshift(),splice(),sort(),reverse()),这些方法是会让数组的值发生改变的,具体代码请点击查看
所以dep实例化的地方有两处:
- 一处是在defineReactive函数里,每次调用这个函数的时候都会创建一个新的Dep实例,存在在getter/setter闭包函数的作用域链上,是为对象属性服务的。在Watcher获取属性的值的时候收集订阅者,在设置属性值的时候发布更新。
- 另一处是在observe函数中,此时的dep挂在被observe的数据的__ obj__属性上,他是为对象或数组服务的,在Watcher获取属性的值的时候,如果值被observe后返回observer对象(对象和数组才会返回observer),那么就会在此时收集订阅者,在对象或数组增删元素时调用$set等api时发布更新的;
Dep
以下为Dep的代码,github地址为点击查看。
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
// subs用于收集订阅者(watcher)
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 添加订阅者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除订阅者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 订阅者收集发布者,并添加订阅者(添加订阅者是在watcher中调用dep的addSub方法,在watcher中会提到)
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 发布通知,通知subs收集的订阅者更新数据
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// Dep.target指向当前的watcher
Dep.target = null
// 以下为watcher中使用
// 存放watcher的
const targetStack = []
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
在上文中我们提到Dep实例化的地方有两处:
- 一处是在defineReactive函数里,是为对象属性服务的,通过在get中childOb.dep.depend()收集订阅者,通过在dep.notify()发布更新。
- 另一处是在observe函数中,是为对象或数组服务的,通过在get中childOb.dep.depend()收集订阅者,通过在set,del和数组方法原型上ob.dep.notify() 发布更新。
Watcher
以下为Watcher的代码,github地址为点击查看。
/**
* vm的$watch方法
* Vue.prototype.$watch()
* new Watcher(vm, expOrFn, cb, options)
* 传参
* vm.$watch(userInfo, onUserInfoChange)
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
getter: Function;
value: any;
// $watch 的params vm,userInfo,onUserInfoChange
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
// 计算属性时生效
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
// getter有两种情况,一种是function,例如挂载之后,获取模板中的值,此时是一个函数,get中会触发getter,一种是表达式,同时get也会触发getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// 不是computed计算属性时,都会调用get函数
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// 入栈,此时Dep.target指向该watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 触发getter,这时对应的dep会触发depend函数,depend函数触发watcher的addDep函数,addDep收集完发布者后,调用发布者的addSub函数,使发布者收集订阅者,完成收集过程。
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// 出栈
popTarget()
// 更新deps和depIds,即去除不再依赖的发布者
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
* 添加发布者
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
* 发布者发布通知后更新数据,最终都会调用run方法
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
// 触发getter,订阅者和发布者相互重新收集
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
// 调用表达式更新值
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
watcher初始化的时候会获取getter函数,在执行get时,触发了getter拦截,促使收集Dep和对应的Dep收集watch,完成了相互收集的过程。当发布者通知watcher更新数据,执行update函数更新数据。
watcher中的deps和newDeps。他们是用来记录已经记录Watcher收集的依赖和新一轮Watcher收集的依赖,每一次有数据的更新都需要重新收集依赖,上述说到数据发布更新后,会调用Dep的notify方法,notify方法会调用update,update调用run方法,run方法会调用get方法,重新获取值,并重新收集依赖。收集完之后cleanupDeps函数,用于更新新的依赖。deps和newDeps作对比之前收集了但是新一轮没收集,会执行对应dep的removeSub函数,使发布者删除watcher,因为两者不存在依赖关系了,这样下次发布者执行notify方法时不会再通知该watcher。
这里可以看出Dep.target是全局唯一的,指向watcher,用于记录当前的watcher。
数据响应式原理实现了数据到视图的更新,而视图到数据的更新,其实就是给表单元素(input等)添加了change等事件监听,来动态修改model和 view。