自定义Vue

defineProperty

1.概述

Object.defineProperty(objName,"attributeName"{
	get(){return ..},
	set(newVal){..}
})
-->挟持objName对象中的attributeName属性(ES6新特性:函数劫持属性)
	一旦attributeName属性发成改变则会触发set函数,做出响应
		-->数据与视图的双向绑定

2.案例

<div id="app"></div>
		
<script>
	var obj = {};
	obj.name="hello";
	
	Object.defineProperty(obj,"name",{
		get(){
			return document.querySelector("#app").innerHTML;
			//return document.getElementById("app").innerHTML;
		},
		set(val){
			document.querySelector("#app").innerHTML=val;
		}
	})
</script>

data的取值原理

  • html
<div id="app">
	<p>{{name}}</p>
</div>

<script type="text/javascript" src="1.data的取值.js" ></script>
<script>
	var app = new QVue({
		el:"#app",
		data:{
			name:"name",
			age:12
		}
	})
</script>
  • js
//创建QVue类,接收一个options对象
class QVue{
	//构造方法
	constructor(options){
		//缓存option对象数据, $是为了防止命名污染
		this.$options = options;
		/* opthins====
		 * {
		 *		el:"#app",
		 * 		data:{
		 * 			name:"name",
		 * 			age:12
		 * }	
		 */
		
		//取出data数据做数据响应
		//data==null?{}:data
		this.$data = options.data||{};
		//短路或,避免$data为undefined
	}
}

原理刨析

1.Observer

  • 数据监听器
  • 对数据对象所有的属性进行监听,有变动时拿到最新值并通知Watcher

2.Compile

  • 指令解析器
  • 对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,并绑定相应的更新函数

3.Watcher

  • 连接Observer和Compile的桥梁
  • 接收Observer发出的属性变动通知,执行指令绑定的相应回调函数,从而更新视图

视频讲解地址

代码汇总

/*
 *双向绑定原理
 * 页面元素的值发生变化时,将最新的值设置在vue实例中,因为vue已经实现数据的响应化
 * 响应化的set函数会触发界面中所有函数依赖模块的更新,所以所有有该数据模块就都更新了
 */

//创建QVue类,接收一个options对象
class QVue{
	//构造方法
	constructor(options){
		//缓存option对象数据, $是为了防止命名污染
		this.$options = options;
		
		//取出data数据做数据响应
		this.$data = options.data||{};
		//短路或,避免$data为undefined*/
		
		//监听数据的变化,搭建observer与watcher的桥梁
		this.observe(this.$data);
		
		//解析vue语法对应的指令,并渲染到页面上,搭建compile与watcher的桥梁
			//解析el对应id的容器中的文档树(文本节点{{}},属性节点v-xx)
		new Compile(this,options.el);
		//此处this已经绑定好数据监听(app的set/get,即data的set/get)
	}
	
	//观察数据变化
	observe(data){
		//$data不存在或不是对象类型则不予监听,{}[]是object
		if( !data || typeof data !== "object"){
			return;
		}
		
		//取data中所有的属性名                                                (数组的话取出下标)   ["name","age"]
		let keys = Object.keys(data);
		//keys:Object内置对象方法,返回对象自身的属性组成的数组
		
		//循环data的每一个属性名   -->$data,app对象分别添加set get方法
		keys.forEach( (key)=>{
			//数据响应-defineProperty(data中的每个属性设置set/get)----(对象,属性名,属性值)
			this.defineReactive(data,key,data[key]);
			
			//代理data中的属性到vue实例-defineProperty(app的data中的每个属性设置set/get) 
			this.proxyData(key);
			//model中有的映射一份到view-model(view-model=Watcher桥梁)--双向绑定
		});
	}
	
