谈谈我对Vue-MVVM模式中数据响应式的原理
笔者会竭尽所能的减少复杂逻辑描述,和需要阅读的时间。
关键字 observe、Observer、defineProperty、defineReactive、def
结论
1.observe函数
:是一个入口函数,并提供一个出口。
2.Observer类
: 实例化的过程中对实例添加__ob__属性,并遍历data
3.defineReactive函数
: 直接实现将数据转换为响应化的函数
顺序是1 => 2 => 3 => 1 ... 间接递归的出口在 1 当传入的值不是对象时。
这好像是所有学习Vue源码中最开始的部分。笔者初学前端半年,最近认真学习了一下MVVM模式的实现。希望把知识简单的呈现给读到的朋友们。这个部分的知识其实不复杂,主要就是围绕着Object.defineProperty()这个API展开的。
本文抛开 options,只抽取data数据来做说明,也不谈数据代理,就单纯说说
[响应式]
这张图回头再看就好了。
一、如何知道使用者访问/修改了数据呢?——defineReactive函数
借助Object.defineProperty()
方法,直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。来自MND的原话
let data = {}
let val = '一个属性值'
Object.defineProperty(data,'obj',{
get(){
console.log('访问数据触发get' + '您试图访问' + 'obj' + '属性:', val,"还想干点别的吗?")
return val
},
set(newVal){
console.log('修改数据触发set' + '您试图修改' + 'obj' + '属性:', newVal,"还可以做很多事情!")
val = newVal
}
})
data.obj // "访问数据触发get您试图访问obj属性:" "一个属性值""还想干点别的吗?"
data.obj = '另一个属性值' //"修改数据触发set您试图修改obj属性:" "另一个属性值""还可以做很多事情!"
上述代码表明,使用Object.defineProperty()
使属性具有get
和set
达到了[响应式]
。也就是在访问和修改的时候可以做出某种反应。
defineReactive函数的产生和闭包。
defineReactive(obj,key,value)
函数就是对Object.defineProperty()
的封装,并利用了一个闭包特性,来缓存值
它接收3个参数。它直接负责让对象的属性成为[响应式]
的。
@obj
要定义属性的对象@key
要定义的属性名@value
要定义的属性值。上文中我们利用了一个变量val来保存值,在Vue中则是利用了一个闭包
环境来保存值,也就是这个参数
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log('访问数据触发get' + '您试图访问' + key + '属性', value)
return value
},
set(newVal) {
if (newVal === value) {
return
}
console.log('修改数据触发set' + '您试图修改' + key + '属性', newVal)
value = newVal
}
})
}
let data = {
a: {
b: {
c: 3,
}
},
d: [33, 44, 55]
}
defineReactive(data,'a',data.a)
data.a //"访问数据触发get您试图访问a属性:" {b:{c:3}} "还想干点别的吗?"
有了这个函数,我们就可以指定任何一个属性让它具有[响应式]
二、如何让所有的属性都成为响应式?并且Vue知道它已经是响应式的了?——Observer类
到这里,大家应该发现了defineReactive函数是一个公用的方法,它可以在很多地方被调用。在Observer实例化过程中就会调用它。
Observer是一个间接实现数据响应式的类。接受1个参数。在new的时候会为这个数据添加__ob__
属性,表示这个数据被观察了。
- 参数
@value
: 是一个对象类型的值。非对象类型的值无法添加属性呀。
- 方法(本文要讨论的)
walk
: 用来遍历对象
使成员都具有[响应性]
observeArray
: 用来遍历数组
使元素都具有[响应性]
(1)通过__ob__标识判断一个属性是否已经被观察过了。
class Observer(value){
constructor() {
def(value,'__ob__',this,false)
// 执行可以遍历 value 的方法....
}
//...
}
// 在Vue中封装了一个 def函数用来定义访问器属性
function def(obj,key,value,enumerable){
Object.defineProperty(obj, key, {
value: value,
enumerable: enumerable,
writable: true,
configurable: true
})
}
let ob = new Observer({a:{b:1})
console.log(ob.a.__ob__ instanceof Observer) // true
这样一来每个被传递进来的value都会多一个__ob__
属性,同时说明它被响应化
了,同时我们发现__ob__
的值设置为this
。而在类的constructor方法
中this
指向的是实例,那么Observer在哪里被实例化呢?接着往下看,很快就达到了。
(2)遍历所有层次的属性,让所有的属性都是[响应式]
的。
1. 由于非对象
类型的值无法遍历,也无法添加__ob__
属性,所以应该排除它。
2. 这点很好理解,所以在实例化的时候就应该排除,这里引出observe公共
方法,它就是用来排除非对象
类型值的。
observe
函数大体的工作就是这样。稍后详细说。
function observe(value){
//判断value是否为对象...
if(){
//不是对象就return
}
return new Observer(value)
}
3. 但是对象
和数组
也要区分。原因是因为Object.defineProperty()方法无法对数组长度的变化做出[响应性]
。(本文不展开说明~日后再写。)
class Observer(value){
constructor() {
def(value,'__ob__',this,false)
if(Array.isArray(value)){
observeArray(value) `是数组`
}else{
walk(value) `不是数组`
}
}
walk(value){
//遍历对象,为每个属性调用正常的 ——defineReactive()
}
observeArray(value){
//遍历数组,为每个元素调用特殊的 ——defineReactive()
}
}
到这里我们就完成了对new Observer(value)实例化时,对传入参数value
所有属性的遍历并且实现响应式,但这仅仅是第一层
。
现在思考
如果value
中的成员有的也是对象
,或者数组
怎么办呢?
在调用defineReactive
的时候,再判断一次,这个工作是oberve在做,因此调它就好了。
function defineReactive(obj, key, value) {
observe(value)
Object.defineProperty(obj, key, {
//...set...get...
})
}
三、Observer类在observe函数中实例化
observe函数内容很少,它是一切的入口
和唯一的出口
,只有调用observe()
并传入一个有效value
时,一切才开始。
在Vue
源码中它在initData,也就是数据初始化阶段调用,传入的值是data
。也就是用户传入的那个data的副本
function observe(value) {
if (!value || typeof value !== 'object') return; 判断传入的值是否是对象,若不是返回到调用它的主函数上
let ob
//这里是判断这个对象是否已经有__ob__属性了,避免重复观察
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__
} else {
ob = new Observer(value)
}
//如果是对象那么久返回这个数据对应的Observer实例
return ob
}
let data = {
a: {
b: {
c: 3,
}
},
d: [33, 44, 55]
}
observe(data)// 观察data所有层次的属性
data.a.b.c // 您试图访问 a >> 您试图访问 b >> 您试图访问 c
data.d.push(66) // 您试图修改 d
通过observe(data),就可以使data所有层次的属性都具有[响应式]
。这整个流程经过了3道流程,是一个链式的循环调用过程,可以理解成间接的递归
。
至此一张说明[响应式]
的流程图
在间接递归开始之前,有一只手就是实例化Vue,它会按下调用第一个observe(data)
的按钮,响应式
的一起也就被启动了。
题外话,初学observe
的时候,以为它是一个小个头,后面才知道它是Vue-MVVM模式的核心入口之一
。
下篇文章写打算写Vue的MVVM模式响应式原理——数组的特殊处理之偷梁不换柱
。
下下篇文章写就写Vue的MVVM模式响应式原理——如何追踪变化之Dep、Watcher、Observer。
本文的书写顺序不知道会不会影响读者的理解,如果可以的话,点个赞
,或者点个倒赞
或评论
给我你的意见把!
本文详细的源码和注释在我的GitHub仓库中。mvvm-webpack-demo