设计一个简单mvvm例子

1. 引言 学习vue有段时间了,mvvm在vue中是个典型应用,最近参考了参考网上一些资料,整理了一下,也加入了自己的理解,实现一个简单版的demo,也方便有些面试的同学遇到设计一个mvvm的面试题。 2. 逻辑结构 mvvm的设计模式是“发布与订阅者”模式(observe/watcher),主要步骤有三步: observe来劫持并监听所有的属性(也就是vue中的data) 给每一个需要监听的属性,绑定一个订阅者(watcher) 当observe监听到属性变化时,通知watcher去更新视图 ok,步骤讲完了,接下来就开始实现每一步 3. observe observe的主要功能: 劫持和监听数据 当数据更新时,触发通知(后面的dep会讲,这里跳过) 那么observe劫持和监听数据的呢?用Object.defineProperty来实现 先看一个例子: function observe (obj) { var keys = Object.keys(obj) keys.forEach(function(key){ var val = void 0; Object.defineProperty(obj, key, { enumerable : true, configurable : true, get : function () { console.log('这个属性是', key) return val; }, set : function ( newValue ) { val = newValue console.log('属性' + key + '已经被监听了,此时的值是:' + newValue) } }) }) } var book = {page : 300} observe(book) var b = book.page // 后台会打印 这个属性是page book.page = 400 // 后台会打印 属性page已经被监听,此时的值是400 在这里我们就实现了属性的监听,上述例子中,我们用Object.defineProperty重写了set和get函数,使得属性的值变化时可以被我们监听到(如果不了解Object.defineProperty的,可以查阅Object.defineProperty ok,原理我们清楚了,接下来就开始写observe了 //data对象,key和val分别是data的键值对 function defineReactive(data, key, val){ observe(val) //递归调用data中的子对象 Object.defineProperty(data, key, { enumerable : true, //可枚举,可在for in 和 Object.keys中得到 configurable : true, get : function () { return val; }, set : function ( newValue ) { if(val === newValue){ return; } val = newValue console.log('属性' + key + '已经被监听了,此时的值是:' + newValue) } }) } //观察者,用来监听数据 function observe (obj) { if(!obj || typeof obj !== 'object'){ return; } Object.keys(obj).forEach(function(key){ defineReactive(obj, key, obj[key]) }) } defineReactive函数的三个参数,分别是要注册的对象,对象的key,以及对象的值 defineReactive中调用observe,目的是递归调用所有的属性 3. watcher observe写完了,接下来我们就要看watcher了,因为每个属性都绑定一个watcher,所以可能会有很多的watcher,因此我们需要一个调度中心(暂时定义为Dep),来统一指挥watcher Dep的主要功能: 将每个watcher都push进去 当接收到observe的属性更新通知时,通知对应的watcher来更新视图 接下来上代码 //订阅器,用来收集订阅者,并且通知订阅者更新函数 function Dep(){ this.subs = [] } Dep.prototype = { addSub : function (sub){ this.subs.push(sub) }, notify : function (){ this.subs.forEach(function(sub){ sub.update() }) } } Dep已经定义好了,接下来我们需要改一下observe,将dep加进去,这样我们就实现了在get函数中将属性注册一个watcher再push进dep中,并且set函数中数据更新时通dep,dep会再通知watcher去更新视图 function defineReactive(data, key, val){ observe(val) //递归调用data中的子对象 var dep = new Dep(); Object.defineProperty(data, key, { enumerable : true, //可枚举,可在for in 和 Object.keys中得到 configurable : true, get : function () { // 这里目的是定义一个flag,用来判断什么时候需要push一个sub //因为不能每次调用属性都push一个sub,只有在第一次时才需要push if(Dep.target){ dep.addSub(Dep.target) } return val; }, set : function ( newValue ) { if(val === newValue){ return; } val = newValue console.log('属性' + key + '已经被监听了,此时的值是:' + newValue) dep.notify() } }) } 到这里Dep调度中心就完成了,接下来我们实现watcher watcher的主要功能: observe中get函数只是定义了一个watcher,但是触发这个get函数需要在这里,这样就完成了注册 接到dep的更新通知后,调用更新函数 ok,先实现代码: function Watcher (vm, exp, cb) { this.vm = vm this.exp = exp this.cb = cb this.value = this.get() } Watcher.prototype = { update : function(){ this.run() }, run : function (){ var value = this.vm.data[this.exp] var oldVal = this.value if (value !== oldVal) { this.value = value this.cb.call(this.vm, value, oldVal) } }, get:function(){ Dep.target = this //这个暂时不知道干嘛 var value = this.vm.data[this.exp] Dep.target = null return value } } Watcher的三个参数,分别是vue,要订阅的属性,以及回调函数(触发更新时调用的函数) this.value = this.get()这行代码就是初始化就去获取这个属性值,这样就会调用observe中的get函数,然后将watcher加入到Dep中去。 Dep.target = this 就是上文中提到的只有target有值时才会将watcher加入到Dep中 ok,到这里最简易版本的mvvm已经完成了 然后我们定义一个vue: // data 是所有的属性,el是绑定的元素节点(#app),exp是绑定的属性 function dVue (data, el, exp) { this.data = data; observe(data); el.innerHTML = this.data[exp]; // 初始化模板数据的值 new Watcher(this, exp, function (value) { el.innerHTML = value; }); return this; } 在html中调用

