简易vue,论mvvm的实现原理

455 阅读4分钟

         vue是采用数据劫持配合发布订阅者模式的方式通过object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给依赖收集器Dep(订阅者),去通知观察者作出对应的回调函数去更新视图。
语言整理:mvvm作为绑定的入口,整合了observer,compile,和watcher三者,通过observer监听model数据变化,compile解析模板指令,最终利用watcher搭起了observer和compile之间的世界名桥。达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

一、MVue实例

//lev vm = new MVue() 将整个实例赋值给vm,控制台输入vm可拿到整个实例
let vm = new MVue({ 
      el:'#app',
      data:{
		msg:'学习mvvm实现原理',
        person:{
          name:'小马',
          age:18,
          fav:'姑娘',
		  obj:{
			  a:'123'
		  }
        },
		htmlStr:'好好学习,天天向上'
      },
	  methods:{
		  handlerClick(){
			  // const p = new Proxy(target, handler) 劫持
			  // Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
			  // target
			  // 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
			  // handler
			  // 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
			  // console.log(p)
			  console.log(this)
			  this.msg = '11111'
			  this.$data.person.name = '张三'
		  }
	  }
    })

Mvue与proxyData代理

class MVue{
	constructor(options) { //this指向的是MVue
	    this.$el = options.el 
		this.$data = options.data
		this.$options = options
		if(this.$el){
			//1.实现一个数据观察者
			new Observer(this.$data)
			//2.实现一个指令解析器
			new Compile(this.$el,this)
			this.proxyData(this.$data) //劫持this.$data 指向 this
		}
	}
	proxyData(data){
		for(const key in data){
			Object.defineProperty(this,key,{ //this=MVue
				get(){
					return data[key] 
                    // data:{ msg:'学习mvvm实现原理' }
				},
				set(newVal){
					data[key] = newVal
				}
			})
		}
	}
}

proxyData代理前
proxyData代理后

二、Compile解析器

解析指令,页面重绘
class Compile{
	constructor(el,vm) {
		console.log('compile')
	   this.el = this.isElementNode(el) ? el : document.querySelector(el)
	   // console.log(this.el,'el')
	   this.vm = vm
	   // 1.获取文档碎片对象 放入内存中会减少页面的回流和重绘
	   const fragment = this.node2Fragment(this.el)
	   // console.log(fragment,'fragment')
	   //2.编译模板
	   this.compile(fragment)
	   //3.追加子元素到根元素
	   this.el.appendChild(fragment)
	}
	compile(fragment){
		// 1.获取子节点
		const childNodes = fragment.childNodes
		childNodes.forEach(child=>{
			// console.log(child,'child')
			if(this.isElementNode(child)){
				//是元素节点
				//编译元素节点
				// console.log('元素节点',child)
				this.compileElement(child)
			}else{
				//文本节点
				//编译文本节点
				// console.log('文本节点',child)
				this.compileText(child)
			}
			if(child.childNodes && child.childNodes.length){
            // 当子节点还有子节点时
				this.compile(child)
			}
		})
	}
	compileElement(node){
		const attributes = node.attributes
		// console.log(Object.keys(attributes),'attributes') //{0: v-text, v-text: v-text, length: 1} <div v-text="msg"></div>
		Object.keys(attributes).forEach(item=>{
			let attr = attributes[item] //attributes 属性返回指定节点的属性集合,即 NamedNodeMap。您可以使用 length 
			//属性来确定属性的数量,然后您就能够遍历所有的属性节点并提取您需要的信息。
			// v-text:name = msg:value  v-on:click="handlerClick" //@click="handlerclick" v-bind:src = "img"
			const {name,value} = attr //结构赋值 取得name,value
			// console.log(attr)
			if(this.isDirective(name)){ // 是一个指令 v-text,v-html,v-model,v-on:click
			  const [,directive] = name.split('-')	// text,html,model,on:click
			  const [dirName,eventName] = directive.split(':') //dirName:text,html.model,on
			  //更新数据 数据驱动视图
			  compileUtil[dirName](node,value,this.vm,eventName)
			  //删除指令标签上的属性
			  node.removeAttribute('v-'+directive)
			}else if(this.isEventName(name)){ //@click="handlerclick"
				let [,eventName] = name.split('@')
				compileUtil['on'](node,value,this.vm,eventName)
			}
		})
	}
	isEventName(attrName){
		return attrName.startsWith('@')
	}
	compileText(node){ //{{}} v-text
		// console.log(node.textContent)
		const content =node.textContent
		if(/\{\{(.+?)\}\}/.test(content)){
			// console.log(content)
			compileUtil['text'](node,content,this.vm)
		}
	}
	isDirective(attrName){
		// startsWith() 方法用于检测字符串是否以指定的子字符串开始。
		// 如果是以指定的子字符串开头返回 true,否则 false。
		// startsWith() 方法对大小写敏感。
		return attrName.startsWith('v-')
	}
	node2Fragment(el){
		//创建文档碎片
		const f = document.createDocumentFragment()
		//createDocumentFragment是用来创建一个虚拟的节点对象,或者说是创建一个文档碎片节点,它可以包含各种类型的节点,在创建之初是空的
		//DocumentFragment不属于文档树,继承的parentNode属性总是null。当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment自身,
		//而是它的所有子孙节点,即插入的是括号里的节点。这个特性使得DocumentFragment成了占位符,暂时存放那些一次插入文档的节点。
		//它还有利于实现文档的剪切、复制和粘贴操作。
		//如果使用appendChid方法将原dom树中的节点添加到DocumentFragment中时,会删除原来的节点。
		let firstChild;
		while(firstChild = el.firstChild){ //while是条件为真的情况下才执行,也就是必须el.firstChild有值的情况下才执行当判定while(firstChild)为真的情况执行
			f.appendChild(firstChild) //appendChild是剪切效果不是复制黏贴,dom会剪切到fragment
		}
		return f
	}
	isElementNode(node){
		return node.nodeType === 1
	}
}

