vue响应式原理 对象篇

前言

vue是通过数据驱动视图,如何知道数据发生改变,改变后又如何通知视图改变:

最近作者再面试中遇到了一个这样的问题:说一下有关VUE2对数据的变化侦听。在Vue2中对数据变化的侦听主要分成两种,一种是通过Object.defineProperty方法对Object对象的侦听,一种是对数组方法重写的方式去侦听有关数组的变化。

这篇文章主要是梳理一下,有关vue中对数据侦听的一些内容,当是笔者学习源码的一个方式吧,如果有任何写的不对的地方,麻烦评论区留言。

(这一篇主要是对对象数据的侦听)

主要包括一些知识点:

image.png

1. Object.defineProperty

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

注意几个关键的点:

  • 可以在对象上定义一个新的属性
  • 修改一个对象的现有属性

这个方法接收三个参数

Object.defineProperty(obj, prop, descriptor)

你在哪个对象上使用该方法,你在对象的哪个属性(名称)上进行操作,以及,你操作的属性描述符是什么。最后返回一个对象

这些操作符包括:configurable(是否可配置),enumerable(是否可枚举),value(属性对应的值),writable(是否可写),get(执行访问属性),set(属性值被修改)

  • 拥有布尔值(true,flase): configurableenumerablewritable 的默认值都是 false
  • 属性值和函数: valuegetset 字段的默认值为 undefined

1.1 使用Object.defineProperty

下面我定义了一个对象jing,它有一个属性age,我们可以对这个属性进行读取,修改。

let jing = {
    age:22
}

console.log(jing.age) // 读取数据
jing.age = 18 // 修改数据
console.log(jing.age) // 读取数据
// 22
// 18

但是我们无法知道它上面时候被修改,被读取,而通过Object.defineProperty,重写这个获取和修改的操作则可以达到

let jing = {}
let val = 22
Object.defineProperty(jing, 'age', {
  enumerable: true,
  configurable: true,
  get(){
    console.log('age属性被读取了')
    return val
  },
  set(newVal){
    console.log('age属性被修改了')
    val = newVal
  }
})

console.log(jing.age)
jing.age = 18
console.log(jing.age)

这个jing的属性被读取或修改时,能够让jing主动告诉我们,它的属性被修改或者是被获取

image.png

2 对Object的侦听和响应

2.1 Object数据的侦测 Observer

源码在 src>core>observer>index>Observer(类) 这里我贴出的代码和源码有些出入,学习的话请自己拉下来看哦

export class Observer {
    constructor (value: any) {
      this.value = value
      this.dep = new Dep()
      this.vmCount = 0
      def(value, '__ob__', this)
      if (Array.isArray(value)) {
        // 如果是数组 走这里的方法
      } else {
        this.walk(value) // 如果是对象,通过walk方法转可观测对象
      }
    }
    walk (obj: Object) {
      const keys = Object.keys(obj) // 把对象的所有属性 存放到keys里面
      for (let i = 0; i < keys.length; i++) { // 遍历这些属性值
        defineReactive(obj, keys[i])
      }
    }
}

上面定义的Observer类可以将一个object转换成一个可被观测的object,通过遍历它里面的属性,对里面的属性进行操作,通过defineReactive,下面我们来看看这个方法

export function defineReactive (
    obj: Object,
    key: string,
    val: any,
  ) {  
    // 如果只传了两个参数,说明只要obj和key
    if (arguments.length === 2) {
      val = obj[key]
    }
  
    // 使用Object.defineProperty 对数据进行处理
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        console.log(`${key}属性被读取了`)
        return val
      },
        set: function reactiveSetter(newVal) {
            if (val === newVal) {
              return
            }
            console.log(`${key}属性被修改了`)
          val = newVal
      }
    })
  }

defineReactive,就是做了我们最开始讲的通过Object.defineProperty,去实现对象的可观测

比如说我们把之前的哪个jing对象通过Observer变成可观测的,就可以这么写:

let jing = new Observer({
     age: 22
})

2.2 依赖收集器 Dep

又回到最开始说的,vue是通过数据驱动视图,现在我们知道的只是数据什么时候发生改变了,那么当数据改变后,又是如何通知视图发送改变的呢?在一个时刻可能有不同的数据发生改变,要做的应该是哪个数据发生了改变,就更新哪个数据对应的视图。

image.png

可是数据和视图的对应关系,并不是一对一的,他们可能是这样的:每个数据也可能影响多个地方的视图

image.png

就是说每个数据它都可能影响多个视图,假设是一个依赖数组(存放会受影响的视图),一旦数据被使用,就创建一个依赖数组,让这个依赖数组去收集依赖,如果数据发生了改变就去通知依赖更新。

之前以及将object变成可观测的数据了,因此我们可以知道它上面时候被使用(被获取时会触发getter属性,那么我们就可以在getter中收集这个依赖),什么时候发生了改变(当这个数据变化时会触发setter属性,那么我们就可以在setter中通知依赖更新)

