Vue侦测对象变化的原理


如何追踪变化

在JS中有两种方法可以侦测到变化:

  1. Object.defineProperty
  2. ES6的Proxy

Vue2中使用的是第一种方法,即Object.defineProperty,这是因为当时ES6在浏览器中的支持度并不理想。

利用Object.defineProperty侦测对象

    function defineReactive(data, key, val){
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function(){
          console.log('get');
          return val;
        },
        set: function(newVal){
          console.log('set');
          if(val===newVal){
            return 
          }
          val = newVal
        }
      })
    }

每当从data的key中读取数据时,get函数被触发,每当往data的key中写入数据时,set函数被触发。

如何收集依赖

把用到数据的地方收集起来,然后等属性发生改变时,把之前收集好的依赖循环触发一遍。

在getter中收集依赖,在setter触发依赖

依赖收集在哪里

    // 收集依赖
    class Dep{
      constructor(){
        this.deps = [];
      }
      addDeps(dep){
        this.deps.push(dep);
      }
      // 添加依赖
      depend(){
        if(target){
          this.addDeps(target);
        }
      }
      // 移除依赖
      removeDep(dep){
        if(this.deps.length){
          let id = this.deps.indexOf(dep);
          if(id>-1){
            this.deps.splice(id,1);
          }
        }
      }
      // 依赖更新并通知
      notify(){
        let deps = this.deps.slice();
        deps.forEach((dep)=>{
          dep.update();
        })
      }
    }
    // 侦测变化
    function defineReactive(data, key, val){
      let dep = new Dep();
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get(){
          console.log('get');
          dep.depend();
          return val
        },
        set(newVal){
          console.log('set');
          if(val === newVal){
            return;
          }
          val = newVal;
          dep.notify()
        }
      })
    }
    // 自定义的依赖
    function Target(name){
      this.name = name,
      this.update = function(){
        console.log(`${this.name} has been updated!`)
      }
    }
    let target = new Target('watcher')

依赖收集的代码封装成一个Dep类,用于管理依赖,通过这个类,我们可以收集依赖、删除依赖或者向依赖发送通知等。

Watcher

数据变化时,曾经使用了该数据的地方有很多,而且类型可能还不一样,既有可能是模板,也有可能是用户写的一个watch。所以,我们需要抽象出一个能集中处理这些情况的类。
然后,在收集依赖阶段,只收集这个封装好的类的实例进来,通知也只通知它一个,它再负责通知其他地方,这个抽象的东西(类),就叫做Watcher .

// 什么是watcher?watcher抽象类
export default class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    // 执行this.getter(),就可以读取data.a.b.c的内容
    this.getter = parsePath(expOrFn) // parsePath是一个读取一个字符串keypath的函数,这里不再列举
    this.cb = cb
    this.value = this.get()
  }
  get() {
    window.target = this
    let value = this.getter.call(this.vm, this.vm)
    window.target = undefined
    return value
  }
  update() {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

在get方法中先把window.target设置成了this,也就是当前的watcher实例。然后读取数据时,就会触发getter(收集依赖的逻辑),并把依赖添加到Dep中。
依赖注入到Dep中后,每当数据发生变化时,就会让所有的依赖循环触发update方法,而update方法会执行参数中的回调函数,将value和oldValue传到参数中。

递归侦测所有key

class Observer {
        constructor(value) {
          this.value = value;
          if (!Array.isArray(value)) {
            this.walk(value);
          }
        }
        // 侦测对象的所有属性
        walk(obj) {
          const keys = Object.keys(obj);
          keys.forEach((key) => {
            defineReactive(obj, key, obj[key]);
          });
        }
      }
    // 收集依赖
    class Dep{
      constructor(){
        this.deps = [];
      }
      addDeps(dep){
        this.deps.push(dep);
      }
      // 添加依赖
      depend(){
        if(target){
          this.addDeps(target);
        }
      }
      // 移除依赖
      removeDep(dep){
        if(this.deps.length){
          let id = this.deps.indexOf(dep);
          if(id>-1){
            this.deps.splice(id,1);
          }
        }
      }
      // 依赖更新并通知
      notify(){
        let deps = this.deps.slice();
        deps.forEach((dep)=>{
          dep.update();
        })
      }
    }
      // 侦测变化
      function defineReactive(data, key, val) {
        // 侦测子属性
        if(typeof val === 'object'){
          new Observer(val);
        }
        let dep = new Dep();
        Object.defineProperty(data, key, {
          enumerable: true,
          configurable: true,
          get() {
            console.log("get");
            dep.depend();
            return val;
          },
          set(newVal) {
            console.log("set");
            if (val === newVal) {
              return;
            }
            val = newVal;
            dep.notify();
          },
        });
      }
    // 自定义的依赖
    function Target(name){
      this.name = name,
      this.update = function(){
        console.log(`${this.name} has been updated!`)
      }
    }
    let target = new Target('watcher')

    let person1={
    };
    let grade={
      Math: 98,
      Eng: 90
    }
    defineReactive(person1,'grade',grade);

上面代码定义的Observer类将一个正常的object转换成被侦测的object(响应式的object)。也就是说,只要将一个对象传到Observer中,那么这个对象就会变成响应式的了。

关于object的问题

Vue.js通过Object.defineProperty将对象的key转换成getter/setter的形式来追踪变化,但Object.defineProperty无法追踪属性的增加和删除,这是因为在ES6之前JS没有提供元编程的能力(Proxy和Reflect)。

为了解决这个问题,Vue.js提供了两个API:

  • vm.$set
  • vm.$delete

总结

  • 初始化数据Data通过Observer转换成了 getter/setter 的形式来追踪变化。
  • 当外界通过 Watcher 读取数据时(watcher订阅的数据),会触发getter将watcher添加到依赖中。
  • 当订阅的数据发生变化时,会触发setter,向Dep中的依赖(watcher)发送通知。
  • Watcher 接收到通知后,会向外界发送通知,触发视图的更新。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值