今天学习了Mvvm的原理

首先我们先解释一下下面用到的几个函数和对象
1、observe 观察者
2、Watcher 监听者
3、dep 监听者容器,就是一个数据的监听者(Wacher)都放在里面
4、notifiy 用来通知所有某数据的所有监听者,即通知数据的 dep的所有对象
5、updater 更新器包含多个更新器 根据不同的数据类型,执行不同的更新器。如 文本节点 指向TextUpdater 的逻辑
6、Combine 编译器 获取dom节点 将里面的模板数据 替换为真实数据。如
<div> {{name}} </div> 编译之后 变为 <div> 小明</div>

我也是初学者,这里看了别人的原码 加上了自己的理解
源码博客请看源码的博客地址
这些函数具体怎么用,怎么配合,请看下面:

一、数据代理

function MVVM(options) {
	this.$options = options || {};
	var data = this._data = this.$options.data;
	var _this = this;//当前实例vm
	// 数据代理
	// 实现 vm._data.xxx -> vm.xxx 
	Object.keys(data).forEach(function(key) {
		_this._proxyData(key);
	});
	observe(data, this);
	this.$compile = new Compile(options.el || document.body, this);//定义编译器且初始化

}

MVVM.prototype = {
//数据代理函数实现
_proxyData: function(key) {
	var _this = this;
	if (typeof key == 'object' && !(key instanceof Array)){//这里只实现了对对象的监听,没有实现数组的
		this._proxyData(key);
	}
	Object.defineProperty(_this, key, {
		configurable: false,
		enumerable: true,
		get: function proxyGetter() {
			return _this._data[key];
		},
		set: function proxySetter(newVal) {
			_this._data[key] = newVal;
		}
	});
},
};

这里使用了 Object.defineProperty(对象,属性,配置)来实现数据代理,为什么要数据代理呢 ?
1、不想暴露内部逻辑
2、方便访问 如 vm._data.xxx -> vm.xxx
而且这里采用递归的方式 为对象以及对象的属性都添加代理

二、观察者

为数据添加观察者,即上面代码的observe(data, this);

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

function Observe(data){
	this.data = data;
	this.walk(data);//遍历 data
}

Observe.prototype = {
	walk: function(data){
		let _this  = this;
		for (key in data) {
			if (data.hasOwnProperty(key)){//如果这个属性自身存在
				let dep = new Dep();
				let value = data[key];
				if (typeof value == 'object'){
					observe(value);
				}
				_this.defineReactive(data,key,data[key]);
			}
		}
	},
	defineReactive: function(data,key,value){//为这个属性设置get方法,同时生成一个dep数组订阅器,
		let dep = new Dep();//在data的属性key内形成闭包,因此每个key都对应一个唯一的订阅器
		Object.defineProperty(data,key,{
			enumerable: true,//可枚举
			configurable: false,//不能再define
			get: function(){
				//console.log('你访问了' + key);//测试代码,忽略
				//如果是是初始化一个Watcher时引起的,则添加进订阅器
				if (Dep.target){
					dep.addSub(Dep.target);
				}
				//console.log(dep);//测试代码,忽略
				return value;
			},
			set: function(newValue){
				//console.log('你设置了' + key);//测试代码,忽略
				if (newValue == value) return;
				value = newValue;
				observe(newValue);//监听新设置的值
				//console.log(dep);//测试代码,忽略
				dep.notify();//通知所有的订阅者
			}
		})
	}
}

function Dep(){
	this.subs = [];
}

Dep.prototype = {
	addSub: function(sub){
		this.subs.push(sub);
	},
	notify: function(){
		this.subs.forEach(function(sub) {
			sub.update();//每个监听器都有 update 函数
		})
	}
}

这段代码为每一个数据都添加get/set函数,当数据发生改变时,做出一些响应。为什么要做出响应啊?为了当该数据改变的时候去通知关注该数据的那些节点让他更新。但是这时候dep数组里面没有对象,我怎么知道谁使用它了呢? 其实dep里的对象是编译的时候添加进去的。。

