Vue的MVVM模式响应式原理之observe、Observer和defineReactive

1,868 阅读7分钟

谈谈我对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()使属性具有getset达到了[响应式]。也就是在访问和修改的时候可以做出某种反应。

defineReactive函数的产生和闭包。

defineReactive(obj,key,value)函数就是对Object.defineProperty()的封装,并利用了一个闭包特性,来缓存它接收3个参数。它直接负责让对象的属性成为[响应式]的。

  1. @obj 要定义属性的对象
  2. @key 要定义的属性名
  3. @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__属性,表示这个数据被观察了。

  • 参数
  1. @value : 是一个对象类型的值。非对象类型的值无法添加属性呀。
  • 方法(本文要讨论的)
  1. walk: 用来遍历对象使成员都具有[响应性]
  2. 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