	//数据响应defineProperty(给每个属性添加set/get方法,感知数据变化)
	defineReactive(data,key,val){
		//解决数据层次嵌套,递归绑定监听
		this.observe(val);
		//-->data中属性还是一个对象 data:{name:"name",addr:{pro:"湖南",city:"长沙"}}
		//若不循环,则监听的是addr对象,只有对象地址改变才监听得到,当值改变监听不到
		
		
		//管理watcher(请求和响应与谁)
		const dep= new Dep();
		
		
		//开始数据监听--data对象
		Object.defineProperty(data,key,{//第1次时,只是绑定set/get,并没有执行(不是因为值改变)
			get(){
				//update中new Watcher时会调用Watcher构造函数,构造函数中Dep.target=watcher的对象,然后会调用get函数就到了此处
				Dep.target && dep.addWatcher(Dep.target);
				//addWatcher会向watchers数组中保存target对象,也就是watcher对象
				//返回之后Watcher构造函数继续执行Dep.target=null
				
				//返回当前属性的值
				return val;
			},
			set(newVal){
				if(newVal===val){//值没有变
					return;
				}
				//值发生变化-->更新
				val=newVal;
				
				//值在更新,通知所有的watcher起作用,将页面中vue重新渲染一次
				dep.notify();
			}
		})
	}
	
	//代理data中的属性到vue实例上--app对象
		//model中有的映射一份到view-model(view-model=Watcher桥梁)--双向绑定
	proxyData(key){//key=data中的属性名,this=app
		Object.defineProperty(this,key,{
			get(){
				return this.$data[key];
				//$data中的所有属性都绑定过set/get,此处就相当于调用了$data中的get方法
			},
			set(newVal){
				this.$data[key] = newVal;
				//此处就相当于调用了$data中的set方法
			}
		})
	}
}

//解析vue语法对应的指令,并渲染到页面上,搭建compile与watcher的桥梁
class Compile{
	//vm:app对象 -- el:层的id名 -- this:Compile对象
	constructor(vm,el) {
	    this.$vm=vm;
	    this.$el=document.querySelector(el);
	    
	    //此处忽略<template>-->解决:加个else,el和template按两套标准解析
	    if(this.$el){
	    	//解析节点内容,将宿主元素的代码片段取出
	    	this.$fragment = this.nodeFragment(this.$el);
	    	//将vue语法对应的内容渲染上$fragment代码片段
	    	this.compile(this.$fragment);
	    	//将$fragment渲染到页面上
	    	this.$el.appendChild(this.$fragment);
	    }
	}
	
	//解析节点内容,将宿主元素的代码片段取出
	nodeFragment(el){
		//创建根节点
		const frag = document.createDocumentFragment();
		
		let child;
		while(child = el.firstChild){//直到取出的节点为undefined或null
			frag.appendChild(child);
		}
		return frag;
	}
	
	//分析容器内的vue语法,渲染$fragment代码片段
	compile(el){
		//取宿主节点下所有的子元素
		const childNodes = el.childNodes;
		
		//转成数组,迭代每个子元素
		Array.from(childNodes).forEach((childNode)=>{
			//判断是不是元素节点(标签)
			if(this.isElement(childNode)){
				console.log("编译元素节点的name:"+childNode.nodeName);
				//取出元素节点上所有的属性
				const nodeAttrs = childNode.attributes;
				
				//转成数组,迭代每个属性节点
				Array.from(nodeAttrs).forEach((nodeAttr)=>{
					//取属性名  --->  判断是普通属性(不需要操作),还是vue语法中的属性(v-xxx/:/@)
					const attrName = nodeAttr.name;
					//取属性值  --->  如果不是普通属性,判断需要做的操作
					const attrValue = nodeAttr.value;
					
					//判断是不是指令  v-开头
					if(this.isDirective(attrName)){
						//取指令v-后面的内容
						const dir = attrName.substring(2);
						
						//执行更新   -->  不同的dir操作在compile中有对应的,以dir命名的函数来实现(text()/html()/on()..)
						this[dir] && this[dir](this.$vm,childNode,attrValue);
						//this[dir]:寻找compile中的dir属性  ---> 取到声明部分
						//&&:有对应的方法则继续
						//this[dir](this.$vm,childNode,attrValue):激活dir函数同时传入参数(vue实例(app),子元素,属性值)
					}
					
					//判断是不是事件处理   @
					if(this.isEvent(attrName)){
						//取出事件名  @click--click
						let dir = attrName.substring(1);
						
						//事件处理                        (vue实例(app),子元素,属性值,事件类型)
						this.eventHandler(this.$vm,childNode,attrValue,dir);
					}
				})
			}else if(this.isInterPolation(childNode)){
				//判断是不是文本节点(内容是不是插值语法)
				
				//更新插值文本
				this.compileText(childNode);
				console.log("插值文本:"+childNode.textContent);
			}
			
			//递归子元素,解决元素嵌套问题   --> 有子节点且长度不为0
			if(childNode.childNodes && childNode.childNodes.length){
				this.compile(childNode);
			}
		})
	}
	
