vue蓄力1:通俗易懂了解Vue双向绑定原理及实现

有部分TypeScript,defineProperty,es6语法,不懂的请自行查阅哦!
阅读目录

 


回到顶部
 

1. 前言

每当被问到Vue数据双向绑定原理的时候,大家可能都会脱口而出:Vue内部通过Object.defineProperty方法属性拦截的方式,把data对象里每个数据的读写转化成getter/setter,当数据变化时通知视图更新。虽然一句话把大概原理概括了,但是其内部的实现方式还是值得深究的,本文就以通俗易懂的方式剖析Vue内部双向绑定原理的实现过程。

回到顶部

2. 思路分析

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

也就是说:

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

要实现这两个过程,关键点在于数据变化如何更新视图,因为视图变化更新数据我们可以通过事件监听的方式来实现。所以我们着重讨论数据变化如何更新视图。

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

回到顶部

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

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

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

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

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

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

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

我们定义了这个car的品牌brandBMW,价格price是3000。现在我们可以通过car.brandcar.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 对象
     */
    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) {
        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);
        },
        //判断是否增加订阅者
        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的实现如下:

    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-modelv-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);
            }
    }
}

效果:

完整代码,在这里https://github.com/SanlyShi/vueBinding

回到顶部

7. 总结

总结一下:

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


再附一个 更花的图

(完)

使用JSDoc

复制代码

 /**基础类型*/
 /**@type {string} */

 /**联合类型 */
 /**@type {string | number} */

 /**数组类型 */
 /**@type {string[]} */
 /**@type {Array.<string>} */

 /**对象字面量类型 */
 /**@type {a:string,b:number} */

 /**map-like 类型 */
 /**@type {Object.<string,number>} */

 /**array-like 类型 */
 /**@type {Object.<number,object>} */

 /**closure 语法 */
 /**@type {function(string,boolean):number} */
 /**TypeScript 语法 */
 /**@type {(a:string,b:string) => number} */

 /**函数类型 */
 /**@type {function} */

 /**任意类型 */
 /**@type {*} */
 /**@type {?} */

 /**导入类型 */
 /**@param {import('./a').Pet} */
 /**@typedef Pet {import("./a").Pet} */
 /**@type {Pet} */

 /**params语法类似于type,唯一区别于可选属性 ,arg可选且默认值为test*/
 /**@param [arg = 'test'] */

 /**返回值类型 */
 /**@return {string} */

 /**复杂类型 */
 /**@typedef {prop1:string,prop2:string,prop3?:number} SpecialType */
 /**@typedef {(prop1:string,prop2?:number) => number} Predicate*/

 /**泛型类型 */
 /**@template T */
 /**@param {T} */
 /**@return {T} */
 function id(x) {return x;}

 /**指定this的类型 */
 /**@this {HTMLElement} */

 

复制代码

 

/**基础类型*/

/**@type{string} */

 

/**联合类型 */

/**@type{string | number} */

 

/**数组类型 */

/**@type{string[]} */

/**@type{Array.<string>} */

 

/**对象字面量类型 */

/**@type{a:string,b:number} */

 

/**map-like 类型 */

/**@type{Object.<string,number>} */

 

/**array-like 类型 */

/**@type{Object.<number,object>} */

 

/**closure 语法 */

/**@type{function(string,boolean):number} */

/**TypeScript 语法 */

/**@type{(a:string,b:string) => number} */

 

/**函数类型 */

/**@type{function} */

 

/**任意类型 */

/**@type{*} */

/**@type{?} */

 

/**导入类型 */

/**@param{import('./a').Pet} */

/**@typedefPet {import("./a").Pet} */

/**@type{Pet} */

 

/**params语法类似于type,唯一区别于可选属性 ,arg可选且默认值为test*/

/**@param [arg = 'test'] */

 

/**返回值类型 */

/**@return{string} */

 

/**复杂类型 */

/**@typedef{prop1:string,prop2:string,prop3?:number}SpecialType */

/**@typedef{(prop1:string,prop2?:number) => number}Predicate*/

 

