Vue2.0 双向绑定原理及实现以及数据响应式实现原理

279 阅读9分钟

1. 前言

Vue 数据双向绑定大概原理概括: Vue 内部通过 Object.defineProperty 方法属性拦截的方式,把 data 对象里每个数据的读写转化成 getter/setter,当数据变化时通知视图更新。

2. 思路分析

所谓 MVVM 数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据。如下图:
\

也就是说:

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

要实现这两个过程,关键点在于数据变化如何更新视图,因为视图变化通过事件监听的方式来实现更新数据

数据变化更新视图的关键点则在于我们如何知道数据发生了变化,只要知道数据在什么时候变了,那么问题就变得迎刃而解,只需在数据变化的时候去通知视图更新即可。

3. 使数据对象变得 “可观测”

能够知道数据什么时候被读取了或数据什么时候被改写了,我们将其称为数据变的‘可观测’。

要将数据变的‘可观测’,我们就要借助前言中提到的 Object.defineProperty 方法了,关于该方法,MDN 上是这么介绍的:

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

在本文中,我们就使用这个方法使数据变得 “可观测”。

首先,我们定义一个数据对象 car

let car = {
		'brand':'BMW',
		'price':3000
	}

我们定义了这个 car 的品牌 brand 是 BMW, 价格 price 是 3000。现在我们可以通过 car.brand 和 car.price 直接读写这个 car 对应的属性值。但是,当这个 car 的属性被读取或修改时,我们并不知情。那么应该如何做才能够让 car 主动告诉我们,它的属性被修改了呢?

接下来,我们使用 Object.defineProperty() 改写上面的例子:

	let car = {}
	let val = 3000
	Object.defineProperty(car, 'price', {
		get(){
			console.log('price属性被读取了')
			return val
		},
		set(newVal){
			console.log('price属性被修改了')
			val = newVal
		}
	})

通过 Object.defineProperty() 方法给 car 定义了一个 price 属性,并把这个属性的读和写分别使用 get() 和 set() 进行拦截,每当该属性进行读或写操作的时候就会出发 get() 和 set()。如下图:
\

可以看到,car 已经可以主动告诉我们它的属性的读写情况了,这也意味着,这个 car 的数据对象已经是 “可观测” 的了。

为了把 car 的所有属性都变得可观测,我们可以编写如下两个函数:

/**
	 * 把一个对象的每一项都转化成可观测对象
	 * @param { Object } obj 对象
	 */
         //定义一个监听函数observable
	function observable (obj) {
        //如果参数obj不是对象直接结束监听
		if (!obj || typeof obj !== 'object') {
                
        	return;
    	}
        //keys是可枚举属性数组【即属性名称组成的数组】
		let keys = Object.keys(obj);
		keys.forEach((key) =>{
                //defineReactive方法是对当前对象的属性进行set(),get()拦截
			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:

let car = observable({
		'brand':'BMW',
		'price':3000
	})

car 的两个属性都变得可观测了。

4. 依赖收集

完成了数据的 ' 可观测 ',即我们知道了数据在什么时候被读或写了,那么,在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是典型的 “发布订阅者” 模式,数据变化为 “发布者”,依赖对象为 “订阅者”。

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

创建消息订阅器 Dep:

	class Dep {
		constructor(){
			this.subs = []
		},
		//增加订阅者,收集依赖
		addSub(sub){
			this.subs.push(sub);
		},
        //判断是否增加订阅者,target全局唯一 的 `Watcher`
		depend () {
		    if (Dep.target) {
		     	this.addSub(Dep.target)
		    }
		},

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

有了订阅器,再将 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()                    //数据变化通知所有订阅者
			}
		})
	}

从代码上看,我们设计了一个订阅器 Dep 类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,这是一个全局唯一 的 Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组。

将订阅器 Dep 添加订阅者的操作设计在 getter 里面,这是为了让 Watcher 初始化时进行触发,因此需要判断是否要添加订阅者。在 setter 函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。

到此,订阅器 Dep 设计完毕,接下来,我们设计订阅者 Watcher.

5. 订阅者 Watcher

订阅者 Watcher 在初始化的时候需要将自己添加进订阅器 Dep 中,那该如何添加呢?监听器 Observer 是在 get 函数执行了添加订阅者 Wather 的操作的,所以我们只要在订阅者 Watcher 初始化的时候出发对应的 get 函数去执行添加订阅者操作即可,那要如何触发 get 的函数,再简单不过了,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了 Object.defineProperty( ) 进行数据监听。这里还有一个细节点需要处理,我们只要在订阅者 Watcher 初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在 Dep.target 上缓存下订阅者,添加成功后再将其去掉就可以了

订阅者 Watcher 的实现如下:

/**
	 * 使一个对象转化成可观测对象
	 * @param { Object } vm vue实例
	 * @param { String } exp `node` 节点的 `v-model` 或 `v-on:click` 等指令的属性值。如 `v-model="name"`,`exp` 就是 `name`
	 * @param { Any } cb 绑定的更新函数
	 */
	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 绑定的更新函数;

当我们去实例化一个渲染 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 进行更新。

至此,简单的订阅者 Watcher 设计完毕。

6. 测试

完成以上工作后,我们就可以来真正的测试了。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
</head>
<body>
	<h1 id="name"></h1>
	<input type="text">
	<input type="button" value="改变data内容" onclick="changeInput()">
	
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
	function myVue (data, el, exp) {
	    this.data = data;
	    observable(data);                      //将数据变的可观测
	    el.innerHTML = this.data[exp];           // 初始化模板数据的值
	    new Watcher(this, exp, function (value) {
	        el.innerHTML = value;
	    });
	    return this;
	}

	var ele = document.querySelector('#name');
	var input = document.querySelector('input');
	
    var myVue = new myVue({
		name: 'hello world'
	}, ele, 'name');
 	
	//改变输入框内容
    input.oninput = function (e) {
    	myVue.data.name = e.target.value
    }
	//改变data内容
	function changeInput(){
		myVue.data.name = "难凉热血"
	
	}
</script>
</body>
</html>

observer.js

	/**
	 * 把一个对象的每一项都转化成可观测对象
	 * @param { Object } obj 对象
	 */
	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) {
		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()                    //数据变化通知所有订阅者
			}
		})
	}
	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;

watcher.js

	class Watcher {
		constructor(vm,exp,cb){
		    this.vm = vm;
		    this.exp = exp;
		    this.cb = cb;
		    this.value = this.get();  // 将自己添加到订阅器的操作
		}
		get(){
			Dep.target = this;  // 缓存自己
        	let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        	Dep.target = null;  // 释放自己
        	return value;
		}
		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);
			}
	}
}

效果:
\

7. 总结

实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器 Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者 Watcher 看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者,然后在监听器 Observer 和订阅者 Watcher 之间进行统一管理的。