Vue双向绑定原理

MVVM 数据双向绑定

MVVM 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。

2.png

通过实现以下 4 个步骤,来实现数据的双向绑定:

1、实现一个监听器 Observer ,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;

2、实现一个订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理;

3、实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图;

4、实现一个解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。

以上四个步骤的流程图表示如下:

3.png

监听器 Observer 实现

监听器Observer的底层原理是Object.defineProperty,传入一个对象,实现对此对象的监听,包括以下:

  • 遍历对象的所有属性,都增加get/set方法
  • 通过递归调用observer实现对对象的深度监听
  • 如果给属性赋的新值是一个对象,对这个新值递归observer 实现监听

具体看Object.defineProperty与Proxy

class Observer {
  constructor(data) {
    this.observer(data);
  }
  observer(obj) {
    if (obj && typeof obj === 'object') {
      // 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
      for (let key in obj) {
        this.defineRecative(obj, key, obj[key])
      }
    }
  }
  // obj: 需要操作的对象
  // attr: 需要新增get/set方法的属性
  // value: 需要新增get/set方法属性的取值
  defineRecative(obj, attr, value) {
    // 如果属性的取值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
    this.observer(value);
    Object.defineProperty(obj, attr, {
      get() {
        return value;
      },
      set: (newValue) => {
        if (value !== newValue) {
          // 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
          this.observer(newValue);
          value = newValue;
          console.log('监听到数据的变化');
        }
      }
    })
  }
}

实现解析器Compile

通过监听器 Observer 订阅器 Dep 和订阅者 Watcher 的实现,其实就已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析 dom 节点,而是直接固定某个节点进行替换数据的。

compile主要做的事情是:

  • 在生命周期的initState方法中将data,prop,watch、computed等通过observe方法(底层是Object.defineProperty)数据劫持,
  • 在inintRender中解析模板指令,将模板中的指令v-model、v-text、{{}}}和生成Watcher实例,通过watcher实例将指令与数据建立依赖(用Dep.target实现添加Watcher进入Dep容器)
  • 并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图.

使用观察者设计模式,创建Dep和Wather类

1,在解析模板,收集data中各个属性在模板中被引用的dom节点集合,当该数据改变时,更新依赖了该数据的dom节点集合,就实现了数据驱动页面更新。

2,创建Dep类和Watcher类

  • Dep:用于收集某个data属性依赖的dom节点集合,并提供更新方法
  • Watcher:每个dom节点的包裹对象

订阅器 Dep 实现

发布 —订阅设计模式

发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态改变时,所有依赖于它的对象都将得到通知。

在数据被读或写的时候通知那些依赖该数据的视图更新,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是前一节所说的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。

 现在,我们需要创建一个依赖收集容器,也就是消息订阅器 Dep,用来容纳所有的“订阅者”。订阅器 Dep 主要负责收集订阅者,然后当数据变化的时候后遍历执行订阅者的更新函数。

Watcher和Observer之间的纽带,每一个Oserver都有一个Dep订阅器实例,用来存储订阅者Watcher

Dep总结(自己总结的,可能有误):

  • 通过var dep = new Dep(),新建个数组容器subs,用于收集某个data属性依赖的dom节点(收集订阅者Watcher)集合
  • 如何收集(依赖)订阅者Watcher:
    • 在Vue实例初始化时,Compile解析模板,将V-modol、{{}}等指令解析并创建订阅者Watcher对象
    • Watcher对象在初始化时的构造函数会调用Watcherget()方法,Watcherget()方法中将Dep.target指向该dom节点,然后强行触发Object.definProperty的getter方法
    • 在getter方法里面添加Dep.target到数组subs(即添加订阅者)就完成了一次收集。因为每次触发getter之前都对该静态变量赋值,所以不存在收集错依赖的情况。
  • 数据变化时,执行notify()方法来遍历执行容器内所有Watcher的更新函数

部分简化源码理解:

function Dep () {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null;



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

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];//更新的话不会再绑定target,就不会再加入订阅器
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this; // 全局变量 订阅者 赋值
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null; // 全局变量 订阅者 释放
        return value;
    }
};

// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
        if(Dep.target) dep.addSub(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 调用订阅者的update方法,通知变化
        });
    }
};

订阅者 Watcher 实现

订阅者 Watcher 分析如下:

订阅者 Watcher 是一个 类,在它的构造函数中,定义了一些属性:

  • **vm:**一个 Vue 的实例对象;
  • **exp:**是 node 节点的 v-model 等指令的属性值 或者插值符号中的属性。如 v-model="name"exp 就是name;
  • **cb:**是 Watcher 绑定的更新函数;

