前言
接上一章节,我们知道响应式系统大致就这三步:
- 观测数据,将其转化为响应式对象
- 暴露
$watch
方法,接收exp、fn
参数,确定要监听的属性以及接收数据变化的回调函数 -
get
收集依赖,set
触发依赖,也就是get
里收集fn
,set
里触发fn
现在我们看看Vue
里是怎么实现响应式的
function initData(vm: Component) {
// ...
// observe data
observe(data, true /* asRootData */)
}
我们就从initData
为入口看看是如何观测数据的
正文
observe函数
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
}
可见initData
调用observe
方法观测数据。它接收俩个参数value(要观测的数据)
、asRootData(是否是根数据)
if (!isObject(value) || value instanceof VNode) {
return
}
首先判断value
若不是对象或者是VNode
实例对象就return
,很容易理解,不是对象自然是不能观测,若是VNode
实例对象也是不需要观测的,因为VNode
实例对象用于DOM
比对更新,这不需要响应式。即使自己new VNode
也没什么实际意义。demo
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...else
分支我们先看if
分支,它先判断value
上有没有__ob__
属性且是Observer
实例,是的话就将其赋值给ob
其实
__ob__
属性是new Observer(value)
之后添加上的,我们暂且不管它其他作用,在此它可以判断是否已经观测,也就是防止重复观测。它是Observer
实例对象
然后是else if
分支,它有五个条件:
shouldObserve
export let shouldObserve: boolean = true
export function toggleObserving(value: boolean) {
shouldObserve = value
}
可见该变量初始值为true
,且暴露toggleObserving
方法用于修改其值。其实从这if
语句可知shouldObserve
为false
的话就不会观测,那么很显然这个值是用于控制转换响应式数据的。比如props
就调用了此方法用于禁止观测。盖其数据来源一般来自于data
,其本身已经是响应式得了,就不需要观测了
-
!isServerRendering()
这个就是必须非服务端渲染。服务端渲染的话不观测。原因呢官网讲的很清楚,服务器上的数据响应 -
(Array.isArray(value) || isPlainObject(value))
必须是数组或者纯对象,无需解释 -
Object.isExtensible(value)
必须是可扩展,Object.preventExtensions()、Object.freeze()、Object.seal()
可禁用对象扩展 -
!value._isVue
必须不是Vue
实例,这个也是因为Vue
实例诸如_isVue
之类的属性不可能去修改,而data
之类的已经观测了。https://nymlc.github.io/vue2.x-analysis/demo/observer/4.html#2
五个条件为真才调用Observer
观测且返回一个对象赋值给ob
if (asRootData && ob) {
ob.vmCount++
}
return ob
最后判断下是否是根data
(vm._data
)且ob
也存在,那么就给ob.vmCount++
,最后返回ob
vmCount
稍微讲下,比如一个Hello
组件(data
引用的同一个对象),每用一次组件就会vmCount++
。https://nymlc.github.io/vue2.x-analysis/demo/observer/4.html#3
Observer 类
上文可知,其实observe
只是做了些边界情况处理,真正转换响应式对象是在Observer
export class Observer {
value: any;
dep: Dep;
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)
}
}
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])
}
}
}
看一个类首先看整体,也就是看它的属性和方法,然后从constructor
开始看起
vmCount
初始化vmCount
为0
,这个在observe
里有重新赋值,其实就是用于判断是否是根data
。比如可以在Vue.set
里防止给根data
添加新属性
我们知道
Vue.set
可以添加新属性,但是并不能给根data
添加新属性。但是我们稍加修改是可以的,在Vue.set
详解,在此不再赘述。https://nymlc.github.io/vue2.x-analysis/demo/observer/4.html#4
ob
这个是个很关键的属性,之后会看见它的诸多用处。这里只是简单说下它的构造和基本用途
首先为什么使用def
呢,是因为使用了def
,这个__ob__
属性就不可枚举,这样子遍历观测就不会观测此属性。它可以用于判断当前对象是否已观测,其值是Observer
实例对象,更是诸多用处
其构造如下
const data = {
a: 1,
// 不可枚举
__ob__: {
value: value, // 其实就是data
dep: new Dep(), // Dep实例
vmCount: 0 // 根data被观测使用次数(其实就相当于组件被实例化次数)
}
}
dep
这个其实就是上章节说的那个收集依赖的容器,只是比我们例子复杂很多而已,之后详述。值得注意的是,这里的dep
容器是对应于对象的,下面还有个dep
是对应于属性的
defineReactive
初始化完了几个实例属性之后就开始观测数据
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
可见是区分数组、对象的。我们先从对象看起,即else
分支,调用walk
方法处理
walk(obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
此方法很简单,就是遍历每个属性然后调用defineReactive
处理即可
其实早先并非如此设计,是
defineReactive(obj, keys[i], obj[keys[i]])
,为何如此下文到了再说
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
}
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
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
首先定义dep
变量,且初始化,这就是上文说的对应属性(key
)的容器,通过闭包机制被setter、getter
调用
然后获取当前属性值得属性描述符,若是不可配置,那么就return
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
这段还是比较难理解的,说道说道。首先这个是控制val
值情况,因为若是没有执行到if
里,那么val
为undefined
,也就意味着之后的深度观测失效
这个if
有俩个判断条件:demo
-
arguments.length === 2
首先参数至少有俩,若是假,说明参数传了val
,那么自然无需求值 -
!getter || setter
其实最先并非这么设计的除了上文的defineReactive
调用还有如这个issue
if (!getter && arguments.length <= 2)
我们先不管setter
,那么这里看来若是有getter
的话自然就不会深度观测。可见demo的data.a
属性
若是有getter、setter
的话,按此逻辑就不会深度观测,重新赋值的话可见新值被深度观测了。这样子本未深度观测的被深度观测了,前后不一致,所以就改成现在样子,即setter
在的话就取值,以达到深度观测效果,代码如下,在线demo
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
getter
在setter
不在的话,不深度观测,因为getter
是用户自定义的,可能有未知的问题,所以规避setter
在的话,深度观测。这是因为要是不深度观测,重新赋值之后又被深度观测,前后违背了定义响应式数据行为不一致,简单处理就是只要有setter
就深度观测
观测之后,无论之前有没有getter、setter
现在都会有
除了上文说的val = undefined
影响深度观测,很明显shallow
也是用于控制深度观测的,前者隐控制后者显控制
let childOb = !shallow && observe(val)
很明显,只有显示传参为true
才能非深度观测。返回的Observer
实例对象赋值给childOb
,搜索可知只有以下俩者是非深度观测
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
控制深度观测手段有俩者:
val = undefined
深度观测失效shallow = true
不会深度观测
收集依赖
接下来就是设置getter、setter
访问设置器,我们先看getter
,也就是收集依赖
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
}
})
get
需要干俩件事:
- 返回属性值
我们先判断此属性是否有getter
,它保存的是该属性原本的getter
函数。若存在就直接执行获取值,否则就返回之前的val
赋值给value
,最后返回即可 - 收集依赖
我们先假设这就是我们已经观测完的数据,然后我们收集data.a
依赖,那么俩个依赖容器分别是:-
dep
就是a
属性对应的依赖容器,通过闭包引用的 -
a
属性对象对应的依赖容器,也就是如下的ob2.dep
-
const data = {
a: {
b: 1,
__ob__: ob2
},
__ob__: ob1
}
这个我们先回想下上章节的小栗子,我们先判断Dep.target
存不存在,这个其实就是我们上章节的Target
也就是我们要收集的依赖,有的话就进入收集逻辑
- 首先是
dep.depend()
,我们姑且先不往里看,看字面意思就是调用dep.depend
方法将依赖收集到dep
这个容器里,也就是容器1,这是用于setter
里dep.notify()
触发依赖更新 - 然后判断
childOb
是否存在,由上可知这个就是observe(data.a)
的返回值,因为data.a
是个对象,所以就有值,而childOb === ob2
,所以childOb.dep.depend()
就是把当前依赖还收集到容器2
这个是因为没有Proxy
,js
不能监听到属性的新增,那么我们得想个法子把这个依赖给收集了
window.app = new Vue({
data: {
a: {
b: 1
}
},
methods: {
change() {
this.$set(this.a, 'c', 6)
}
},
watch: {
'a.c': function (nVal, oVal) {
console.log(nVal, oVal)
},
'a.b': function (nVal, oVal) {
console.log(nVal, oVal)
}
},
template: `<div>
<p class="title">Vue.set</p>
<button @click="change">Change a.c</button>
</div>`
}).$mount('#app')
如此例所示,初始化完了之后$data
如下
{
a: { // dep是[{ expression: "a.c" }, { expression: "a.b" }]
b: 1, // dep是{ expression: "a.b" }
__ob__: {
value: {
b: 1
},
dep: [{
expression: "a.c"
}, {
expression: "a.b"
}],
vmCount: 0
}
},
__ob__: {
value: {
a: {
b: 1
}
},
dep: [],
vmCount: 1
}
}
假设我们this.a.c = 2
,这里必然不能触发a.c的getter
,因为它根本就没有。那么我们就得调用defineReactive()
将a.c
转成响应式数据
然后我们就得想办法触发依赖,为什么不是收集依赖呢?因为this.a.c = 2
这句语句就得触发依赖,其实触发依赖就会重新计算值,自然也会收集依赖
既然触发依赖,那么就得收集依赖,这也是childOb.dep.depend()
的存在意义,所以我们只需要调用ob.dep.notify()
即可,这个功能也就是Vue.set
如下
function set(target, key, val) {
const ob = target.__ob__
defineReactive(ob.value, key, val)
ob.dep.notify()
}
- 最后判断这个值是否是数组,若是数组的话就
dependArray(value)
function dependArray(value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
可见其只是简单的遍历这个数组,然后判断子项是否有.__ob__
属性。我们知道只有这个子项是对象,那么才有这个属性,所以同第2条,对象需要把依赖收集到对应的dep(依赖容器)
。判断子项是否是数组,是的话递归即可
触发依赖
然后就是setter
,也就是触发依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
set
也做了俩件事:
- 设置属性值
首先计算当前值,因为能拿到将设置的新值(newVal
),那么我就需要判断下新旧值有没有变化,没变化的话自然就return
就是了。值得注意的是(newVal !== newVal && value !== value)
,这个就是NaN
,也就是新旧值都是NaN
,那么自然也是无变化
然后就是customSetter
,这个就是一个设置值得时候的提示,我们可以看$listeners
可见设置时就会触发defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => { !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm) }, true)
customSetter
函数,也就是提示$listeners is readonly.
接下来我们判断是否有setter
,它存有该属性原有的setter
,存在的话就调用setter
设置新值,这样子就保证其原有的设置操作不被影响,否则的话简单val = newVal
即可 - 触发依赖
因为新值可能是对象也可能是非对象基本数据类型,所以调用observe
观测,且赋值给childOb
,这是因为旧值被覆盖了,那么childOb
自然也无效了,得重新计算
然后dep.notify()
触发yila即可
后言
这章节主要讲的是如何将数据转化为响应式数据,但是关于数组的观测、新增属性观测(提到一点)都没有讲,下章节继续