更多createDocumentFragment的用法

CompileUtil

compile工具函数
compileUtil={
	getVal(expr,vm){ //取值
		// [person,name]
		return expr.split('.').reduce((data,currentVal)=>{ //[person,fav]
		// console.log(data)
		// console.log(currentVal)
			return data[currentVal] //这里触发了object.defineProtype()的get方法 并将watcher推入subs
		},vm.$data)
	},
	setVal(expr,vm,inputVal){ //expr:person.name 数据驱动视图
		return expr.split('.').reduce((data,currentVal)=>{ //[person,fav]
		 if(typeof data[currentVal] == 'object'){ 优化修复源码留下的bug
			 	return data[currentVal]
		  }
			data[currentVal] = inputVal
		},vm.$data)
	},
	getContent(expr,vm){
		return expr.replace(/\{\{(.+?)\}\}/g,(...arguments)=>{
			return this.getVal(arguments[1],vm)
		})
	},
	text(node,expr,vm){
		 //node:节点,expr:msg=》data.msg,vm:整个实例 v-text:'person.fav' {{}}
		 let value;
		 if(expr.indexOf('{{')!=-1){
			 // {{person.name}} -- {{person.age}}
			 value = expr.replace(/\{\{(.+?)\}\}/g,(...arguments)=>{
				 // console.log(arguments[1])
				 //绑定观察者,将来数据发生变化触发这里的回调 进行更新
				 new Watcher(vm,arguments[1],()=>{ //watcher 更新视图 编译阶段建立watcher  先调用observer再调用的compile
				 // console.log(this.getContent(expr,vm))
				 	this.updater.textUpdater(node,this.getContent(expr,vm))  //触发了html方法 回调回来新值
				 })
				 // 在函数调用时(不管是调用还是被调用),会自动在该函数内部生成一个名为 arguments 的隐藏对象,该对象是个类数组。
				 // console.log(arguments)
				 return this.getVal(arguments[1],vm)
			 })
		 }else{
			 value = this.getVal(expr,vm);
		 }
		// const value = vm.$data[expr]
		this.updater.textUpdater(node,value);
	},
	html(node,expr,vm){ //computil html方法
		const value = this.getVal(expr,vm)
		new Watcher(vm,expr,(newVal)=>{ //watcher 更新视图 编译阶段建立watcher  先调用observer再调用的compile
			this.updater.htmlUpdater(node,newVal)  //触发了html方法 回调回来新值
		})
		// watcher和数据绑定进行数据的监听
		this.updater.htmlUpdater(node,value)
	},
	model(node,expr,vm){
		const value = this.getVal(expr,vm)
		// 绑定更新函数,数据=》视图
		new Watcher(vm,expr,(newVal)=>{ //watcher 更新视图 编译阶段建立watcher  先调用observer再调用的compile
			this.updater.modelUpdater(node,newVal)  //触发了html方法 回调回来新值
		})
		//视图=》数据=》视图
		node.addEventListener('input',(e)=>{
			// 设置值
			this.setVal(expr,vm,e.target.value)
		})
		this.updater.modelUpdater(node,value)
	},
	on(node,expr,vm,eventName){
		let fn = vm.$options.methods && vm.$options.methods[expr]
		node.addEventListener(eventName,fn.bind(vm),false)
		//fn.bind(vm) 指向vm实例:MVue
		//fn.bind(this) 指向compileUtil类
		//fn 指向调用者,button
	},
	updater:{
		modelUpdater(node,value){
			node.value = value
		},
		htmlUpdater(node,value){
			node.innerHTML = value
		},
		textUpdater(node,value){
			//textContent 属性设置或者返回指定节点的文本内容。
			// 如果你设置了 textContent 属性, 任何的子节点会被移除及被指定的字符串的文本节点替换。
			node.textContent = value
		}
	}
}