Watcher 在初始化的时候将自己添加进订阅器 Dep的步骤:

注意:只需要在订阅者 Watcher 初始化的时候才需要添加订阅者,所以需要做一个判断操作,用Dep.target来进行判断。

  1. 实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入 get 函数,首先会执行:
    1. Dep.target = this; // 将自己赋值为全局的订阅者
    2. let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
  2. 在这个过程中会对 vm 上的数据访问,其实就是为了触发监听器中的 getter
    在getter方法里面添加Dep.target到数组subs(即添加订阅者)就完成了一次收集。完成依赖收集后,还需要把 Dep.target 恢复null状态。
  3. 因为每次触发getter之前都对该静态变量赋值,所以不存在收集错依赖的情况。

订阅器的update()函数

  • 在监听器setter()监听到属性变化时,调用dep.notify()函数,notify将遍历subs容器里的所有订阅器Watcher的update函数执行。
  • update() 函数是用来当数据发生变化时调用 Watcher 自身的更新函数进行更新的操作
  • 先通过 let value = this.vm.data[this.exp]; 获取到最新的数据,然后将其与之前 get() 获得的旧数据进行比较,如果不一样,则调用回调函数 cb 进行更新。

具体步骤

第一步:
需要observer的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么久能监听到了数据变化

第二步:
compile解析横板令,将模板中的变量替换成数据.然后初始化渲染页面视图,并将每个令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

第三步:
Watcher订阅名是 observer和 Compile之间通信的桥梁,主要做的事情是:
1.在自身实例化时往属性订倒器(dep)里面添加自己
2.自身必须有一个 update()方法
3.待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中定的回调,则功成身退

第四步:
MVVM作为数据绑定的入口,合 observer、 Compile和 Watcher三者,通过 Observer来监听自己的model数据変化,通过 Compile来解析编译模板指令,最终利用 Watcher搭起 Observer和 Compile之间的通信标梁,达到数据变化->视图更新新:视图交互变化(Input)->数据mode变更的双向绑定效果。

Observer

将一个正常的object转换成每个层次都是响应式

数组的响应式处理

正因为我们可以通过Array原型上的方法来改变数组的内容,所以ojbect那种通过getter/setter的实现方式就行不通了。

ES6之前没有提供可以拦截原型方法的能力,我们可以用自定义的方法去覆盖原生的原型方法。

Vue是通过改写数组的七个方法(可以改变数组自身内容的方法)来实现对数组的响应式处理

这些方法分别是:pushpopshiftunshiftsplicesortreverse
在这里插入图片描述

这七个方法都是定义在Array.prototype上,要保留方法的功能,同时增加数据劫持的代码

思路:就是 以Array.prototype为原型,创建一个新对象arrayMthods
然后在新对象arrayMthods上定义(改写)这些方法
定义 数组 的原型指向 arrayMthods

这就相当于用一个拦截器覆盖Array.prototype,每当使用Array原型上的方法操作数组时,其实执行的是拦截器中提供的方法。在拦截器中使用原生Array的原型方法去操作数组。

import def from "./def";

const arrayPrototype = Array.prototype;

// 以Array.prototype为原型创建arrayMethod
export const arrayMethods = Object.create(arrayPrototype);

// 要被改写的7个数组方法
const methodsNeedChange = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

// 批量操作这些方法
methodsNeedChange.forEach((methodName) => {
  // 备份原来的方法
  const original = arrayPrototype[methodName];

  // 定义新的方法
  def(
    arrayMethods,
    methodName,
    function () {
      console.log("array数据已经被劫持");

      // 恢复原来的功能(数组方法)
      const result = original.apply(this, arguments);
      // 把类数组对象变成数组
      const args = [...arguments];

      // 把这个数组身上的__ob__取出来
      // 在拦截器中获取Observer的实例
      const ob = this.__ob__;

      // 有三种方法 push、unshift、splice能插入新项,要劫持(侦测)这些数据(插入新项)
      let inserted = [];
      switch (methodName) {
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }

      // 查看有没有新插入的项inserted,有的话就劫持
      if (inserted) {
        ob.observeArray(inserted);
      }

      return result;
    },
    false
  );
});

参考:

https://juejin.cn/post/6844903903822086151

https://segmentfault.com/a/1190000006599500

https://juejin.cn/post/7065967379095748638#heading-6

阅读剩余
THE END