{{math}}

OK,接下来我们完善一下,实现vue中的{{ }}绑定 4. compile compile主要功能: 获取模板,并且解析模板,将数据替换模板,完成初始化视图 给模板中绑定的属性,new初始化一个watcher(之前是在dVue函数中完成的,现在移到这里) function Compile(el, vm){ this.vm = vm this.el = document.querySelector(el) this.fragment = null this.init() } Compile.prototype = { init : function(){ if(this.el){ this.fragment = this.nodeToFragment(this.el) this.compileElement(this.fragment) this.el.appendChild(this.fragment) }else{ console.log('节点不存在') } }, nodeToFragment : function(el){ //创建一个虚拟的文档片段,用来操作dom节点,因为这个片段是存在于内存中 //所以相对于直接操作dom,性能会更好一点 var fragment = document.createDocumentFragment() var child = el.firstChild while(child){ fragment.appendChild(child) child = el.firstChild } return fragment }, compileElement :function(el){ var childNodes = el.childNodes var self = this; Array.prototype.slice.call(childNodes).forEach(function(node){ var reg = /\{\{\s*(.*?)\s*\}\}/ var text = node.textContent; //判断该节点是否含有{{ }}这个指令 if(self.isTextNode(node) && reg.test(text)){ self.compileText(node, reg.exec(text)[1]) } if(node.childNodes && node.childNodes.length){ self.compileElement(node) } }) }, compileText :function(node, exp){ var self = this var initText = this.vm[exp] this.updateText(node, initText) //初始化视图 new Watcher(this.vm, exp, function(value){ self.updateText(node, value) }) }, updateText : function(node, value){ node.textContent = typeof value == 'undefined' ? '' : value }, isTextNode : function(node){ return node.nodeType == 3 } } nodeToFragment是在内存建立一个虚拟的节点,然后将模板赋值给它,再继续操作模板,这样可以提升性能,参考文档nodeToFragment compileElement这个函数,解析模板,找到{{ }}指令的文本节点,然后运行核心函数compileText,解析文本节点 compileText这个函数中,做了两件事,第一件事是初始化视图,也就是调用updateText函数,第二件事就是给这个文本节点绑定一个watcher,用于订阅该属性,当属性值改变时,会调用里面的回调函数 到这里compile就完成了,这样我们需要把dVue重新修改一下 function dVue (options) { var self = this this.data = options.data this.vm = this Object.keys(this.data).forEach((key)=>{ this.proxyKeys(key) }) //重写所有的data属性的set和get方法,用于劫持监听数据 observe(this.data) //编译模板,得到绑定的节点,初始化视图,并且给该节点所绑定的属性注册一个watcher new Compile(options.el, this) return this } //代理一下属性,这样的话 dVue.name = dVue.data.name ,不用每次都带着data了 //相当于把data的所有属性都注册到了dVue上 dVue.prototype = { proxyKeys : function(key){ var self = this Object.defineProperty(this, key, { enumerable : false, configurable : true, get : function(){ return self.data[key] }, set :function(val){ self.data[key] = val } }) } } 到这里就基本结束了,我们在html中调用一下 实现一个简单的mvvm
{{name}}
{{age}}
{{like}}
到此结束! 参考文章: https://www.cnblogs.com/libin-1/p/6893712.html https://github.com/canfoo/self-vue/tree/master/v2

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):