/**泛型类型 */

/**@templateT */

/**@param{T} */

/**@return{T} */

function id(x) {return x;}

 

/**指定this的类型 */

/**@this{HTMLElement} */


原文转自:https://www.cnblogs.com/wangjiachen666/p/9883916.html



更形象的在这儿哦!!!!!!!!!!
 

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
</head>
<body>
	<h1 id="name"></h1>
	<input type="text">
	
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
	function myVue (data, el, exp) {
	    this.data = data;
		observable(data);                      //将数据变的可观测
		console.log("我要读取name属性数据了")
	    el.innerHTML = this.data[exp];           // 初始化模板数据的值
	    new Watcher(this, exp, function (value) {
			console.log("你妹,你猜傻子标签,我显示出来行了吧")
	        el.innerHTML = value;
	    });
	    return this;
	}

	var ele = document.querySelector('#name');
	var input = document.querySelector('input');
	
    var myVue = new myVue({
		name: '我是实例name属性的初始值'
	}, ele, 'name');
 	
	//改变输入框内容
    input.oninput = function (e) {
		console.log("~~~~~~~~~~~动作分隔符~~~~~~~~~~~~~~bling~bling~~~~~~~")
		console.log("监听输入框变化,变化后将目标值赋值给实例的name属性")
    	myVue.data.name = e.target.value
    }
</script>
</body>
</html>
	/**
	 * 把一个对象的每一项都转化成可观测对象
	 * @param { Object } obj 对象
	 */
	function observable (obj) {
		console.log("对对象属性做出变化监听")
		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) {
		console.log("进入监听属性设置")
		let dep = new Dep();
		Object.defineProperty(obj, key, {
			get(){
				console.log(`${key}属性被读取了`);
				dep.depend();
				return val;
			},
			set(newVal){
				val = newVal;
				console.log(`${key}属性被修改了,实例中的值已经被更新啦`);
				dep.notify()                    //数据变化通知所有订阅者
			}
		})
	}

	// 消息订阅器Dep,用来容纳所有的“订阅者”
	class Dep {
		
		constructor(){
			console.log("初始化Dep依赖收集器(依赖该数据的视图)")
			this.subs = []
		}
		//增加订阅者
		addSub(sub){
			console.log('增加订阅者')
			this.subs.push(sub);
		}
        //判断是否增加订阅者
		depend () {
			console.log("判断是否增加订阅者")
		    if (Dep.target) {
				console.log("我曹,订阅者target不是空了,那我要存起来")
		     	this.addSub(Dep.target)
		    }
		}

		//通知订阅者更新
		notify(){
			console.log("我去,好像数据被谁改了!那我要去通知儿子们更新了")
			this.subs.forEach((sub) =>{
				sub.update()
			})
		}
		
	}
	Dep.target = null;
// 订阅者Watcher
	class Watcher {
		constructor(vm,exp,cb){
			console.log("初始化观察者构造器")
		    this.vm = vm;
		    this.exp = exp;
		    this.cb = cb;
		    this.value = this.get();  // 将自己添加到订阅器的操作
		}
		get(){
			Dep.target = this;  // 缓存自己
			console.log("现在开始Dep的target不是空啦,是一个Watcher订阅者")
			console.log("那我要再把初始化的name属性的值get一次")
			let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
			console.log("既然存好了,那我就把茅坑让出来了,我要清空Dep的target,至此,监听完成,初始化完成,下面更新一下试试吧")
        	Dep.target = null;  // 释放自己
        	return value;
		}
		update(){
			console.log("嗯,好吧,我是他儿子,我要变了")

			console.log("我来看下实例中的新值")
			let value = this.vm.data[this.exp];

			console.log("我来看下我这个订阅者里存的值")
			let oldVal = this.value;
			
        	if (value !== oldVal) {
				console.log("我擦,不一样,那赶紧跟进人家的新玩意儿吧")
				this.value = value;
				
				console.log("光我自己知道了不行啊,我要让外面的傻子标签页显示出来")
                this.cb.call(this.vm, value, oldVal);
			}
	}
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值