响应式原理源码分析
上一章我们通过从零构建了一个极简响应式系统后,对响应式系统中的Dep类、Watcher类和defineReactive方法都有了一定的了解。这一章我们将会结合源码来看看Vue到底是如何实现响应式系统的,以及还有哪些细节需要我们注意和优化。
这一章主要分为以下几个模块:
observe方法的作用与实现。Observer类的实现。defineReactive方法做了哪些额外处理?- 拦截数组变异方法 -
Vue是如果处理数组的监听的? $set/$del的实现。Dep类的实现。Watcher类的实现。
observe方法
上一章我们已经提到了defineReactive方法是可以将对象的某一个key转换成响应式。但是object.defineProperty是无法监测到对象和数组的变化的。
因此,为了解决这个问题,Vue封装了一个方法,专门用于监测对象和数组:如果是对象,就循环遍历处理所有的key;如果是数组,就遍历每一个元素,对每一个元素进行响应式处理。下面我们看一下这个observe方法的具体实现。响应式的核心代码是在src/core/observer目录下,我们先打开该目录下的index.js文件,找到observe方法:
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 1. 如果不是对象或者是VNode则不监测
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
}第一步,先判断要监测的value是否是对象,如果不是则不需要监测。如果value是VNode,那么也不需要监测,因为虽然VNode中会使用数据,但是监测这种依赖关系没有意义。
// 1. 如果不是对象或者是VNode则不检测
if (!isObject(value) || value instanceof VNode) {
return
}第二步,判断value是否具有__ob__属性,如果存在且为Observer类的实例,说明该value已经被监测了,返回相应的监测对象即可。否则,说明value尚未被监测,继续进行判断,这里一共有5个判断条件:
// shouldObserve 用于控制是否数据需要监测,因为有的时候是不需要监测的
shouldObserve &&
// 服务端渲染也不需要监测
!isServerRendering() &&
// 只有数组或对象才会被监测
(Array.isArray(value) || isPlainObject(value)) &&
// 被 Object.seal 或 Object.freeze等处理过的对象不可被监测
Object.isExtensible(value) &&
// 如果是 Vue 实例则不会被监测
!value._isVue如果所有条件满足,那么就会实例化一个Observer类。可以看出,observe函数主要是做了一些判断,实际的响应式处理都在Observer类当中,后续我们会继续分析Observer类。
最后,如果是根数据的话,那么ob.vmCount会加1,这个有什么用呢?实际上从这里我们可以得出,如果是根数据被监测的话,那么ob.vmCount是大于0的,而Vue.set方法里进行监测时,会判断ob.vmCount存在时不会进行监测。因此Vue.set方法是无法将根数据处理成响应式。
if (asRootData && ob) {
ob.vmCount++
}Observer类
Observer类的定义在observe方法的上方,部分代码如下:
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()
// 当为根数据时,vmCount > 0,$set 方法不进行处理
this.vmCount = 0
// 在 value 上添加属性 __ob__
def(value, '__ob__', this)
// 1. 如果是数组,由于 Object.defineProperty 无法检验数据的变化,因此需要额外处理
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
// 2. 如果是对象,遍历对每个 key 进行响应式处理
this.walk(value)
}
}
}Observer实例化时主要做了这几项工作:
首先是记录下要监测的值,同时生成一个dep实例。注意,这里的dep不同于上一章我们提到的闭包里的dep,闭包里的dep是属于某一个字段的,而这里的dep是针对value这个对象的。当这个对象发生改变时,触发这个dep相关的Watcher更新。
接下来,value上被添加了__ob__属性,用于保存当前Observer实例,前面observe就是通过__ob__访问已经实例化好的Observer对象,达到不重复监测的目的。
最后,如果value是数组,则会调用observeArray方法,我们看一下该方法是怎样的:
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}这里不难发现,observeArray就是遍历value里的每一个元素,然后进行响应式处理。
当代码走到else分支时,代表value是一个对象,此时会执行walk方法:
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}walk方法比较简单,就是通过defineReactive方法对对象里的每个key做响应式处理。下面我们看一下defineReactive的实现。
defineReactive方法
defineReactive方法代码如下所示:
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
/* 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()
}
})
}可以看出,defineReactive方法和我们上一章实现的思路大致是一致的:在get的时候,使用dep.depend()来建立起Dep和Watcher之间的依赖关系,在set的时候通过dep.notify()来通知相应Watcher进行更新。
所以这里我们主要讨论一下不一样的地方。
首先,defineReactive在响应式处理前做了一层判断:
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}如果property.configurable为false的话,那么是defineProperty方法是无效的,因此不做任何处理。
接着,通过property获取了这个字段原有的get和set,并且缓存起来:
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)!shallow && observe(val)这段代码则表示当val值是对象时,需要进行递归监测,这样才能保证对象里任何的属性都是响应式的。
但是上面的这行代码(!getter || setter) && arguments.length === 2就比较难以理解了,这牵涉到Vue中的两个issue:#7302和#7828。我也纠结了比较长的时间,下面讲一下我的理解。
Vue原本处理对象时的walk方法是这样定义的:
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}defineReactive是传入三个参数的,也就是将对应的值也传进入了。这样做会有什么问题呢?我们知道,当获取obj[keys[i]]这个值的时候,实际上会调用keys[i]这个字段的get方法,如果用户自己定义了这个get方法,那么获取值时是无法预知用户会如何定义的。这里参考《Vue技术内幕》中的一句话。
Vue
之所以在深度观测之前不取值是因为属性原本的 getter 由用户定义,用户可能在 getter 中做任何意想不到的事情,这么做是出于避免引发不可预见行为的考虑。
所以,我们在深度监测之前,如果用户自己设置了get,那么我们就跳过监测,可以参考#7302。所以代码改成只传两个参数:
// walk 方法
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
// defineReactive 方法
const getter = property && property.get
const setter = property && property.set
if ((!getter && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)这样,如果传入的只有两个参数,也就是没有传value值时,如果用户设置了get,那么就不会执行val = obj[key]这行代码,因此此时val的值为undefined,那么observe(val)就不会进行监测了。
但是这样又会存在怎样的问题呢?如果对于某个字段没有get,我们会对这个字段的值(这里代称叫做value)进行深度监测。监测完后,我们使用了Object.defineProperty对这个字段添加了get和set方法,此时get又是存在的。如果改变value里的值,由于该字段的get是存在的,所以在递归监测时会跳过对value的深度监测。这就导致了前后不一致的情况,详情参考#7828。
为了解决这个问题,我们将判断方式改为,增加了setter判断:
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}这样,当getter和setter同时存在时,值一样会被深度监测。至此,这几行代码就解析完了,如果这里的解析存在偏差,恳请各位不吝赐教~
好了,我们继续看后面的代码,在get方法里:
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}如果childOb存在,说明value是对象或数组,且已经被监测,这个时候需要记录这个value与当前Watcher之间的关系。另外,如果值是数组的话,会执行dependArray方法:
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)
}
}
}这里value主要分为数组和对象两种情况:
- 如果
value是数组,那么会遍历数组,遍历的结果如果是对象或数组,那么e.__ob__存在,进行依赖搜集。如果是数组,重复该步骤。 - 如果
value是数组,遍历键值对,如果遍历的值为数组,重复上述步骤,如果是对象,重复本步骤
所以,本质上这段代码是递归处理了对象和数组的依赖搜集过程,这里需要慢慢理解一下递归的过程。
拦截数组变异方法
如果监测的value为数组时,这里会出现一种问题:如果数组新增了某个元素,那么实际上defineProperty是不会检测到这种数组的增删变化的,因此新增的元素也不能自动进行响应式处理了。
那么Vue又是如何处理这种情况的呢?让我们回到Observer类中,当value是数组时,做了以下处理:
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
}
function protoAugment (target, src: Object) {
target.__proto__ = src
}这里主要看一下protoAugment方法,它将value的__proto__指向了arrayMethods,我们看看arrayMethods是什么,打开./array.js文件:
const arrayProto = Array.prototype
// 继承于Array
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
// 缓存数组的原始方法
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
// 调用数组的原始方法
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果是增加了新的元素,那么需要将新增元素也进行响应式处理
if (inserted) ob.observeArray(inserted)
// notify change
// 数组改变了,通知相应 watcher 更新
ob.dep.notify()
return result
})
})首先,arrayMethods继承于Array,因此拥有数组相关的属性方法。然后,遍历数组中所有与增删操作相关的方法名,进行响应式处理,这样每次数组的增删方法时,都会触发这里的mutator函数。mutator函数主要做了以下三点工作:
- 执行数组原有的方法,保持原有输出不变。
- 如果执行的是增加相关的方法,如
push,unshift,splice,记录下增加了哪些元素,然后将这些元素也进行响应式处理。 - 因为此时能够”感受“到数组变化了,所以会通知数组相关的
watcher进行更新。
$set/$del的实现
现在还存在一个问题:如果我们为对象添加了一个属性,我们却不知道这个对象发生了变化,从而也就不能对这个属性自动进行响应式处理了。同样,如果我们对数组用索引来增加新值的时候,而不触发数组变异方法,也就无法感知变化了。
那么Vue是如何处理这一问题的呢?这里就要提到$set方法的实现了,它的目的就是为了解决设置新的属性不具备响应式的问题。
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1. 如果是数组,key 是 index,且是有效索引,那么将对应的 value 进行替换即可
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 2. 如果对象自身就存在这个键,直接赋值即可
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = target.__ob__
// 3. 如果是 Vue的实例,或者是 根数据,那么不应该被设置。
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
}
// 4. 如果没有 ob,说明对象本来就不是响应式的,这里也没必要做响应式处理
if (!ob) {
target[key] = val
return val
}
// 5. 如果对象本来就是响应式的,那么添加的 key 也要做响应式处理
defineReactive(ob.value, key, val)
// 6. 对象变化了,通知相关 watcher 更新
ob.dep.notify()
return val
}如代码中注释所示,前4个判断都只将键值进行普通设置处理。到了第5步,也就是数组索引超出边界或者对象设置了新的属性时,才会对新增属性进行响应式处理。这里看一下第6步,为什么需要通知Watcher更新呢?这里举一个例子:
// dom
<div> {{ user.name }}</div>
// index.vue
export default {
data() {
return {
user: {}
}
},
mounted() {
this.$set(this.user, 'name', 'qgh')
}
}我们分析一下,因为data里的user没有name属性,所以name属性是没有做过响应式处理的,那么改变this.user.name的时候也不会触发渲染watcher更新,即视图不会发生任何变化。这里需要注意的是,获取user.name的前提是需要获取user这个对象,而上面我们已经提到过的childObj在这里就起作用了,它会在获取user的时候,执行childObj.dep.depend(),建立与渲染watcher 的关系。最后user形式如下:
user: {
// __ob__是Observer,里面的dep记录着相应的watcher
__ob__: Observer
}当我们使用$set的时候,触发Watcher更新,实际上就是这里的渲染Watcher更新,所以user.name也会随之更新。又因为$set将name设置为响应式了,所以后续更改user.name视图也会更新。这就是$set妙处所在了。另外$del的实现也相差不多,最终也会通知watcher更新,这里不做过多介绍了,有兴趣的可以自己看源码了解一下。
Dep类
Dep类是在./dep.js文件下单独定义的:
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()
}
}
}看过《响应式原理上篇》的同学应该知道,上篇的实现与这里几乎相差无几,所以还有不了解Dep实现细节的同学,可以阅读一下本系列文章的《响应式原理上篇》。这里主要介绍一下另外一个细节:
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,那么这里为什么要用栈的形式来处理target呢?
试想一个场景:当前Dep.target存在时,我们想执行某段代码,但并不想要进行依赖搜集,此时用栈的优点就体现出来了。我们可以往栈里推入一个null,即targetStack为[watcher, null]那么在执行后续代码时,取得的当前target也就是最后一个target为null,就不会进行依赖搜集了。当这段代码执行完成后,我们把null弹出去,即targetStack为[watcher],这时target就又会恢复原来的watcher,后续代码就可以正常进行依赖搜集了。
Watcher类
Watcher类定义在./watcher.js文件,相较于Dep类,Watcher类则显得复杂许多。我们先看看Watcher的constructor:
// 渲染 watcher
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)首先如果是渲染Watcher,则会单独记录到vm实例上,因此可以通过调用vm._watcher强制重新渲染界面,这也是$forceUpdate的核心实现原理。而所有的Watcher都被记录到vm._watchers上,方便后续移除相应的Watcher。
接下来是options的一些处理:
// watcher 触发后的回调函数
this.cb = cb
this.id = ++uid // uid for batching
// watcher 是否还能使用
this.active = true
// 是否是惰性计算
this.dirty = this.lazy // for lazy watchers
// 上一次计算搜集的依赖
this.deps = []
// 当前计算搜集的依赖
this.newDeps = []
// 上一次计算搜集的依赖id
this.depIds = new Set()
// 当前计算搜集的依赖id
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
// 如果是函数,直接赋值即可
this.getter = expOrFn
} else {
// 解析 'a.b.c' 的形式,取得对应函数
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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
)
}
}这里的dirty是computed计算属性实现的关键,我们将会在下一章进行讲解,这里先放一放。
另外,deps和newDeps分别记录了上一次计算和当前计算搜集的依赖.这是因为每次计算的时候,搜集的依赖可能不一样,所以每次计算的时候,都会将新的依赖重新记录一遍:
addDep (dep: Dep) {
const id = dep.id
// 如果新的 dep 中不包含该 dep,则添加该 dep
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
// 如果旧的 dep 中不包含该 dep,则在 dep 里添加该 watcher
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}而在每次计算完毕后,又会将新的依赖赋值给旧的依赖,将新的依赖置空:
// 进行依赖搜集
get () {
// 标明当前正在执行的 watcher
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 {
// 如果 deep 为 true 的话,会循环遍历获取对象里的每一个值,
// 从而触发每一个相关的 watcher 进行 update
if (this.deep) {
traverse(value)
}
// 当前正在执行的 watcher 结束,不需要标明了
popTarget()
// 搜集完依赖后,清除依赖
this.cleanupDeps()
}
return value
}
// 计算完成后,将新 deps 赋值给旧 deps, 移除新 deps
cleanupDeps () {
let i = this.deps.length
// 清除在新 deps 中不存在的旧 dep
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// 将新 deps 赋值给旧 deps, 移除新 deps
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
}注意,这里有个细节就是deep代表深度搜集依赖。例如 { user: { name: { first: 'a' } } }这个对象,如果我们获取user时,这时只会将user作为依赖项进行搜集。但是如果deep为true时,会调用traverse方法,该方法会遍历对象,将对象内部所有的键值(如user的name和name的first)获取一遍,这个获取的过程也就是搜集依赖的过程,所以最终会将对象内所有字段全做为依赖搜集。
Watcher类剩下没介绍的主要就是update方法了:
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}可以看出,这里有三种情况,我们将会在下一章结合数据初始化过程分别讲讲这三种情况,并学习computed和watch两个方法的具体实现。
总结
这一章我们主要通过源码的角度,分别了解了observe,Observer类,defineReactive,$set/$del,Dep类,Watcher类的实现,以及介绍了Vue做的一些特殊处理,比如变异数组的拦截等。建议最好是能够亲自动手调试一下源码,才能更好的理解这几者之间的关系。
下一章我们将会回到Vue的实例化过程,看看再实例化过程中,到底是如何处理数据响应式的,同时我们也会彻底地理解computed和watch两个方法的具体实现过程。
