学习前端也有半年多了,我的的学习欲望还比较强烈,很喜欢那种新知识在本身的演练下一点点实现的过程。最近一直在学vue框架,像网上大佬说的,入门容易深究难。不论是跟着开发文档学仍是视频教程,按步骤操做老是最肤浅,想要把这门功课作好毕竟得下足功夫。所以,特地花了好几天时间阅读相关技术博客和源码,简单实现了一个数据双向绑定的vue框架,但愿能让各位有点启发...html
MVVM即modle-view-viewmole,MVVM最先由微软提出来,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,二者作到了最大限度的分离。把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。前端
在vue框架中,经过控制台或者Vue Devtools修改data里的一些属性时会看到页面也会更新,而在页面修改数据时,data里的属性值也会发生改变。咱们就把这种model和view同步显示称为是双向绑定。其实单向绑定原理也差很少,视图改变data更新经过事件监听就能轻松实现了,重点都在但愿data改变视图也发生改变,而这就是咱们下面要讲的原理。vue
首先要知道的是vue的数据绑定经过数据劫持配合发布订阅者模式实现的,那么什么是数据劫持呢?咱们能够在控制台看一下它的初始化对象是什么样的:node
let vm = new Vue({ el:"#app", data:{ obj:{ a:1 } }, created() { console.log(this.obj) }, })
能够看到属性a分别对应着一个get 和set方法,这里引伸出Object.defineProperty()方法,传递三个参数,obj(要在其上定义属性的对象)、prop(要定义或修改的属性的名称)、descriptor(将被定义或修改的属性描述符)。该方法更多信息参考:参考更多用法,着重强调一下get和set这两个属性描述键值。git
日常咱们在打印一个对象属性时会这样作: github
var obj = { name:"tnagj" } console.log(obj.name) //tangj
若是咱们想要在输出的同时监听obj的属性值,而且输出的是tangjSir呢?这时候咱们的set和get属性就起到了很好的做用数组
var obj ={}; var name = ''; Object.defineProperty(obj,'name',{ set:function(value){ name = value console.log('我叫:' + name) }, get:function(){ console.log(name + 'Sir') } }) obj.name = 'tangj'; //我叫tangj obj.name; //tangjSir
首先咱们定义了一个obj空对象以及name空属性,再用一个Object.defineProperty()方法来判断obj.name的访问状况,若是是读值则调用get函数,若是是赋值则调用set函数。在这两个函数里面咱们分别对输出的内容做了更改,所以在get方法调用时打印tangjSir,在set方法调用时打印我叫tangj。缓存
其实这就是vue数据绑定的监听原理,咱们能经过这个简单实现MVVM双向绑定。app
view的变化,好比input值改变咱们很容易就能知道经过input事件反应到data中,数据绑定的关键在于怎样让data更新view。首先咱们要知道数据何时变的,上文提过能够用Object.defineProperty()的set属性描述键值来监听这个变化,当数据改变时就调用set方法。框架
那么咱们能够设置一个监听器Observe,用来监听全部的属性,当属性变化的时候就须要告诉订阅者Watcher看是否须要更新。由于订阅者是有不少个,因此咱们须要有一个消息订阅器Dep来专门收集这些订阅者,而后在监听器Observer和订阅者Watcher之间进行统一管理的。固然咱们还须要有一个指令解析器Compile,对每一个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此,咱们大体能够把整个过程拆分红五个部分,MVVM.html,MVVM.js,compile.js,observer.js,watcher.js,咱们在MVVM.js中建立所须要的实例,在.html文件中引入这些js文件,这样拆分更容易理解也更好维护。
为了和Vue保持一致,咱们向MVVM.js传入一个空对象options,并让vm.$el = options.el,vm.$data = options.data,若是能取到vm.$el再进行编译和监听
class MVVM { constructor(options){ this.$el = options.el, //把东西挂载在实例上 this.$data = options.data if(this.$el){ // 若是有要编译的就开始编译 new Observer(this.$data); //数据劫持,就是把对象的全部属性改为get和set方法 new Compile(this.$el,this);//用数据和元素进行编译 } } }
编译的时候有一个问题须要注意,若是直接操做DOM元素会特别消耗性能,因此咱们但愿先把DOM元素都放在内存中即文档碎片,待编译完成再把文档碎片放进真实的元素中
class Complie{ constructor(el,vm){ this.el = this.isElementNode(el)?el:document.querySelector(el); this.vm = vm; if(this.el){//若是这个元素能获取到,咱们才开始编译 let fragment = this.nodeToFragment(this.el); //1.先把真实的DOM移入到内存中,fragment this.compile(fragment); //2.编译=>提取想要的元素节点v-modle 和文本节点{{}} this.el.appendChild(fragment) //3.把编译好的fragment塞回页面 } nodeToFragment(el){ //须要el元素放到内存中 let fragment = document.createDocumentFragment(); let Child; while(Child = el.firstChild){ fragment.appendChild(Child); } return fragment; } }
}
接下来咱们要判断须要编译的是元素节点仍是文档节点,还记得Vue中有不少颇有用的指令吗?好比"v-modle"、"v-for"等,因此咱们还要判断元素节点内是否包含指令,若是是指令,它应该包含一些特殊的方法
/* 省略.... */ isElementNode(node){ //是否是元素节点 return node.nodeType === 1; } isDirective(name){ //是否是指令 return name.includes('v-') } compileElement(node){ //带v-modle let attrs = node.attributes; Array.from(attrs).forEach( attr =>{ let attrName = attr.name; if(this.isDirective(attrName)){ // 取到对应的值放到节点中 let expr = attr.value; // node vm.$data expr let [,type] = attrName.split('-') //解构赋值 CompileUtil[type](node,this.vm,expr) } } ) } compileText(node){ // 带{{}} let expr = node.textContent; //取文本的内容 let reg = /\{\{([^}]+)\}\}/g //全局匹配 if(reg.test(expr)){ // node this.vm.$data expr CompileUtil['text'](node,this.vm,expr) } } compile(fragment){ //须要递归,拿到的childNodes只是第一层 let childNodes = fragment.childNodes; Array.from(childNodes).forEach( node=>{ if(this.isElementNode(node)){ //是元素节点,还须要递归检查 this.compileElement(node) //编译元素 this.compile(node) //箭头函数this指向上一层的实例 }else{ //文本节点 this.compileText(node) //编译文本 } } ) }
根据获取的节点类型不一样,执行不一样的方法,咱们可把这些方法统一都放到一个对象里面去
CompileUtil = { getVal(vm,expr){ //获取实例上的数据 expr = expr.split('.'); //若是遇到vm.$data[a.a],但愿先拿到vm.$data[a] return expr.reduce((prev,next)=>{ return prev[next] },vm.$data) }, getTextVal(vm,expr){ //获取编译文本之后的结果 return expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{ return this.getVal(vm,arguments[1]); }) }, text(node,vm,expr){ // 文本处理 let updateFn = this.updater['textUpdater']; let value = this.getTextVal(vm,expr); updateFn && updateFn(node,value); }, modle(node,vm,expr){ // 输入框处理 let updateFn = this.updater['modleUpdater'] updateFn && updateFn(node,this.getVal(vm,expr)) }, updater:{ textUpdater(ndoe,value){ ndoe.textContent = value //文本更新 }, modleUpdater(node,value){ node.value = value } } }
编译的时候咱们还须要一个监听者,当数据变化调用get和set方法
class Observer{ constructor(data){ this.observer(data) } observer(data){ if(!data || typeof data !== 'object') return; Object.keys(data).forEach(key =>{ this.defineReactive(data,key,data[key]); this.observer(data[key]) }) } defineReactive(obj,key,value){ let that = this; Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get(){ Dep.target && dep.addSub(Dep.target); return value; }, set(newvalue){ if(value === newvalue) return; that.observer(newvalue); //若是新值是对象,继续劫持 value = newvalue; }, }) } }
前面已经实现了监听和编译,可是怎么样才能让它们之间进行通讯呢,也就是当监听到变化了怎么通知呢?这里就用到了发布订阅模式。默认观察者watcher有一个update方法,它会更新数据。Dep里面建立一个数组,把观察者都放在这个数组里面,当监听到变化,一个个调用监听者update方法。
// Watcher.js //观察者的目的就是给须要变化的那个元素增长一个观察者,当数据变化后执行对应的方法 class Watcher{ constructor(vm,expr,cb){ this.vm = vm; this.expr = expr; this.cb = cb; //先获取老的值 this.value = this.get() } getVal(vm,expr){ //获取实例上的数据 expr = expr.split('.'); //若是遇到vm.$data[a.a],但愿先拿到vm.$data[a] // console.log(expr) return expr.reduce((prev,next)=>{ return prev[next] },vm.$data) } get(){ Dep.target = this; //缓存本身 let value = this.getVal(this.vm,this.expr); Dep.target = null; //释放本身 return value; } update(){ let newValue = this.getVal(this.vm,this.expr); let oldValue = this.value; if(newValue != oldValue){ this.cb(newValue); } } } //Dep.js class Dep{ constructor(){ //订阅的数组 this.subs = [] } addSub(watcher){ this.subs.push(watcher) } notify(){ this.subs.forEach(watcher =>{ watcher.update() }) } }
watcher逻辑: 当建立watcher实例的时候,先拿到这个值,数据变化又拿到一个新值,若是新值和老值不同,那么调用callback,实现更新;
dep逻辑:建立数组把观察者放在这个数组里,当监听到变化,执行watcher.update()
咱们再它们分别添加到Observer和compile中
// complie.js // 省略.... text(node,vm,expr){ // 文本处理 let updateFn = this.updater['textUpdater']; //{{message.a}} => tangj let value = this.getTextVal(vm,expr); expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{ new Watcher(vm,arguments[1],(newVaule)=>{ // 若是数据变化,文本节点须要从新获取依赖的属性更新文本的的内容 updateFn && updateFn(node,this.getTextVal(vm,expr)); }) }) updateFn && updateFn(node,value); }, modle(node,vm,expr){ // 输入框处理 let updateFn = this.updater['modleUpdater'] // 'message.a' => [message.a] vm.$data['message'].a // 这里应该加一个监控,数据变化,调用这个watch的cb new Watcher(vm,expr,(newVaule)=>{ //当值变化后将调用cb,将新的值传递过来 updateFn && updateFn(node,this.getVal(vm,expr)) }); node.addEventListener('input',(e)=>{ let newVaule = e.target.value; this.setVal(vm,expr,newVaule) }) updateFn && updateFn(node,this.getVal(vm,expr)) } // 省略...
// observer.js
class Observer{ constructor(data){ this.observer(data) } observer(data){ //要对这个data数据原有属性改为set和get的形式 if(!data || typeof data !== 'object') return; Object.keys(data).forEach(key =>{ this.defineReactive(data,key,data[key]); this.observer(data[key]) }) } defineReactive(obj,key,value){ let that = this; let dep = new Dep(); //每一个变化的数据都会对应一个数组,这个数据存放了全部数据的更新 Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get(){ Dep.target && dep.addSub(Dep.target); return value; }, set(newvalue){ if(value === newvalue) return; that.observer(newvalue); //若是新值是对象,继续劫持 value = newvalue; dep.notify(); //通知全部人数据更新 }, }) } } class Dep{ constructor(){ //订阅的数组 this.subs = [] } addSub(watcher){ this.subs.push(watcher) } notify(){ this.subs.forEach(watcher =>{ watcher.update() }) } }
到这里咱们就实现了数据的双向绑定,MVVM做为数据绑定的入口,整合Observer、Compile和Watcher三者,经过Observer来监听本身的model数据变化,经过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通讯桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变动的双向绑定效果。
固然咱们还须要数据代理,用vm代理vm.$data,也是经过Object.defineProperty()实现
proxyData(data){ Object.keys(data).forEach(key =>{ let val = data[key] Object.defineProperty(this,key,{ enumerable:true, configurable:true, get(){ return val }, set(newval){ if(val == newval){ return; } val = newval } }) }) }
本次学习源码已上传github:https://github.com/Tangjj1996/MVVM,喜欢的朋友能够stars
PS:MVVM是学习框架很是重要的一步,掌握了这些原理才能更好地运用,知其然更要知其因此然,水平有限有错误的地方烦请多多指教