当object数据被使用的时候,在getter中收集这个依赖,那么在vue中如何实现这个依赖收集?(也就是之前说的收集依赖的数组)

在vue里面有个叫依赖管理器Dep类:

src/core/observer/dep.js

export default class Dep {
    static target: ?Watcher; // 静态属性
    id: number;
    subs: Array<Watcher>;
  
    constructor () {
      this.id = uid++
      this.subs = []
    }
  
    // 添加
    addSub (sub: Watcher) {
      this.subs.push(sub)
    }
  
    // 删除
    removeSub (sub: Watcher) {
      remove(this.subs, sub)
    }
    // 收集
    depend () {
      if (Dep.target) {
        Dep.target.addDep(this)
      }
    }
  
    // 通知所有的依赖更新
    notify () {
      const subs = this.subs.slice()
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        subs.sort((a, b) => a.id - b.id)
      }
      for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }
  }

这个一个Dep类,在里面定义了一个this.subs = []用来存放依赖,并且有一些增加删除的方法,最后有一个通知所有依赖进行更新的 notify ()方法

有了这个依赖收集器,再看看如何对我们的数据进行处理:
在之前的将object转换成可观测数据的类里面加上这个Dep

export function defineReactive (
    obj: Object,
    key: string,
    val: any,
) {
  const dep = new Dep()
    // 如果只传了两个参数,说明只要obj和key
    if (arguments.length === 2) {
      val = obj[key]
    }
  
    // 使用Object.defineProperty 对数据进行处理
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        console.log(`${key}属性被读取了`)
        dep.depend()
        return val
      },
        set: function reactiveSetter(newVal) {
            if (val === newVal) {
              return
            }
            console.log(`${key}属性被修改了`)
          val = newVal
          dep.notify()
      }
    })
  }

2.3 依赖的更新 Watcher

每个数据它都可能影响多个视图,假设是一个依赖数组(存放会受影响的视图),一旦数据被使用,就创建一个依赖数组,让这个依赖数组去收集依赖,如果数据发生了改变就去通知依赖更新。

上面这句话说的数据我们又是怎么去判断描述是哪个数据被使用了呢?
在vue中如果谁用到了数据,就为他创建一个Watcher实例,在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的Watch实例,由Watcher实例去通知真正的视图

2.4 不足的地方

Object.defineProperty方法可以实现了对object数据的可观测,但是这个方法仅仅只能观测到object数据的取值及设置值

当我们向object数据里添加一对新的key/value

Vue 无法探测普通的新增 property (比如 `this.myObject.newProperty = 'hi'`)

或删除一对已有的key/value时,

Vue 不能检测到 property 被删除的限制

它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。

Vue增加了两个全局API:Vue.setVue.delete

3. 一些实现

3.1 Observer

function mvvm(data, key, val) {
    observer(val);
    Object.defineProperty(data, key, {
        get: function () {
            return val;
        },
        set: function (newVal) {
            val = newVal;
        }
    })
}

function observe(data) {
    if (!data || typeof data != 'object') {
        return;
    }

    Object.keys(data).forEach(function (key) {
        mvvm(data, key, data[key]);
    })
}

3.2 实现一个dep消息订阅器和订阅者

function mvvm(data, key, val) {
    observe(val); 
    var dep = new Dep(); 
    Object.defineProperty(data, key, {
        get: function() {
            if (Dep.self) { // 判断是否需要添加订阅者
                dep.additem(Dep.self); // 在这里添加一个订阅者
            }
            return val;
        },
        set: function(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            dep.notify(); // 如果数据变化,通知所有订阅者
        }
    });
}

// 源码里面是类哦
function Dep () {
	// 这里的消息订阅是可以是一个数组
    this.items = [];
}
Dep.prototype = {
    addItem: function(item) {
        this.items.push(item);
    },
    notify: function() {
        this.items.forEach(function(item) {
            item.update();
        });
    }
};


// 订阅者
function Watcher(_this, key, fun) {
    this.fun = fun;			//订阅者要触发的回调函数
    this._this =_this;		//订阅者订阅的对象
    this.key = key;			//订阅者订阅的key
    this.value = this.get();  // 将自己添加到订阅器
}
 
Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this._this.data[this.key];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.fun.call(this._this, value, oldVal);
        }
    },
    get: function() {
        Dep.self = this;  
        var value = this._this.data[this.key]  
        Dep.self = null;  
        return value;
    }
};

总结

对应官网的话:

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

image.png

  1. 可观测数据(有getter和setter)有对应的依赖管理器Dep
  2. 谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例,在创建Watcher实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中Dep
  3. 这个Watcher实例就代表这个依赖
  4. 当数据变化时,我们就通知Watcher实例
  5. Watcher实例再去通知真正的依赖

Vue响应式原理的核心就是ObserverDepWatcher

Observer中进行响应式的绑定,在数据被读的时候,触发get方法,执行Dep来收集依赖,也就是收集Watcher

在数据被改的时候,触发set方法,通过对应的所有依赖(Watcher),去执行更新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值