三、编译模板

编译就是为了把dom中绑定的变量 编译为真实的数据,并且添加对该数据的监听,这里说说其中的一些要点。
1、let fragment = document.createDocumentFragment();
fragment 的appendChild(child); 会把child剪切到内存中,对其进行操作,当然原来的child所指向的节点就会从dom树移除。
那么为什么要这么做呢??
我们都知道操作dom树是相当耗时的,因为我们对一个节点修改时,浏览器需要计算该元素相对于全局的位置、属性、样式等,而且当我们修改时会出现回流或者重绘,这是相当影响性能的。这里我们选择将要操作的dom 移动到内存中操作,就不会影响其他的dom元素,我们修改完成之后,在将该节点插入到dom树中,只计算一次,性能更好
2、我们是如何将节点添加到数据的监听者里的?
这里我们以所编译的节点为文本节点为例,如{{name}} ,看看是如何操作的。
(1)、首先用正则表达式/\{\{(.*)\}\}/将变量取出,即name,
(2)、调用compileUtil 的text函数,text函数中又调用了bind方法
将节点绑定到dep数组,通过代码中通过new Watcher

new Watcher(vm,exp,function(value){  // wacher 把自己挂载到 vm的 订阅者里
			updaterFn && updaterFn(node,value)//更新函数
});

我们看watcher,这个操作的意思就是把该节点弄成一个监听者对象,如何添加到dep数组呢?因为我们之前已经对name添加了get方法,并且那个get方法里
if (Dep.target){
dep.addSub(Dep.target);
}
我们在watcher的get函数里操作,通过Dep.target将自己传送到 name的get方法中,如下操作:因为有name有get方法,所以 this.vm[name]就会被调用

	get: function() { 
        Dep.target = this;  // 缓存自己
        var value = this.vm[this.exp];  // 强制访问自己,执行defineProperty里的get函数         
        Dep.target = null;  // 释放自己
        return value;
    }
		
//Watcher
function Watcher(vm, exp, cb) {
	this.vm = vm;
	this.cb = cb;//更新函数
	this.exp = exp;
	this.value = this.get();//将自己添加进订阅器
};

