系列文章
- [Vue源码学习] new Vue()
- [Vue源码学习] 配置合并
- [Vue源码学习] $mount挂载
- [Vue源码学习] _render(上)
- [Vue源码学习] _render(下)
- [Vue源码学习] _update(上)
- [Vue源码学习] _update(中)
- [Vue源码学习] _update(下)
- [Vue源码学习] 响应式原理(上)
- [Vue源码学习] 响应式原理(中)
- [Vue源码学习] 响应式原理(下)
- [Vue源码学习] props
- [Vue源码学习] computed
- [Vue源码学习] watch
- [Vue源码学习] 插槽(上)
- [Vue源码学习] 插槽(下)
前言
在前几章节中,我们已经可以根据组件模板,渲染成真实的DOM,而响应式系统则可以控制渲染的逻辑和实际内容,同时收集在渲染过程中访问到的数据,在数据发生变化时,通知所有的观察者,从而使它关联的组件进行重新渲染,那么接下来,就来看看Vue中的响应式系统是如何工作的。
initData
在初始化Vue实例的过程中,会调用initState方法,代码如下所示:
/* core/instance/init.js */
Vue.prototype._init = function (options?: Object) {
// ...
initState(vm)
// ...
}
/* core/instance/state.js */
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
// ...
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// ...
}
可以看到,如果配置选项中存在data选项,就会调用initData方法进行处理,否则会创建一个空对象,然后调用observe方法,其实initData方法内部同样是调用observe方法,只不过多了一些校验和代理的逻辑,initData方法的代码如下所示:
/* core/instance/state.js */
function initData(vm: Component) {
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 */)
}
/* core/util/lang.js */
export function isReserved(str: string): boolean {
const c = (str + '').charCodeAt(0)
return c === 0x24 || c === 0x5F
}
可以看到,在initData方法中,首先通过$options.data获取到真实的数据,并赋值给vm._data;然后获取该实例上已经定义的props和methods,接着遍历data中的数据,对已经存在于props或methods中的数据,在开发模式下会提示警告,对于不存在的数据,并且属性名不是以$或_开头,就调用proxy方法,将当前数据代理到当前实例上,proxy方法的代码如下所示:
/* core/instance/state.js */
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)
}
经过proxy方法处理后,就可以使用this[key]代替原来的this._data[key]了。在initData方法的最后,还是调用observe方法,进行数据的响应式处理,那么接下来就来看看observe方法的具体实现。
observe
observe方法的代码如下所示:
/* core/observer/index.js */
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是对象并且不是VNode的实例时,才允许创建响应式;对于已经创建过响应式的对象,可以直接通过value.__ob__进行访问,对于普通的对象,在满足一定的条件下,会调用Observer构造函数,创建响应式对象;接下来的ob.vmCount++是用来防止Vue实例重复使用同一个data选项,确保每个实例可以维护一份自己的数据;最后返回ob对象。接下来,就来看看Vue是如何通过Observer构造函数将普通对象转换成响应式对象的。
Observer
Observer构造函数的代码如下所示:
/* core/observer/index.js */
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) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
可以看到,每一个Observer对象都包含一个Dep的实例,它是用来连接目标对象和观察者的管理器,在下一小节中会详细介绍Dep的作用,然后使用def方法,将Observer实例保存在value的__ob__属性中,代表着value已经经过响应式处理,不用再次重新new Observer;接下来就开始处理传入的数据了,需要处理的数据分为普通对象和数组,由于结构的不同,使用的处理方式也不一样,下面分别来看看它们是如何进行处理的。
普通对象
对于普通对象而言,Vue会通过walk方法进行处理,代码如下所示:
/* core/observer/index.js */
export class Observer {
walk(obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
可以看到,walk方法就是遍历传入的数据,然后调用defineReactive方法,从而将普通数据变为响应式数据,其代码如下所示:
/* core/observer/index.js */
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方法中,Vue也同样创建了一个Dep的实例,与每个目标属性相连;接下来是处理不可配置属性和访问器属性的逻辑;然后在满足shallow参数为假的情况下,会继续调用observe方法,递归的处理深层嵌套对象,从而使整个对象的每一层级,都变成响应式的;最后就是通过Object.defineProperty方法,将属性定义为访问器属性,从而实现数据的响应式化,其实就是在访问或设置属性的过程中,添加一个中间层,从而执行一些额外的逻辑。需要注意的是,定义响应式的代码是在初始化Vue实例时执行的,此时并没有访问或设置属性,也就是没有执行上面的get和set访问器,在下一小节中,会详细介绍它们是如何工作的。那么接下来,就看看Vue是如何处理数组的。
数组
对于数组来说,Vue执行的逻辑如下所示:
/* core/util/env.js */
export const hasProto = '__proto__' in {}
/* core/observer/index.js */
export class Observer {
constructor(value: any) {
// ...
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
}
// ...
}
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
function protoAugment(target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
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])
}
}
可以看到,Vue处理数组的逻辑其实有两步,第一步是在数组上添加变异方法arrayMethods,第二步是调用observe方法,递归处理数组中的每一项数据,observe方法的逻辑在上面已经介绍过,那么接下来,就来看看变异方法arrayMethods是如何创建的。
在core/observer/array.js中,包含了arrayMethods的全部逻辑,代码如下所示:
/* 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
})
})
可以看到,arrayMethods首先是继承自Array.prototype的,所以它拥有数组原型上的所有原生方法,然后给arrayMethods对象上重新定义push、pop、shift、unshift、splice、sort、reverse这7个方法,当该数组调用这些变异方法时,首先会执行变异方法的默认操作,然后会从该数组上获取__ob__属性,也就是之前定义的Observer对象,对于push、unshift、splice方法而言,由于它们可以给数组添加新的数据,所以首先需要使用ob.observeArray方法,将它们处理成响应式数据,最后所有的变异方法都会调用ob.dep.notify方法,这段逻辑是变异方法中最关键的逻辑,通过调用notify方法,可以通知所有与之相关的观察者,从而完成组件的重新渲染。
总结
Vue实例在初始化的过程中,会调用initData方法,处理配置中的data选项,通过Observer构造函数和defineReactive方法,就可以将整个对象通过Object.defineProperty方法,处理成响应式数据,并且对于数组来说,Vue对7个方法进行了扩展,使之能够触发响应式。