对于 Vue 框架来说,数据响应式原理是它的核心特点之一,即数据发生变化,驱动视图改变。而对于了解过 Vue 的同学来说,其实现响应式的核心是利用 ES5 的 Object.defineProperty ,将普通对象定义为响应式对象,即给对象属性设置 getter 和 setter 方法,监听属性值的变化。
本文将来分析数据响应式原理,先从 data 开始。
initState
沿着主线将 data 的初始化逻辑整理成一张图,如下:
在初始化 Vue 实例时,函数 initState 对 data 做了初始化操作,位于 src/core/instance/init.js,具体实现如下:
export 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)
}
}
函数接收一个参数:Vue 实例。其作用是对 props 、methods 、data 、computed 、watch 做初始化操作。而对 data 的初始化操作仅有 4 行代码:
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
data 的初始化有两种场景:
- 当执行
new Vue实例化对象时,此时vm.$options.data为undefined,执行else逻辑; - 在实例化 Vue 的过程中,当执行到
patch,即将虚拟 Vnode 转换为真实 DOM 的过程中,会执行子组件初始化操作,基于父组件已经初始化的情况下,此时vm.$options.data不为空,执行if逻辑。
在 initData 内部实现过程中,其最后也会调用 observe ,那么来看其内部具体实现。
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
)
}
// 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 */)
}
函数接收一个参数:Vue 实例,其作用是将 data 对象设置为响应式对象。
data 对外提供的数据类型有两种:一是对象;二是函数。
如果是对象的话,直接赋值给 vm._data 和 data;如果是函数,则执行函数返回对象赋值给 vm._data ,即调用函数 getData
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
函数接收两个参数:
data:用户传进来的函数vm:Vue 实例
其作用是执行函数 data,返回对象,即调用 call 来执行函数的。
接着调用函数 isPlainObject 检查 data 其数据类型是否为 Object,否的话则会在开发环境中抛出告警。
然后将 data 属性代理到 vm 实例上,但在代理之前会检查其属性合法性,即 data 属性(key)是否存在于 methods 或者 props ,如果存在的话,则在开发环境中抛出告警。
如果属性不存在于 methods 或者 props,并且 key 不是以 _ 或者 $ 开头,则将其代理到 vm 实例上,具体实现如下:
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)
}
函数接收三个参数:
target:vm实例sourceKey:vm实例上属性_datakey:属性名称
其作用是将 data 属性定义在 vm 实例上。
从代码实现可以看出,平时经常使用 this.xxx 时,实际上通过 this._data.xxx 返回其值的,这也就解释了为什么可以直接通过 vm 实例访问属性。
最后调用函数 observe 将 data 对象设置为响应式对象。
observe
/**
* 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
}
函数接收两个参数:
value:即将被设置为响应式对象的普通对象,此时的值为data对象asRootData:表示root data,其数据类型为布尔值,此时的值为true
首先,对 value 做类型检查,如果其数据类型不是 Object 或者是 VNode 实例,则结束程序;从这里也可以看出 VNode 不能是响应式的。
接着,检查 value 是否有属性 __ob__,并且其属性 __ob__ 是否为 Observer 实例,如果两个条件都满足,则将 value.__ob__ 赋值给 ob ,说明已经设置过;否则满足其它条件时实例化 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)
}
}
}
从构造器可看出,先初始化 Dep 实例,用于收集依赖。
接着在 value 定义属性 __ob__ ,其值指向 Observer 实例,具体实现如下:
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
然后判断 value 其数据类型是否为数组,如果是数组的话,则执行 if 逻辑,否则执行 else 逻辑;先来看下数据类型为数组逻辑。
如果 hasProto 为 true,则调用 protoAugment 函数,否则调用 copyAugment ,那么来看下这两个函数是如何实现的?
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
函数接收两个参数:
-
`target:目标对象 -
src:数据类型为对象,此处传入的参数arrayMethods指向如下:/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ import { def } from '../util/index' 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原型,然后遍历methodsToPatch(包含数组一系列方法),将数组方法定义到对象arrayMethods` 上。
copyAugment 实现如下:
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* 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])
}
}
遍历 keys ,即数组七个方法(push、pop、shift、unshift、splice、sort、reverse),分别将它们设置到 value 上。
执行以上逻辑后,会调用 observeArray 函数,具体实现如下:
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
逻辑实现很简单,遍历数组,然后再调用 observe 将其设置为响应式属性。
最后来看 else 分支的实现逻辑,即 walk 函数的代码实现:
/**
* 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])
}
}
获取对象 keys,对其进行遍历,调用函数 defineReactive 将属性设置为响应式属性,这也是实现响应式的核心函数,具体实现如下:
/**
* Define a reactive property on an Object.
*/
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 () {
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()
}
})
}
函数的实现使用 ES5 Object.definePropert 在对象上定义响应式属性,即定义了 get 和 set 函数,函数的作用分别是依赖收集(换句话说,就是将观察者添加到订阅列表)和派发更新(换句话说,当数据发生变化,对已经订阅的观察者发出通知,让它们做出相应的更新)。
当获取属性值时,则会触发 get 函数;比如在渲染 vm 实例时就会触发;当改变属性值,则会触发 set 函数。
除此之外,如果 value 其数据类型为 Object,则会调用函数 observe 处理其属性,这样确保了不管对象嵌套的深度有多深,都可以将其设置为响应式属性。
至此,data 初始化过程就分析到这里。