Watcher.prototype = {
	update: function(){
		this.run();
	},
	run: function(){
		const value = this.vm[this.exp];
		//console.log('me:'+value);//测试代码,忽略
		if (value != this.value){
			this.value = value;
			this.cb.call(this.vm,value);
		}
	},
	get: function() { 
        Dep.target = this;  // 缓存自己
        var value = this.vm[this.exp];  // 强制访问自己,执行defineProperty里的get函数         
        Dep.target = null;  // 释放自己
        return value;
    }
}
function Compile(el,vm){
	this.$vm = vm;//vm为当前实例
	this.$el = document.querySelector(el);//获得要解析的根元素
	if (this.$el){
		this.$fragment = this.nodeToFragment(this.$el);//将节点添加到文档碎片
		this.init();
		this.$el.appendChild(this.$fragment);
	}	
}
Compile.prototype = {
	nodeToFragment: function(el){
		let fragment = document.createDocumentFragment();
		let child;
		while (child = el.firstChild){
			fragment.appendChild(child);//append相当于剪切的功能
		}
		return fragment;
	},
	init: function(){//初始编译
		this.compileElement(this.$fragment);
	},
	compileElement: function(node){
		let childNodes = node.childNodes;//获取子节点
		const _this = this;
		let reg = /\{\{(.*)\}\}/;
		[].slice.call(childNodes).forEach(function(node){	
			if (_this.isElementNode(node)){//如果为元素节点
				_this.compile(node);
			} else if (_this.isTextNode(node) && reg.test(node.textContent)){
				//如果为文本节点,并且包含data属性(如{{name}}),则进行相应操作
				_this.compileText(node,reg.exec(node.textContent)[1]);
			}	
			if (node.childNodes && node.childNodes.length){
				//如果节点内还有子节点,则递归继续解析节点
				_this.compileElement(node);	
			}
		})
	},
	compileText: function(node,exp){
		compileUtil.text(node,this.$vm,exp);
	},
	isElementNode: function(node){
		return (node.nodeType == 1);
	},
	
	isTextNode: function(node){
		return node.nodeType == 3;
	},
	isDirective: function(attr){
		return (attr.indexOf('v-') == 0);
	},
	isEventDirective: function(attr){
		return attr.indexOf('on') == 0;
	},
	isLinkDirective: function(attr){
		return attr.indexOf('bind') == 0;
	},
	//编译节点内的属性
    compile: function(node) {
    	console.log('编译节点的属性')
        var nodeAttrs = node.attributes,//获取当前节点的属性,v-on:,v-bind:,v-model,class等属性
			_this = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            var attrName = attr.name;
            if (_this.isDirective(attrName)) {//判断是否为vue指令
                var exp = attr.value;
                var dir = attrName.substring(2);
                // 事件指令: v-on:
                if (_this.isEventDirective(dir)) {
					//node:当前节点, _this.$vm:当前实例, exp:指令属性值(表达式或函数), dir:指令类型(on)
                    compileUtil.eventHandler(node, _this.$vm, exp, dir);
                } 
				if (_this.isLinkDirective(dir)){
					let attr = dir.split(':')[1];
					compileUtil[attr] && compileUtil[attr](node, _this.$vm, exp);
				}else {
					//其它指令: v-bind:,v-model
					
                    compileUtil[dir] && compileUtil[dir](node, _this.$vm, exp);
                }
            }
        });
    },
};
let updater = {
	textUpdater: function(node,value){
		node.textContent = typeof value == 'undefined' ? '' : value;
	},
    modelUpdater: function(node, value) {
        node.value = typeof value == 'undefined' ? '' : value;
    },
	classUpdater: function(node, value) {
        var className = node.className;
        node.className = className + value;
    },
}
let compileUtil = {
	eventHandler: function(node,vm,exp,dir){
		let eventType = dir.split(':')[1];
		let fn = vm.$options.methods && vm.$options.methods[exp];
		if (eventType && fn){
			node.addEventListener(eventType,fn.bind(vm),false);
		}
	},
	class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },
	model: function(node,vm,exp){
		this.bind(node,vm,exp,'model');
		let _this = this;
		let value = node.value;
		node.addEventListener('input',function(e){
			let newValue = e.target.value;
			if (value == newValue) return;
			vm[exp] = newValue;//设置exp属性值,触发视图的更新
		},false);
	},
	text: function(node,vm,exp){
		this.bind(node,vm,exp,'text');
	},
	_getVMVal: function(vm,exp) {
		let arr = exp.split('.');
		let value = vm;
		arr.forEach(function(item){//a.b  -->arr = [a,b]-->value = ->> value = value[b]-->a[b]
			value = value[item];//经过了数据代理,所以data里的数据直接绑定到了实例上
			console.log('设置节点值',item,value)
		})
		//console.log(value);
		return value;
	},
	bind: function(node,vm,exp,dir){//初始化相关
		let updaterFn = updater[dir + 'Updater'];//获取更新器
		updaterFn && updaterFn(node,this._getVMVal(vm,exp));//初始化首先去 vm实例拿值
		new Watcher(vm,exp,function(value){  // wacher 把自己挂载到 vm的 订阅者里
			updaterFn && updaterFn(node,value)//更新函数
		});
		//console.log('实例化了一个Watcher');//测试代码,忽略
	}
};

至此,当我们修改某个数据的值的时候,就会自动调用该数据的监听者的updatefn函数,执行更新逻辑。

发布了14 篇原创文章 · 获赞 8 · 访问量 6132

猜你喜欢

转载自blog.csdn.net/KangTongShun/article/details/104882912