	//是否为元素节点
	isElement(node){
		return node.nodeType===1;
	}
	//是否为文本节点(内容是不是插值语法{{内容}})
	isInterPolation(node){
		return node.nodeType===3 && /\{\{(.*)\}\}/.test(node.textContent);
	}
	//是否为指令(v-xx)
	isDirective(attr){
		//indexOf:返回字符串第一次的索引
		return attr.indexOf("v-")==0;
	}
	//是否为事件(@xx)
	isEvent(attr){
		return attr.indexOf("@")==0;
	}
	
	//更新函数——桥接
	update(vm,node,exp,dir){
		//获取dir对应的函数名
		const updateFn = this[`${dir}Updater`];//``执行里面的占位符${dir}
		//函数不为空则激活函数     (标签对象,vue对象的属性值 ) 
		updateFn && updateFn(node,vm[exp]);//vm[exp]:在app对象中寻找exp属性
		
		
		//依赖收集     --> 桥梁——设置target,触发get,添加依赖
		new Watcher(vm,exp,function(value){
			updateFn && updateFn(node,value);
		})
	}
	
	//v-text
	text(vm,node,exp){
		this.update(vm,node,exp,"text");
	}
	textUpdater(node,value){
		//修改节点上文本内容
		node.textContent = value;
	}
	
	//v-model
	model(vm,node,exp){
		this.update(vm,node,exp,"model");
		
		//对input添加监听 -- 当事件被触发时,调用回调函数
		node.addEventListener('input',(e)=>{
			vm[exp] = e.target.value;
		})
	}
	modelUpdater(node,value){
		node.value=value;
	}
	
	//v-html
	html(vm,node,exp){
		this.update(vm,node,exp,"html");
	}
	htmlUpdater(node,value){
		node.innerHTML=value;
	}
	
	//更新插值文本
	compileText(node){
		let key = RegExp.$1;
		this.update(this.$vm,node,key,"text");
	}
	
	//事件处理器  --> exp:回调函数名
	eventHandler(vm,node,exp,dir){
		//判断是否存在事件中调用的回调函数
		let fn = vm.$options.methods && vm.$options.methods[exp];
		if(dir && fn){
			//给事件添加监听
			node.addEventListener(dir,fn.bind(vm));
			//bind创建新函数(第一个参数指定新函数中的this)
		}
	}
}

//桥梁——设置target,触发get,添加依赖
class Watcher{
	constructor(vm,key,func) {
		//vue实例
	    this.vm = vm;
	    //vue实例中需要更新的属性值
	    this.key=key;
	    //更新执行回调函数XXXUpdater
	    this.func=func;
	    
	    //给Dep新添加属性target(目标对象),target=当前Watcher实例,用于类间通信
	    Dep.target = this;//本来Dep原来没有target属性,在此处才加上的
	    
	    //触发get,添加依赖
	    this.vm[this.key];//app[name] --> 相当于调用对象app中get --> 相当于调用$data中get
	    
	    Dep.target=null;
	}
	
	//激活func回调函数---具体的更新操作
	update(){
		//通过回调函数更新页面      (vue实例,实例对应属性值)
		this.func.call(this.vm,this.vm[this.key]);
		//this --> watcher对象,每个对象的更新操作(即回调函数func的内容)可能不同
		//func --> updateFn && updateFn(node,value)调用对应类型的函数更新
	}
}


//管理Watcher的中间对象,用来存数据---方便A类存值,B类取值
class Dep{
	constructor() {
	    //页面中每个vue语法对应一个watcher
	    this.watchers=[];//初始化
	}
	
	//添加
	addWatcher(watcher){
		this.watchers.push(watcher);
	}
	
	//通知所有watcher更新
	notify(){
		this.watchers.forEach((watcher)=>{
			//通知更新
			watcher.update();
		})
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值