Vue2.0:双向数据绑定 之 监听对象,源码分析

前言:
关于Vue2.0的双向数据绑定,会大致分为三个部分来讲解,分别是Vue对 对象 的监听Vue对数组的监听以及和Vue双向数据绑定相关的API的分析。
希望大家在看这篇文章的时候,先下载一份源码,然后一边看文章,一边对照源码,这样可以建立一个比较清晰的架构出来。当然,文章里也是会附上源码分析的。
Vue源码下载:https://github.com/vuejs/vue
看源码的时候,直接上src文件夹下找就ok啦。

我下面来开始吧,这部分先讲解Vue对对象{}是怎么进行双向数据绑定的。

双向数据绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
双向数据绑定的核心就是:数据劫持 + 发布订阅模式。
下面就是讲对对象的数据劫持 + 发布订阅模式(Dep + Watcher)。
在这里插入图片描述

那么,我们怎么知道数据(当然,这里的数据指的是我们的对象{})发生变化了呢?
有两种方法:
一种是使用Object.definedProperty()来实现对数据的变化的监听
一种是使用Proxy

在Vue2.0时,是使用**Object.defineProperty()**来实现对对象的变化监听的,因为在Vue2.0开发时,浏览器对ES6的支持不理想,现在的Vue3.0时Proxy来实现的。
所以,我们可以写出如下代码来对对象进行一个数据的监听:

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

这个函数就是对Object.defineProperty()进行了简单的封装。使用这个函数来进行监听的对象里面的属性,如果这个属性被读取时,就会触发get函数;如果这个属性被设置值的话,就会触发set函数。

完成了对对象的监听,然后要干嘛呢?我们是不是可以触发get函数或者是set函数时,在这两个函数里添加一些操作呢?可以的。

下面要完成的对依赖的收集了,可能大家会问为什么啊?
为什么要进行依赖的收集?
怎么收集呢?
什么是依赖?
下面就来解决大家的疑惑:

1.为什么要进行依赖的收集?
我们要明确,我们观察数据的目的是:当数据发生变化的时候,可以通知到那些使用了这个数据(指:监听的对象上的某个属性)的组件,然后组件内部再通过虚拟DOM来进行重新渲染。
所以,我们要先进行依赖的收集,把那些使用到这个数据的组件给收集起来,然后等数据发生变化的时候,把之前收集好的依赖循环“通知”一遍就好了。

2.怎么收集依赖呢?
我们把上面的代码改一下:
我们添加了dep数组来存储我们的依赖,在get函数里面收集依赖,在set函数里面触发依赖

function defineReactive(obj, key, val) {
	let dep = []; // 新增 : 用来存储我们的依赖
	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function() {
			dep.push(window.target); // 新增 : 用来添加依赖
			return val
		},
		set: function(newVal) {
			if(val === newVal) {
				return;
			}
			for(let i = 0; i < dep.length; i ++) { // 新增 : 用来触发收集的依赖
				dep[i](newVal, val);
			}
			val = newVal;
		}
	})
}

当然,我们的代码不能这样写,这样代码就耦合啦。应该把依赖管理和数据监听的功能代码给分开:
依赖管理:

export default class Dep {
  constructor () {
    this.subs = []
  }

  addSub (sub) { // 添加依赖
    this.subs.push(sub)
  }

  removeSub (sub) { // 删除依赖
    remove(this.subs, sub)
  }

  depend () { // 添加依赖
    if (window.target) {
      this.addSub(window.target)
    }
  }
  notify () { // 触发更新
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() 
    }
  }
}

function remove(arr, item) { // 删除数组的某个元素
	if(arr.length) {
		const index = arr.indexOf(item);
		if(index > -1) {
			return arr.splice(index, 1);
		}
	}
}


数据监听:


function defineReactive(obj, key, val) {
	let dep = new Dep(); // 修改
	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function() {
			dep.depend() // 修改
			return val
		},
		set: function(newVal) {
			if(val === newVal) {
				return;
			}
			val = newVal;
			dep.notify() // 修改
		}
	})
}

这样,我们就完成了对数据的监听和对依赖的管理了。

那么,依赖是什么呢?
依赖就是当我们的属性发生变化的时候,我们要通知谁,这个谁就是依赖。
在Vue源码中,这个依赖就是一个Watcher实例数据发生改变的时候就会通知这个实例,然后再它通知其他的地方
Watcher构造函数的代码如下:

export default class Watcher{
	constructor (vm, exOrFn, cb) {
		this.vm = vm;
          // 如果执行this.getter函数,就可以获取到exOrFn对应于对象上属性的值
		this.getter = parsePath(exOrFn);
		this.cb = cb;
		this.value = this.get();
	}

