这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战
前言
上篇已经分析了defineReactive函数通过Obejct.defineProperty来为对象属性添加getter/setter的过程,但是对于的observe函数的具体实现并没有深入去看,这篇就来看下observe函数。
在前边看new Vue的时候
observe
observe函数定义在src/core/observer/index.js
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
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
}
整体来看,observe函数用来为value创建一个Observer实例,最后返回这个Observer实例,如果已经创建过则直接使用。
主要接收两个参数:
- 要创建Observer实例的value
- 是否是根数据
具体来看下实现细节:
-
首先判断value的类型,如果是非对象或者Vnode类型,则直接返回。
-
接下来判断value对象中是否存在__ob__属性,且该属性对应的值是一个Observer类型(即该value对象已经添加了Observer),则直接将Observer赋值给ob变量。
-
如果没有__ob__,且value是一个简单的对象(非Regexp等情况),则创建一个Observer实例赋值给ob变量。
-
如果value是根对象,则ob.vmCount值加一。vmCount值是用来标记value作为根数据的个数。在看new Vue实例过程时在初始化过程中会调用initState函数,在它的内部就会调observe(data, true),此时的value就是一个根对象。
-
最后返回ob变量。
看一下几个工具函数,都定义在src/shared/util.js文件中:
hasOwn
hasOwn函数内部直接调用了hasOwnProperty来判断某个属性是否包含在指定对象中(不包括原型链中的属性)。
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}
isPlainObject
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
isObject
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
Observer
接下来看一下Observer类的具体实现:
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)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
...
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
...
}
}
Observer接收三个参数(要转化为Observer的value值,Dep,代表根对象的vms的个数)
添加__ob__属性
初始化完参数后,执行了def(value, '__ob__', this)
先来看下这个def函数,定义在src/core/utils/lang.js
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
def函数就是对Object.defineProperty的封装,它的作用就是在value对象上增加一个__ob__属性,指向这个创建的Observer。其实在开发过程中看data上的数据,会发现,都包含一个__ob__的属性。
value对象上添加完__ob__属性后,接下来就是处理value对象了。
处理value
- 首先判断value是不是数组,如果不是,则调用walk函数去处理value。看一下walk做了什么??
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
walk函数内部还是调用了前边我们已经看过的defineReactive来把value的自身属性都变成响应式的。
- 接下来看下value是数组的情况下,要怎么处理??
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
}
hasProto变量定义在src/core/util/env.js文件中
export const hasProto = '__proto__' in {}
hasProto是用来检测当前运行环境是否支持__proto__,虽然Object.prototype.__proto__在web标准中已经被废除,但是目前大部分的浏览器仍然是支持的。
arrayMethods变量定义在src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
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
ob.dep.notify()
return result
})
})
Vue官方文档提到过Vue对数组是做了特殊处理的,它将数组的七个方法重新做了包裹来实现响应式,但是对于个别数组的操作还是无法监听到变化。
上边的代码就是处理数组的过程:
- 首先利用Object.create方法创建一个继承自Array原型的空对象arrayMethods
- 接下来遍历数组的这7个方法,并把他们一一挂到arryaMethods上。对push,unshift,splice三个可以增加数组长度的方法进行了特别处理:首先拿到新增的数据,再调用observeArray把新增数据变成响应式的。
- 最后调用notify来派发更新
protoAugment
在支持__proto__的环境中,调用protoAugment方法,来看下这个方法:
function protoAugment (target, src: Object) {
target.__proto__ = src
}
我们知道实例通过__proto__指向构造函数的原型对象。protoAugment函数就是用来把value数组实例的__proto__指向重新包装后的数组对象arrayMethods。
copyAugment
不支持__proto__的环境中,调用copyAugment方法。
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
先来看下arrayKey定义:
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
arrayKeys就是我们拦截的那七个数组方法:["push", "pop", "shift", "unshift", "splice", "sort", "reverse"]
copyAugment方法就是用来把arrayMethods中的数组方法一一添加到result中。
observeArray
将数组对象value通过arrayMethods拦截处理完后,最后执行observeArray
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
这个函数用来遍历数组,然后对每一项都调用observe,将数组中的每一项都变成响应式数据。
总结
- observe函数用来返回一个Observer
- Observer类用来为对象添加一个__ob__属性,同时把该对象变为响应式的。
- 在defineReactive函数中看出响应式的实现是通过使用Object.defineProperty拦截对象来添加getter/setter,但是数组是没有getter/setter的,所以要对数组去特殊处理。