响应式对象
大家都知道Vue.js实现响应式是用Object.defineProperty函数,那么我们通过下面例子看看怎么变成了响应式对象。
const app = new Vue({
el: '#demo',
template: `
<div>{{message}}</div>
`,
data: {
message: 'hello Vue!',
},
watch: {
'message': function(val, oldVal) {
console.log(val, oldVal)
}
},
mounted: function () {
this.message = 'hello world!'
},
})
app.$mount()
在Vue初始化阶段,我们重点分析一下data属性的处理。
initData
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// proxy data on instance
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
// observe data
observe(data, true /* asRootData */)
}
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)
}
initData函数做了2件事情
- 通过
proxy函数,将data的属性代理到vm实例上,所以我们可以通过this.message访问到data上的属性。 observe(data)
observe函数new了一个Observer实例
observe
export function observe (value: any, asRootData: ?boolean): Observer | void {
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
Observer构造函数做了2件事:
def函数给value对象添加了__ob__实例- 遍历value调用
defineReactive函数,给我们value对象添加上响应式
我们分析下defineReactive源码,是如何给data函数返回的对象,添加上响应式的
defineReactive
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
// cater for pre-defined getter/setters
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 (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
data函数返回的对象,我们通过defineReactive函数遍历,给对象上的每个属性添加上getter,setter方法,使对象变成一个响应式的对象,如果有子对象,通过observe方法递归子对象,使无论多么复杂的对象,我们访问data的属性,都能触发到getter和setter。
每个key都创建了dep实例,在getter和setter函数里面用来触发和收集依赖,如何触发和收集依赖,下面来讲。
收集依赖和派发更新
在了解收集依赖和派发更新之前,根据我们的例子,先问自己几个问题:
- 什么时候创建
dep实例? - 什么时候创建
watcher实例? dep实例和watcher实例之间的逻辑关系是什么?
如果能把这几个问题解释清楚,我们对响应式的了解就比较清楚了。
第一个问题,什么时候创建了dep实例
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
...
if (Dep.target) {
dep.depend()
}
},
set: function reactiveSetter (newVal) {
...
dep.notify()
}
})
}
当我们将对象变成响应式时,会给对象的每个属性创建一个dep实例,同时利用闭包的方法,在getter和setter方法中访问dep实例。
根据我们的例子,message属性拥有一个dep(id是3)实例。
第二个问题,什么时候创建了watcher实例
根据我们的例子:
watch: {
'message': function(val, oldVal) {
console.log(val, oldVal)
}
},
我们定义的watch监听,会创建一个watcher(id是1)的实例。如下图:
当我们
app.$mounted()时,会调用mountComponent函数。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
return vm
}
我们先说下这个函数的作用
new Watcher会执行updateCompontent函数vm._render()会生成渲染的VNode,在这个过程中触发vm上的数据访问,触发数据对象的gettervm._update()将dom树更新为新的节点
根据我们的例子,我们就创建出来一个监控template模板的watcher(id是2)实例
我们梳理一下逻辑,我们创建了2个watcher实例,针对message属性,会触发两次getter函数,对应的dep收集两次依赖。
dep和watcher之间的逻辑关系
我们先看下简化后的Dep和Watcher源码:
Dep
Dep类比较简单,有一个容器subs数组,可以添加和移除watcher实例,depend、notify和Watcher类一起讲。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher
export default class Watcher {
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
...
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
this.value = this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
popTarget()
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)
}
}
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
this.run()
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
// set new value
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
}
首先根据我们我们监听watch这个例子来讲
watch: {
'message': function(val, oldVal) {
console.log(val, oldVal)
}
},
在new watcher的时候,会调用watcher(id是1)的get方法
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
popTarget()
return value
}
pushTarget(this)和popTarget()函数,这两个函数在Dep类中定义
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
...
}
}
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
根据我们的例子
Dep.target等于watcher(id是1)实例。value = this.getter.call(vm, vm)触发message的getter方法。message属性的dep(id是3)实例调用depend函数Dep.target(watcher(id是1)的实例)调用addDep函数dep(id是3)将watcher(id是1)添加到subs数组中
最终的结果如下
在根据template中对message的调用分析watcher的过程
template: `
<div>{{message}}</div>
`,
在mountComponent函数中,new watcher的时候,会调用watcher(id是2)的get方法
过程同上:
Dep.target等于watcher(id是2)实例。value = this.getter.call(vm, vm)触发message的getter方法。dep(id是3)实例调用depend函数Dep.target(watcher(id是2)的实例)调用addDep函数dep(id是3)将watcher(id是2)添加到subs数组中
结果如下图:
这样,针对message属性,我们对应的dep实例(id是3)的依赖已经收集完成。
当在mounted修改message属性的时候,触发message的setter函数。
mounted: function () {
this.message = 'hello world!'
},
根据我们的例子:
message的dep(id是3)实例,会将subs数组中存的2个watcher实例,调用自身的update函数。接着会执行watcher的cb函数和更新template的视图。
总结
首先我们将data函数返回的对象变成一个响应式对象,对每个属性创建一个dep实例容器,针对watcher,computed和mount分别创建watcher实例,在getter函数中,将watcher收集到对应dep容器中,修改当前属性时,触发setter函数,将dep容器中的所有watcher进行更新。