深入响应式原理(第四章)
响应式对象:
什么是响应式对象?当我们随手写一个对象的时候:
var obj = {
a:10,
b:20
}
其实这不是响应式对象,因为它没有能力来判断我们是否对对象进行操作。我们举个例子,让我们更加的对响应式对象有一个了解。尤大曾经在它演讲的源码解析中举过一个这样的例子。
我们有一个对象obj
。他有两个属性a/b
。它们的值都是数字,我们有一个要求,每次属性b
输出的时候都是a
值的两倍。即:console.log(obj.b)
。那么我们该如何做到呢?
var obj = {
a:10,
b:20
}
其实这里有一个隐含的问题,那就是a
属性的值是可以随时变化的,我们是不知道的。那么如果让属性a
的改变影响到属性b
呢?此时我们就需要了解一个函数:Object.defineProperty()
。
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
参数:
obj
:要定义或修改属性的对象key
:要定义或修改属性的名称descriptor
:要定义或修改的属性描述符。
descriptor
-
configurable
当且仅当该属性的
configurable
键值为true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为false
。 -
enumerable
当且仅当该属性的
enumerable
键值为true
时,该属性才会出现在对象的枚举属性中。 默认为false
。 -
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为
undefined
。 -
writable
当且仅当该属性的
writable
键值为true
时,属性的值,也就是上面的value
,才能被赋值运算符
改变。 默认为false
。
存取描述符还具有以下可选键值:
-
get
属性的 getter 函数,如果没有 getter,则为
undefined
。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为undefined
。 -
set
属性的 setter 函数,如果没有 setter,则为
undefined
。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this
对象。 默认为undefined
。
举例:
var obj = {
a:10
}
Object.defineProperty(obj,'a',{
get(){
console.log('你正在访问')
},
set(){
console.log('你正在修改')
}
})
console.log(obj.a)
obj.a = 20
//结果:
你正在访问
undefined
你正在修改
你看可以发现,当我们去访问或者修改的时候,obj
就知道它被访问或者修改了。那么我们如何让属性b
每次输出的时候都是属性a
的两倍呢?
var obj = {
a:10,
b:20
}
function ReactDefineProperty(o){
const obj = o
Object.defineProperty(o,'a',{
set(value){
obj.b = value*2
}
})
}
ReactDefineProperty(obj)
obj.a = 20
console.log(obj.b)
obj.a = 5
console.log(obj.b)
obj.a = 1
console.log(obj.b)
//结果
40
10
2
其实代码并不难,你会发现:Object.defineProperty
将称为响应式原理的关键。
initState()
initState()
函数用来处理我们传入的data/props/methods
。该函数的调用在beforeCreate
钩子函数之后,在created
钩子函数之前。该函数的代码如下:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)//处理props
if (opts.methods) initMethods(vm, opts.methods)//处理methods
if (opts.data) {//处理data
initData(vm)
} else {//如果我们没有从传入data,那么就使用一个空对象作为默认的data
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)//处理computed
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)//处理watch
}
}
initState()
函数对数据做的初始化顺序为:props -> methods -> data -> computed -> watch
我们主要研究的是data/props
内部的实现原理。
initData(获取data并判断是否有命名冲突)
该函数的代码如下:
function initData (vm: Component) {
let data = vm.$options.data//获取data
//获取data
data = vm._data = typeof data === 'function'
? getData(data, vm)//如果data是一个函数,那么执行getDate()来拿到内部的data数据
: data || {}
//判断类型
if (!isPlainObject(data)) {//对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)//获取data中的key
const props = vm.$options.props//获取props
const methods = vm.$options.methods
let i = keys.length
//判断属性冲突
while (i--) {//该代码就是这个遍历,看是否methods/props/data中是否有命名冲突
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)//将我们的data代理到vm._data_上
}
}
// observe data
observe(data, true /* asRootData */)
}
initData
函数的作用也是对data
进行一些边界情况的处理兼容,最核心是调用observe()
函数。但是在此之前我们要知道initData
函数做了哪些事情。首先获取data
,因为我们传入的data
不一定是对象,所以要处理获取data
。然后将data
中的key
和props/methods
中的key
进行比较,看是否有命名冲突。当这些工作全部都准备好之后开始执行observe()
函数。
observe()
export function observe (value: any, asRootData: ?boolean): Observer | void {
//对data进一步边缘处理
if (!isObject(value) || value instanceof VNode) {//如果data不是一个对象,或者说是一个vnode。那么直接返回,不予进行响应式处理
return
}
let ob: Observer | void
//判断是否已经处理过了。
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
//判断data是否有__ob__属性,因为这个属性只有经过observe处理过后的对象才具有。
ob = value.__ob__
} else if (
shouldObserve &&//是否要进行observe()处理,默认为true
!isServerRendering() &&//非服务端渲染
(Array.isArray(value) || isPlainObject(value)) &&//要么是数组,要么是对象
Object.isExtensible(value) &&//对象是可扩展的
!value._isVue//不是Vue构造函数实例
) {
//我们传给Observer()是data对象
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
observe
函数的核心是调用new Observer()
构造函数,但是在此之前对传入的参数进行了处理。首先如果data
不是对象或者是一个vnode
,那么则不予处理。判断我们要observe
的对象是否已经被observe
过了。如果没有那么就进行条件验证:shouldObserve/ !isServerRendering()/(Array.isArray(value) || isPlainObject(value))/ Object.isExtensible(value)/ !value._isVue
。当这些条件满足之后调用new Observer()
函数。
-> new Observer()
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
//这里的this指向的是observer实例,也即是说每一个data中的属性对象都会生成一个对应的observer实例对象
//为对象添加标记。这里并没有特指data,因为data中如果还有子对象的话也会走这一步的
this.value = value
this.dep = new Dep()//为该属性对象添加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函数
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])
}
}
}
这里有一点需要注意,为属性对象创建的ob
我们在哪里可以获取到呢?在data.__ob__
中可以获取,那和组件又有什么关系呢?我们可以通过vm.$data
获取到data
,然后通过data.__ob__
获取到该组件对应的ob
。每一个组件都有一个单独的data
,每一个data
都有一个ob
,通过data.__ob__
可以访问。不仅每一个data
都有一个__ob__
,甚至data
中的属性如果是一个对象,那么这个对象也有一个__0b__
。
对于对象的监听,我们先假设监听的是一个没有引用类型数据的对象,例如:{name:"xz",age:13}
。然后再假设监听的是一个有引用类型的数据{name:"xz",friends:{name:'hky'}}
。
我们向Observer
构造函数传入的是data
对象,但是返回的ob
实例是另一个对象,那么就让我们看看Observer
函数到底做了什么事情。
首先将我们的data
挂载到ob.value
上。然后添加ob.dep
属性。def(value,'__data__',this)
。这行代码的运行结果是在我们传入的data
对象上添加一个__ob__
属性,而该属性的值就是我们的ob
实例。如果你想想就会发现ob/data
这两个对象相互引用,形成闭环。这个是题外话。
其实observe
函数的参数是一个对象,不一定是根data
。原因是当我们的根data
有子对象的时候,也会将子对象进行observe
处理,也就是说,会深度遍历子属性,将每一个子对象添加一个__ob__
属性。如果我们传入的是对象,则会调用walk()
函数。
->->walk() 获取keys
walk (obj: Object) {
//为每一个key都进行响应式处理
const keys = Object.keys(obj)//获取value中的key
for (let i = 0; i < keys.length; i++) {//然后将key逐个进行defineReactive处理
defineReactive(obj, keys[i])
}
}
walk()
函数的作用就是获取所有的key
,然后将每一个key
都进行defineReactive()
处理。
->->->defineReactive()
export function defineReactive ( obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean ) {
//为每一个key都创建一个dep用来存放访问该属性的依赖
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)//获取到key的描述信息
if (property && property.configurable === false) {//如果该属性是不可配置的,那么将返回
return
}
// cater for pre-defined getter/setters
const getter = property && property.get//获取该属性原生定义