响应式原理
据Vue的官方介绍,Vue的响应式系统是非侵入性的,那Vue是如何做到将不同类型的数据(基本类型、普通对象、数组等)转换为可检测的呢?在了解Vue的具体实现之前,我们先了解一下为什么要将数据变为可检测的。
因为Vue是MVVM框架,即数据可以驱动视图的,在传统的开发中(非数据驱动视图),我们需要去操作DOM来实现视图的更新,但在Vue中,视图是通过数据进行驱动的,也就是说我们只需要操作数据,Vue内部会为我们进行DOM的操作。这样一来,我们的开发工作就会变得比较便捷,我们只需要维护数据,而不需要进行DOM的操作。那数据驱动视图一个核心就是检测数据的变化,当数据发生变化时,需要对视图进行更新。
接下来,我们回到第一个问题:Vue是如何做到将不同类型的数据(基本类型、普通对象、数组等)转换为可检测的呢?
响应式对象
在进行Vue的初始化时,会执行initState方法,它定义在文件中:src/core/instance/state.js
。
function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initState主要是进行初始化props、methods、data、computed、watch等,先看看是如何初始化props的。
initProps
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// 缓存prop的键,以便将来的prop更新时可以使用Array而不是动态对象键枚举进行迭代。
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
// 根实例的props应该被转换
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// ...
} else {
defineReactive(props, key, value)
}
// 在Vue.extend()期间,静态props已经代理在组件的原型上
// 我们只需要代理定义在此处的实例化。
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
initProps主要是遍历propsOptions(即是我们在Vue组件中定义的props属性),接着做两件事情:defineReactive和proxy。
initData
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
)
}
// 在实例上代理data
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') {
// 检查methods上是否有同名键
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
// 检查props上是否有同名键
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)
}
}
// 对data进行observe
observe(data, true /* asRootData */)
}
initData主要做的事情是:
- data可以是函数,如果是,则将函数的返回值当成data
- 遍历data上的键
- 检查每个键是否已经在methods和props上定义过,因为data/methods/props上的键都会代理到vm上,所以不能同名
- 将data的键代理到vm上
- 将data进行observe
初始化props和data都会使用proxy方法和observe方法,那这两个方法究竟是做什么的呢?我们一起来看看。
proxy
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
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的逻辑比较简单,即是将target上的sourceKey中的key直接代理到target上,即是通过defineProperty,将通过sourceKey[key]获取和设置的方式变为通过target[key]直接获取和设置的方式。proxy的核心目的是将props/data等数据直接代理到vm实例上,这样,我们就可以在Vue组件中直接通过vm.xxx
/this.xxx
的方式直接获取到props/data上的数据了。
defineReactive
defineReactive 方法定义在文件中:src/core/observer/index.js
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
}
// 迎合预定义的getter/setter
const getter = property && property.get
if (!getter && arguments.length === 2) {
val = obj[key]
}
const setter = property && property.set
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()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
defineReactive
是将数据转化为响应式的关键,主要是使用Object.defineProperty
方法,如果对这个方法不熟悉的可以查看MDN文档。
defineReactive
主要做的事情是为对象的某个键重新定义getter和setter,在原有的getter的基础上,新增收集依赖的操作,在原有的setter的基础上,新增派发更新的操作。这样,当我们为获取数据的值时,就会触发其getter,然后将使用到这个值的订阅者收集起来;当我们为某一个值重新赋值的时候,就会触发其setter,这时可以通知之前收集到的订阅者数据发生改变,这样一来,通过对数据劫持的方式就可以把对象上的某个键变成响应式。
在此之前,还执行了let childOb = !shallow && observe(val)
,那observe方法是干什么的呢?
observe
observe方法定义在文件中:src/core/observer/index.js
。
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 主要是创建一个Observer实例,并将其返回,那类Observer是什么呢?其定义在文件中:src/core/observer/index.js
// 附加到每个被观察对象的观察者类。
// 附加后,观察者将目标对象的属性键转换为收集依赖和派发更新的getter/setter。
class Observer {
value: any;
dep: Dep;
vmCount: number; // 将此对象作为根$data的vm数量
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
// 遍历每个属性并将它们转换为getter/setter。仅当值类型为“对象”时才应调用此方法。
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 观察数组项列表
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Observer的逻辑比较清晰,先实例化Dep,接着执行def(value, '__ob__', this)
,最后如果value是数组,则将其每一项进行observe,如果是对象,则将其所有键进行defineReactive。
function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
def
是把Observer实例挂载在value的__ob__属性上。
其实observe也是将数据变为响应式的,在observe和defineReactive中,也都有调用对方,那observe和defineReactive有什么区别呢?observe是将一个对象或数组转化为响应式,而defineReactive是对一个键值对转化为响应式,,两者在内部都会调用对方,在 defineReactive 中,如果属性的值为对象,则会通过 observe 将其值转换为响应式; 在 observe 中,会遍历对象的每一个属性,通过 defineReactive 将每个属性都转换为响应式。我们举一个例子:
data(){
return {
time: "2020-02-22 15:01:30",
person: {
name: 'haha',
age: 18,
},
list: ['1', '2']
}
}
Vue先通过initData
进行初始化,会对data返回的整个对象进行observe
,因为data是一个对象,所以会通过walk
遍历对象的所有键值对,进行defineReactive
,也就是将time/person/list
这三个键转化为响应式,当改变这三个键的值时,就会通知对应的订阅者;defineReactive
过程中,因为person/list
的值不是基本类型,所以会对其值进行observe
,通过这种递归的方式,无论一个对象嵌套多少层,其子属性是什么类型,都会将整个对象变成一个响应式的对象。
检测变化的注意事项
Vue 在以下的情况下是无法检测到数据变化的:
- Vue 不能检测到对象属性的添加或删除
- Vue 不能检测以下变动的数组:
- 当利用索引直接设置一个项时,例如:vm.items[index] = newValue
- 当修改数组的长度时,例如:vm.items.length = newLength
所以,Vue通过一些处理,让操作对象和数据也是可以变成响应式的。
Vue.set
set方法定义在文件中:src/core/observer/index.js
// 在对象上设置属性。 添加新属性,如果该属性尚不存在,则触发更改通知。
function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
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
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
set 方法的主要逻辑是:
- 如果target是数组,则设置数组的长度,并且使用splice进行替换元素
- 如果target是对象,若key存在其自身,则直接赋值,因为此key已经是响应式的了;若key不存在且ob不存在,则直接赋值并返回,因为说明该对象不是响应式;若key不存在且ob存在,则通过
defineReactive
将此key设置为响应式,并且手动进行派发更新,这样一来,新设置的属性也转化为响应式的。
那为什么数组直接通过splice,就可以通知视图更新呢?
数组
这是因为Vue针对数组的原生方法进行修改,在其中触发派发更新的操作。
class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// ...
}
}
}
在Observer中,若值是数组,会执行augment
和this.observeArray
。
// 通过使用__proto__截取原型链来增强目标对象或数组
function protoAugment (target, src: Object, keys: any) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
// 通过定义隐藏属性来增强目标对象或数组。
/* istanbul ignore next */
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])
}
}
所以,augment的主要作用是将 value 的原型指向了 arrayMethods。arrayMethods定义在文件中:src/core/observer/array.js
。
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 拦截修改方法并触发事件
methodsToPatch.forEach(function (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,并在其上针对数组的方法进行重写。这些方法除了执行原本的逻辑之外,还对数组新增的元素进行observe,最后还手动触发派发更新,这样就实现了数组通过数组方法,也能直接触发视图更新了。
那observe的dep是什么时候收集依赖的呢?答案是在defineReactive中
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// ...
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
},
// ...
})
}
当存在childOb时,执行childOb.dep.depend()
就会进行依赖的收集,所以当触发ob.dep.notify()
时,就会进行派发更新。如果 value 是个数组,那么就通过 dependArray
把数组每个元素也进行依赖收集。