Vue.js 2.X版本中响应式的核心在于,使用了Object.defineProperty来进行依赖的收集与触发。
Object.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。其语法是:
Object.defineProperty(obj, prop, descriptor)
obj是要在其上定义属性的对象。prop是要定义或修改的属性的名称。其中核心的是descriptor中的get和set。get是一个给属性提供getter方法,在访问该属性时会触发,set是一个给属性提供的setter方法,在对该属性进行修改的时候触发。
Vue.js 正是利用了getter与setter方法将对象变成了响应式对象。
我们从Vue初始化执行的_init方法开始。
Vue.prototype._init = function (options?: Object) {
// ...
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
构建响应式对象
在Vue初始化阶段,执行了initState(vm)方法,首先判断data是否存在,如果存在,会执行initData(vm)方法。
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
initData(vm)方法如下:
function initData (vm) {
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:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
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)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
首先进行data类型的判断并返回data的对象结果,接着进行data对象中key值的校验,并执行proxy(vm,_data, key)方法将data数据字段代理到实例vm对象上。最后,执行了observe(data, true /* asRootData */),通过调用该函数,正式开始将data数据对象转变为响应式数据对象。
export function observe (value, asRootData) {
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
}
如果检测对象自身含有__ob__属性,并且该属性是Observer的实例时,直接将该属性的值作为ob的值,该判断是用于避免重复观测一个数据对象。否则,在满足一定的条件下,去实例化一个Observer对象实例。
Observer构造函数
真正将数据对象转换成响应式数据的是Observer函数。
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
// ...
}
walk (obj: Object) {
// ...
}
observeArray (items: Array<any>) {
// ...
}
}
Observer类的实例拥有三个实例属性,分别是value、dep和vmCount,还有两个实例方法walk和observeArray。
__ob__属性
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
实例对象的dep属性,保存了一个Dep实例对象。它的作用是一个收集依赖的集合。
在初始化了三个实例属性之后,执行了def函数,为该数据对象添加了一个我们前面提到的__ob__属性,该属性的值就是当前的实例对象。
假设有如下数据对象:
const data = {
a: 1
}
经过def方法之后,data会变成:
const data = {
a: 1,
__ob__: {
value: data,
dep: dep对象,
vmCount: 0
}
}
接下去是对于value类型的判断,先不看value是数组的情况,当value是一个纯对象的时候,执行 this.walk(value)。
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
walk函数的代码也较为简单,获取数据对象所有可枚举的属性后,为每个属性去调用defineReactive方法。
defineReactive函数
defineReactive 的作用就是定义一个响应式的对象,给对象动态添加getter和setter。
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
}
// 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 () {
// ...
},
set: function reactiveSetter (newVal) {
// ...
}
})
}
defineReactive 函数一开始初始化了一个Dep对象实例,接着缓存了obj的属性描述符property,并且对于obj结构复杂的情况下,通过childObj递归调用observe方法,这样使得在放在obj中深层嵌套的属性时,也能够正确触发getter和setter。
到这里,通过defineReactive 给数据添加了getter和setter,其中,getter用于收集依赖,setter用于触发依赖更新。
依赖收集与更新通知
依赖收集 get
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
}
可以观察到get方法中通过闭包引用这一开始初始化的dep实例对象,通过执行dep.depend()将依赖收集到dep集合中,这里的Dep.target实际上就是依赖本身。
Dep
Dep是依赖收集的核心所在。
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 () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep实际上是对于Watcher的管理,其中静态属性target是全局唯一的Watcher。
Watcher
Dep的作用实际上是对于Watcher的管理,因此有必要先了解Watcher的实现。
首先,我们要找到创建Watcher实例的位置。我们回过头去看Vue初始化执行的_init方法,方法的最后一段为:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
执行完initState方法,创建了响应式对象之后,可以观察到最后执行了vm.$mount(vm.$options.el),这一步是将组件挂在到指定的元素上。
当我们查找$mount的定义后,发现其实际上是调用了mountComponent函数。在其定义的代码中可以看到Watcher的初始化代码:
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
其中Watcher的第二个参数updateComponent ,其定义如下:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
该函数最终是调用了vm._update()函数。以上两个函数的作用可以简单理解为:
vm._render()调用了render方法返回了VNodevm._update()的作用是把生成的VNode渲染为真正的DOM
到这一步,我们可以找到了Watcher实例化的位置,并且了解了Watcher中第二个参数的作用在于将VNode渲染为真正的DOM。这时候我们来观察Watcher的constructor:
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 省略...
this.value = this.lazy
? undefined
: this.get()
}
}
代码的最后一段中,除了计算属性之外的实例对象都会调用this.get()方法。
get () {
pushTarget(this)
let value
const vm = this.vm
try {
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()
this.cleanupDeps()
}
return value
}
首先执行的pushTarget(this)会将当前实例赋值给上文提到的Dep类静态属性target,而这个实例对象正是要被收集的目标。
接着定义了value变量,其值为this.getter函数的返回值,该函数我们通过上述constructor中的定义可以看到,正是实例化Watcher对象的第二个参数。
收集过程
由于触发了this.getter函数,对检测目标求值的过程同时也触发了响应式对象中的get拦截器。我们再回过头看defineReactive 函数:
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
}
此时上述的get方法将被执行,Dep.target属性为Watcher的实例对象。这时候接着执行dep.depend()方法。
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
这里实际上是触发了Watcher的addDep实例方法:
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)
}
}
}
addSub方法如下定义:
addSub (sub: Watcher) {
this.subs.push(sub)
}
这里做出的逻辑判断在于保证同一个数据不会被添加多次,执行的dep.addSub(this)方法便是把当前的Watch订阅到dep集合中的subs中,由此完成了依赖收集的过程。
触发依赖 set
我们首先来看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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
首先进行对于新值的判断并赋值,最后执行的childOb = !shallow && observe(newVal)是考虑了如果newVal是一个对象的情况,需要对其变成响应式对象。最后执行dep.notify()进行派发更新,通知所有的订阅者。
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
这段逻辑较为简单,遍历subs合集,依次调用每个Watcher实例的update方法。
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
在一般组件数据更新的场景,会走到最后一个 queueWatcher(this) 方法,这个方法中引入了队列的概念,是针对Vue每次做通知更新时的优化。它的思路是将这些Watcher先添加到一个队列中,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数。
run () {
if (this.active) {
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)
}
}
}
}
首先执行的const value = this.get(),重新求值等价于重新执行渲染函数,最终的结果是重新生成了虚拟DOM并更新了真正的DOM,这样就是完成了重新渲染的过程。
总结
我们最终可以用一张原理图来表示这整个过程:
在初始化阶段执行了_init方法,首先通过initState方法构建了响应式对象,为目标数据分别添加了getter与setter方法。
在实例挂载的阶段,初始化了Watcher实例对象,执行了渲染函数。在渲染VNode时会对目标数据进行访问,由此触发了数据对象的getter,这时依赖被收集。
在目标数据被修改时,触发了setter方法,数据层修改后会触发dep.notify方法派发更新通知,在调用Watcher实例方法update后,将这些Watcher先添加到一个队列中,在 nextTick 后执行所有 watcher 的 run方法,最后通过借用重新求值方法重新调用渲染函数,完成新的DOM层的更新。