Vue-MVVM数据双向绑定响应式原理之Object.defineProperty

Vue-MVVM数据双向绑定响应式原理之Object.defineProperty_第1张图片

结合项目食用更佳

一、Vue数据双向绑定原理:

Vue内部通过Object.defineProperty方法属性拦截的方式,把data对象里每个数据的读写转化成getter/setter,并添加相应的watcher,当数据变化时watcher通知视图更新

二、MVVM数据--v-model双向绑定:

数据变化更新视图,视图变化更新数据。

1.输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
2.data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。

问题:数据变化如何更新视图?
1.如何知道数据发生了变化
2.数据在什么时候发生改变
3.剩下,我们只需在数据变化的时候去通知视图更新即可

三、数据的观测性--getter,setter:

是Vue底层的数据的取值和给值

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

1.一般情况下我们定义个对象,但是这种定义方式,并不知道什么时候修改或者读取属性,换句话说,不具备观测性。
 let car = {
    'color':'blue',
    'price':3000
}
 console.log(car.color) //blue

2.我们试着用Object.defineProperty去改写一下,car已经可以主动告诉我们它的属性的读写情况了,也意味着,这个car的数据对象已经是“可观测”的了。
    let car = {}
    let val = 3000
    Object.defineProperty(car, 'price', {
      get() {
          console.log('price属性被读取了')
          return val
      },
      set(newVal) {
          console.log('price属性被修改了')
          val = newVal
      }
  })
  car.price=5000  
  console.log(car.price)// price属性被修改了  price属性被读取了 3000

3. 现在把对象所有属性都变得可观测,我们可以编写如下两个函数:
function observable (obj) {
   if (!obj || typeof obj !== 'object') {
       return;
   }
   let keys = Object.keys(obj);
   keys.forEach((key) =>{
      defineReactive(obj,key,obj[key])
   })
   return obj;
 }
 /**
  * 使一个对象转化成可观测对象
  * @param { Object } obj 对象
  * @param { String } key 对象的key
  * @param { Any } val 对象的某个key的值
*/
 function defineReactive (obj,key,val) {
    Object.defineProperty(obj, key, {
         get(){
                console.log(`${key}属性被读取了`);
                return val;
              },
     set(newVal){
                console.log(`${key}属性被修改了`);
                val = newVal;
            }
        })
}              

现在,我们就可以这样定义car,这个car对象里的所有对象都是可观测的。
let car = observable({
        'brand':'BMW',
        'price':3000
})

四. 依赖收集(Observer):

Vue-MVVM数据双向绑定响应式原理之Object.defineProperty_第2张图片

让我们知道数据在什么时候被读或写,即可以进行下一步操作

  1. 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
  2. compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

一旦数据发生变化,实时刷新

“发布订阅者”模式:
数据变化为“发布者”,依赖对象为“订阅者”。
 

我们设计了一个订阅器Dep类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,new.target必须写在构造方法里面,它指向类本身。具体指向哪个类这是一个全局唯一 的Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组,我们将订阅器Dep添加订阅者的操作设计在getter里面,这是为了让Watcher初始化时进行触发,因此需要判断是否要添加订阅者。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。

1.创建一个依赖收集容器(消息订阅器Dep),用来容纳所有的“订阅者”。订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数
class Dep {
        constructor(){
            this.subs = []
        },
        //增加订阅者
        addSub(sub){
            this.subs.push(sub);
        },
        //判断是否增加订阅者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        },

        //通知订阅者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
    }
    Dep.target = null;

2.有了订阅器,再将defineReactive函数进行改造一下,向其植入订阅器:
function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}属性被读取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}属性被修改了`);
                dep.notify()   //数据变化通知所有订阅者
            }
        })
    }

五、订阅者watcher:

Watcher订阅者是Observer和Compile之间通信的桥梁

主要做的事情是:

①在自身实例化时往属性订阅器,在函数取值时(dep.target)里面赋值this指向

②自身必须有一个update()方法

③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,我们只需要在Watcher初始化的时候触发对应的get函数去调用即可


watch允许执行异步操作 


如何触发?
获取对应的属性值就可以

我们只要在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者Watcher的实现如下

class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 将自己添加到订阅器的操作
        },

        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            },
        get(){
            Dep.target = this;  // 缓存自己
            let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放自己
            return value;
        }
    }

    订阅者Watcher 是一个 类,在它的构造函数中,定义了一些属性:
    vm:一个Vue的实例对象;
    exp:是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name;
    cb:是Watcher绑定的更新函数;

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到:

数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入 get 函数,首先会执行:

Dep.target = this;  // 缓存自己

实际上就是把 Dep.target 赋值为当前的渲染 watcher ,接着又执行了:

let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数

在这个过程中会对 vm 上的数据访问,其实就是为了触发数据对象的getter。

每个对象值的 getter都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行this.addSub(Dep.target),即把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target 恢复成上一个状态,即:

Dep.target = null;  // 释放自己

因为当前vm的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。而update()函数是用来当数据发生变化时调用Watcher自身的更新函数进行更新的操作。先通过let value = this.vm.data[this.exp];获取到最新的数据,然后将其与之前get()获得的旧数据进行比较,如果不一样,则调用更新函数cb进行更新。

六、使用这玩意儿进行数据劫持有什么缺点(Object.defineProperty())

有些无法拦截:[数组:大部分操作拦截不到]

属性进行操作时,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,Object.defineProperty 不能拦截到这些操作,Vue 内部通过重写函数的方式解决了这个问题

在 Vue3.0 中不使用Object.defineProperty了,改用 Proxy 对对象进行代理,实现数据劫持。

Proxy可以做到的监听到任何方式的数据改变,唯一的缺点是兼容性问题,因为 Proxy 是 ES6 的语法

七、MVVM、MVC、MVP的区别

共同点:分离关注点的方式来组织代码结构,优化开发效率

(1)MVC

Vue-MVVM数据双向绑定响应式原理之Object.defineProperty_第3张图片

   通过分离 Model、View 和 Controller 的方式来组织代码结构


   View :UI视图,负责数据的展示
   Model :负责存储页面的业务数据,及对相应数据的逻辑操作

   

   View 和 Model 应用了观察者模式,Model 层改变, View 层会实时更新

   详细看下: 观察者模式具体解说

   Controller :Controller 层是 View 层和 Model 层的纽带
                       主要负责用户与应用的响应操作,及用户与页面产生交互

   缺点:View 层和 Model 层耦合在一起,Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题

(2)MVVM
   

Vue-MVVM数据双向绑定响应式原理之Object.defineProperty_第4张图片



    MVVM 分为 Model、View、ViewModel:

    View :UI视图,负责数据的展示
    Model :数据模型,负责存储页面的业务数据,及对相应数据的逻辑操作
    ViewModel:负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

    Model和View并无直接关联,而是通过ViewModel来进行联系
    当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步,数据自动同步,开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM

(3)MVP
     
MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller

     MVC会造成代码的混乱,MVP 模式解决这一点。
     MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新
这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。

你可能感兴趣的:(vue,前端,javascript,vue.js)