三、Observer

劫持数据监听通过观察数据,发现数据变化时发布消息给依赖收集器Dep,通过noticy()通知观察者更新视图
class Observer{
	constructor(data){
		console.log('observer')
		this.observer(data)
	}
	observer(data){
	    /*
		{
			person:{
				name:'',
				fav:{
					a:''
				}
			}
		}
		*/
		if(data && typeof data=='object' ){ //判断是否为对象遍历并劫持
			// console.log(Object.keys(data))
			Object.keys(data).forEach(key=>{
				// console.log(key,'key')
				this.defineReactive(data,key,data[key])
			})
		}
	}
	// 劫持并监听所有的属性
	defineReactive(obj,key,value){ //修改数据先走set
		// 递归遍历	
	    this.observer(value)
		const dep = new Dep() //建立dep依赖收集器
		// console.log('push')
		// console.log(Dep.target) 初始化拿到data没有编译时只是赋予get和set方法,target还为null,并没有执行get方法,在编译阶段getval取值时才执行了get方法
		Object.defineProperty(obj,key,{ //做劫持
			enumerable:true, // 枚举
			configurable:false, // 修改
			get(){
				// 订阅数据变化时且当dep有watcher时往dep添加观察者    
                // 在编译阶段时建立watcher推入subs
				Dep.target && dep.addSub(Dep.target)
				// console.log(Dep.target,'target2')
				return value
			},
			set:(newVal)=>{
				// person = {age:1} 赋于age:get set
				this.observer(newVal)
				if(newVal!==value){
					value = newVal
				}
				//告诉dep通知变化
				dep.notify()
			},
			// get
			// 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。
			//执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
			// set
			// 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。
			//该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
		})
	}
}  

Dep依赖收集器

class Dep{ //依赖收集
	constructor() {
	    this.subs = []
		console.log('dep')
	}
	//收集观察者watcher
	addSub(watcher){
		// console.log(watcher)
		this.subs.push(watcher)
	}
	//通知观察者更新
	notify(){
		// console.log('通知观察者',this.subs)
		this.subs.forEach(w=>w.update())
	}
}

四、Watcher观察者

class Watcher{ //初始化数据时就建立了watcher,绑定到dep.target,在每个数据更新的方法中绑定
	constructor(vm,expr,cb) {
		console.log('watcher')
		this.vm = vm
		this.expr = expr
		this.cb = cb
		//先把旧值保存
	    this.oldVal = this.getOldVal()
	}
	getOldVal(){
		Dep.target = this
		let oldVal = compileUtil.getVal(this.expr,this.vm) //通过compileUtil.getVal触发了object.defineProtype()的get方法 将watcher推入subs 可以使用debugger查看执行栈
		Dep.target = null
		return oldVal
	}
	update(){
		let newVal = compileUtil.getVal(this.expr,this.vm)
		if(newVal!==this.oldVal){
			this.cb(newVal) //new watcher创建的cb
		}
	}
}

代码实现链接

视频观看请在哔哩哔哩搜索vue源码设计