	get() {
         // 把当前Watcher实例(就是这个this),赋值到window.target上,等待触发getter函数,进行依赖收集
		window.target = this;
          // 获取obj上某个属性的值
		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);
	}
}

//下面代码功能: 解析简单路径
const bailRE = /[^\w.$]/;
export function parsePath(path) {
	if(bailRE.test(path)) {
		return;
	}
	const segments = path.split('.');
	return function(obj) {
		for(let i = 0; i < segments.length; i ++) {
			if(!obj) return;
			obj = obj[segments];
		}
		return obj;
	}
}

我们使用构造函数的时候,首先会进行 new Watcher()来创建一个Watcher实例,会获取到this.getter方法,然后触发this.get方法执行。
this.get函数执行时,会把当前的Watcher实例设置到window.target上;然后触发this.getter函数,去读取我们exOrFn这个参数对应的属性值,因为时读取操作,所以会触发get函数,进行依赖收集(这个在上面介绍过)。这个样就完成了一次依赖的收集,我当前的Watcher实例收集到了某个属性的dep数组里面。
每当属性的值发生变化的话,就会让依赖列表(就是dep数组)中的所有依赖循环触发update方法(就是Watcher实例的update方法),update函数会执行参数中的回调函数(这里的回调是创建Watcher实例传进去的)。

上面的功能就可以实现对对象进行一个基本的变化监听,但是,这还不够,我们需要的是监听所有的对象中的属性(包括子属性),所以,我们需要封装一个Observer类。这个类可以帮我们把对象里面的属性(包括子属性)都转换为get函数和set函数的形式,然后我们就可以去追踪这些属性的变化了。
Observer类代码如下:

export class Observer {
  constructor (value) {
    this.value = value
    if (!Array.isArray(value)) {
    	this.walk(value)
    } 
  }

  /**
   * 功能:
   *迭代对象的每一个属性,然后将这些属性转换为get函数和set函数
   */
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

export function defineReactive (obj, key, val) {
  if(typeof val === 'object') { // 新增 递归,进行深度的监听(可以类比到深拷贝)
  	new Observe(val)
  }
  let dep = new Dep();

  Object.defineProperty(obj, key, { 
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend(); 
      return value
    },
    
    set: function (newVal) {
      if(val === newVal) reurn;
      val = newVal
      dep.notify();
    }
  })
}

上面代码不难理解,就是先进行了类型判断,只有对象才可以调用walk函数,将每一个属性转成get/set函数来监听变化。
在defineReactive中新增new Observer(val)来递归子属性,这样,我们就可以把对象中的所有属性(包括子属性)都转换为get/set函数的形式。

这样就完成了对对象的变化监听了。(很开心不??)

但是,
Vue使用Object.defienProperty()来监听一个对象的变化真的好吗?答案是肯定不太好的。
第一点:它只能监听到属性的读取操作和修改操作(即设置值的操);对于一个属性被删除了,被遍历了等其他的操作,它检测不到。
第二点:对于新增的的属性和被删除掉的属性,无法追踪到,这样的变化没办法给依赖发通知。
为了解决新增属性和被删除的属性没办法被追踪到,Vue提供了两个API----vm. s e t 与 v m . set 与 vm. setvm.delete,在第三部分,我们会来分析它们的原理。

源码解析部分:
下面,我们来看看源码是怎么写的,实际上,上面代码基本上就是源码中对一个对象的变化侦测的核心代码了。
(我会给出图片,框出对应的文件夹和代码部分,没有框出来的部分,不是文章应对的内容。
但是还需要大家自己到源码上找对于得部分阅读,因为我们这样主要是为了帮助大家理解,源码这个东西是需要结合上下文来理解的,要从宏观上先理解其原理,然后在深入细节研究)

首先的对我们new Vue({data:{}})里data属性进行观察:
在这里插入图片描述
调用new Observer(val)来监测我们对象的所有属性(包括子属下)
在这里插入图片描述
调用walk函数
在这里插入图片描述
定义的defineReactive函数,创建一个dep实例,来管理我们的依赖
在这里插入图片描述
数据劫持:get函数收集依赖,set函数触发依赖通知
在这里插入图片描述
dep管理依赖的构造函数
在这里插入图片描述
Watcher的更新操作
在这里插入图片描述

参考:
《深入浅出Vue.js》 + Vue源码

最后,希望这篇文章可以帮助呢理解Vue的双向数据绑定, 如果有错误的地方,希望大家指出,欢迎在评论区留言;如果喜欢,可能关注收藏点赞,谢谢。
我们一起成长。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值