上次我们已经分析了vue.js是通过Object.defineProperty以及发布订阅模式来进行数据劫持和监听,并且实现了一个简单的demo。今天,我们就基于上一节的代码,来实现一个MVVM类,将其与html结合在一起,并且实现v-model以及{{}}语法。
tips:本节新增代码(去除注释)在一百行左右。使用的Observer和Watcher都是延用上一节的代码,没有修改。
接下来,让我们一步步来,实现一个MVVM类。
构造函数
首先,一个MVVM的构造函数如下(和vue.js的构造函数一样):
class MVVM { constructor({ data, el }) { this.data = data; this.el = el; this.init(); this.initDom(); } }
和vue.js一样,有它的data属性以及el元素。
初始化操作
vue.js可以通过this.xxx的方法来直接访问this.data.xxx的属性,这一点是怎么做到的呢?其实答案很简单,它是通过Object.defineProperty来做手脚,当你访问this.xxx的时候,它返回的其实是this.data.xxx。当你修改this.xxx值的时候,其实修改的是this.data.xxx的值。具体可以看如下代码:
class MVVM { constructor({ data, el }) { this.data = data; this.el = el; this.init(); this.initDom(); } // 初始化 init() { // 对this.data进行数据劫持 new Observer(this.data); // 传入的el可以是selector,也可以是元素,因此我们要在这里做一层处理,保证this.$el的值是一个元素节点 this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el); // 将this.data的属性都绑定到this上,这样用户就可以直接通过this.xxx来访问this.data.xxx的值 for (let key in this.data) { this.defineReactive(key); } } defineReactive(key) { Object.defineProperty(this, key, { get() { return this.data[key]; }, set(newVal) { this.data[key] = newVal; } //前端全栈学习交流圈:866109386 })//面向1-3年前端开发人员 }//帮助突破技术瓶颈,提升思维能力。 // 是否是属性节点 isElementNode(node) { return node.nodeType === 1; } }
在完成初始化操作后,我们需要对this.$el的节点进行编译。目前我们要实现的语法有v-model和{{}}语法,v-model这个属性只可能会出现在元素节点的attributes里,而{{}}语法则是出现在文本节点里。
fragment
在对节点进行编译之前,我们先考虑一个现实问题:如果我们在编译过程中直接操作DOM节点的话,每一次修改DOM都会导致DOM的回流或重绘,而这一部分性能损耗是很没有必要的。因此,我们可以利用fragment,将节点转化为fragment,然后在fragment里编译完成后,再将其放回到页面上。
class MVVM { constructor({ data, el }) { this.data = data; this.el = el;//前端全栈交流学习圈:866109386 this.init();//针对1-3年前端开发人员 this.initDom();//帮助突破技术瓶颈,提升思维能力。 } initDom() { const fragment = this.node2Fragment(); this.compile(fragment); // 将fragment返回到页面中 document.body.appendChild(fragment); } // 将节点转为fragment,通过fragment来操作DOM,可以获得更高的效率 // 因为如果直接操作DOM节点的话,每次修改DOM都会导致DOM的回流或重绘,而将其放在fragment里,修改fragment不会导致DOM回流和重绘 // 当在fragment一次性修改完后,在直接放回到DOM节点中 node2Fragment() { const fragment = document.createDocumentFragment(); let firstChild; while(firstChild = this.$el.firstChild) { fragment.appendChild(firstChild); } return fragment; } }
实现v-model
在将node节点转为fragment后,我们来对其中的v-model语法进行编译。
由于v-model语句只可能会出现在元素节点的attributes里,因此,我们先判断该节点是否为元素节点,若为元素节点,则判断其是否是directive(目前只有v-model),若都满足的话,则调用CompileUtils.compileModelAttr来编译该节点。
编译含有v-model的节点主要有两步:
为元素节点注册input事件,在input事件触发的时候,更新vm(this.data)上对应的属性值。
对v-model依赖的属性注册一个Watcher函数,当依赖的属性发生变化,则更新元素节点的value。
class MVVM { constructor({ data, el }) { this.data = data; this.el = el; this.init(); this.initDom(); } initDom() { const fragment = this.node2Fragment(); this.compile(fragment); // 将fragment返回到页面中 document.body.appendChild(fragment); } compile(node) { if (this.isElementNode(node)) { // 若是元素节点,则遍历它的属性,编译其中的指令 const attrs = node.attributes; Array.prototype.forEach.call(attrs, (attr) => { if (this.isDirective(attr)) { CompileUtils.compileModelAttr(this.data, node, attr) } }) } // 若节点有子节点的话,则对子节点进行编译 if (node.childNodes && node.childNodes.length > 0) { Array.prototype.forEach.call(node.childNodes, (child) => { this.compile(child); }) } } // 是否是属性节点 isElementNode(node) { return node.nodeType === 1; } // 检测属性是否是指令(vue的指令是v-开头) isDirective(attr) { return attr.nodeName.indexOf('v-') >= 0; } } const CompileUtils = { // 编译v-model属性,为元素节点注册input事件,在input事件触发的时候,更新vm对应的值。 // 同时也注册一个Watcher函数,当所依赖的值发生变化的时候,更新节点的值 compileModelAttr(vm, node, attr) { const { value: keys, nodeName } = attr; node.value = this.getModelValue(vm, keys); // 将v-model属性值从元素节点上去掉 node.removeAttribute(nodeName); node.addEventListener('input', (e) => { this.setModelValue(vm, keys, e.target.value); }); new Watcher(vm, keys, (oldVal, newVal) => { node.value = newVal; }); }, /* 解析keys,比如,用户可以传入 * <input v-model="obj.name" /> * 这个时候,我们在取值的时候,需要将"obj.name"解析为data[obj][name]的形式来获取目标值 */ parse(vm, keys) { keys = keys.split('.'); let value = vm; keys.forEach(_key => { value = value[_key]; }); return value; }, // 根据vm和keys,返回v-model对应属性的值 getModelValue(vm, keys) { return this.parse(vm, keys); }, // 修改v-model对应属性的值 setModelValue(vm, keys, val) { keys = keys.split('.'); let value = vm; for(let i = 0; i < keys.length - 1; i++) { value = value[keys[i]]; } value[keys[keys.length - 1]] = val; }, }
实现{{}}语法