1、通过Object.defineProperty(obj, prop, descriptor)劫持对象的属性读写,其中obj是要在上面定义属性的对象,prop是要定义或修改的属性名称,descriptor是属性的描述符。描述符中可选get和set键值。get是属性的getter方法,返回属性值;set为setter方法,接受唯一参数,并将该参数的值赋值给属性,get和set的默认值均为undefined。
2、双向绑定的简单实现。
<input type="input" id="input"> <span id="show"></span> <script> var obj = {}; Object.defineProperty(obj, 'txt', { get: function () { return obj; }, set: function (newValue) { document.getElementById('input').value = newValue; document.getElementById('show').innerHTML = newValue; } }); document.getElementById('input').addEventListener('keyup', function (e) { obj.txt = e.target.value; }); </script>
当通过input进行输入时,obj.txt的值会相应更新;当通过控制台改变obj.txt的值时,setter会改变view,从而实现了view=>model,model=>view的双向绑定。不过这种简单绑定不会真正执行obj.txt = e.target.value,obj永远为{},如果在set方法中赋值obj.txt = e.target.value,则会造成无限循环。为解决这个问题,将Object.defineProperty()封装为一个函数,即可在其中保存状态obj.txt,修改如下:
<input type="text" id="input"> <div id="show"></div> <script> function defineProperty(obj, attr){ var val; Object.defineProperty(obj, attr, { get: function () { return val; }, set: function (newValue) { if (newValue === val){ return; } val = newValue; document.getElementById("input").value = newValue; document.getElementById("show").innerHTML = newValue; } }); } var obj = {}; defineProperty(obj, "txt"); document.getElementById("input").addEventListener("keyup", function(e){ obj.txt = e.target.value; }) </script>上面就是一个最简单的双向绑定,但要实现类似于Vue的功能还需要继续改进。
3、model=>view绑定
<div id='app'> <input type="text" v-model="input"> {{text}} </div> <script> function compile(node, vm){ if(node.nodeType === 1){ var attr = node.attributes; for(let i = 0; i<attr.length; i++){ if(attr[i].nodeName === 'v-model'){ let name = attr[i].nodeValue; node.value = vm.data[name]; node.removeAttribute('v-model'); } } } if(node.nodeType === 3){ let reg = /\{\{(.*)\}\}/; if(reg.test(node.nodeValue)){ let name = RegExp.$1; name = name.trim(); node.nodeValue = vm.data[name]; } } } function nodeToFragment(node, vm){ var flag = document.createDocumentFragment(); var child; while(child = node.firstChild){ compile(child, vm); flag.appendChild(child); } return flag; } function Vue(options){ var id = options.el; this.data = options.data; var dom = nodeToFragment(document.getElementById(id), this); document.getElementById(id).appendChild(dom); } var vm = new Vue({ el: 'app', data: { input: 'hello', text: 'world' } }); </script>实现功能:通过Vue实例vm的el属性查找作用范围,范围内input节点中有v-model属性时,将input的值改为vm下data对应键的值,当检测到双花括号{{}}时,将该位置替换为vm下data对应属性的值。
vm通过new关键字声明,构造函数Vue中将el确定的节点及vm自身传入nodeToFragment函数,之后再将当前子节点替换为nodeToFragment函数返回的DocumentFragment对象。nodeToFragment函数将当前节点的子节点逐个传入编译函数compile,并将编译好的节点添加到DocumentFragment对象返回。编译函数compile()判断当前节点类型,节点为元素时,判断是否有v-model属性,并替换input.value,当节点为文本时,利用正则表达式判断是否含有花括号,并查找花括号内对应的值替换该节点。
存在的问题:
当{{}}不是app的子节点时,无法编译。解决方法是在compile()函数中,当节点为元素节点时,判断其是否有子节点,有则再次调用compile函数。
function compile(node, vm){ if(node.nodeType === 1){ var attr = node.attributes; for(let i = 0; i<attr.length; i++){ if(attr[i].nodeName === 'v-model'){ let name = attr[i].nodeValue; node.value = vm[name]; node.removeAttribute('v-model'); } } if (child = node.firstChild) { compile(child, vm); } } if(node.nodeType === 3){ let reg = /\{\{(.*)\}\}/; if(reg.test(node.nodeValue)){ let name = RegExp.$1; name = name.trim(); node.nodeValue = vm.data[name]; } } }4、view=>model绑定
依照2中方法,先将vm下data中所有值通过Object.defineProperty()赋值,之后在编译函数compile()中为所有v-model添加事件监听,改动部分如下。
function observe(data, vm){ Object.keys(data).forEach(function(key){ Object.defineProperty(vm, key, { get: function (){ return vm[key]; }, set: function (newValue){ document.getElementById("show").innerHTML = newValue; document.getElementById("input").value = newValue; } }); }); } function compile(node, vm){ if(node.nodeType === 1){ var attr = node.attributes; for(let i = 0; i<attr.length; i++){ if(attr[i].nodeName === 'v-model'){ let name = attr[i].nodeValue; node.addEventListener('keyup', function(e){ vm[name] = e.target.value; }); node.value = vm.data[name]; node.removeAttribute('v-model'); } } if (child = node.firstChild) { compile(child, vm); } } ... } function Vue(options){ var id = options.el; this.data = options.data; observe(this.data, this);//添加view=>model的绑定 ... } <div id='app'> <input type="text" v-model="input" id="input"> <span id="show"></span> </div>此时改变input的值时,span也会改变;通过console改变vm.input时,span也会改变。但当在console中输入vm.input时会出现无限循环的情况:
这是由于在getter中会反复调用自身,下面对observe进行改进,将Object.defineProperty从函数中单独取出,构成一个闭包。
function defineProperty(vm, key, val){ Object.defineProperty(vm, key, { get: function (){ return val; }, set: function (newValue){ document.getElementById("show").innerHTML = newValue; document.getElementById("input").value = newValue; if(newValue === val){ return; } val = newValue; } }); } function observe(data, vm){ Object.keys(data).forEach(function(key){ defineProperty(vm, key, data[key]); }); }
修改后vm.input的值就能正确改变和返回了。但此时view的变化是通过setter手动添加的,而且只能是元素形式的节点,如果节点是{{text}}模板字符串则无法动态改变。解决办法是采用订阅/发布模式进行修改,对每个节点绑定一个观察者Watcher,对每个数据绑定一个分发者Dep,数据改变时,调用分发者的通知方法,通知每个观察者进行相应的改变。
5、订阅/发布模式(subscribe&publish)
采用订阅/发布模式对代码进行修改。首先定义观察者Watcher,并在编译函数compile()中对每个节点添加观察着Watcher,当接收到分发者指令时,调用update方法更新视图。接下来定义消息分发者Dep,Dep维护观察者数组,当值发生变化时,通知各观察者调用update方法。完整代码如下:
//第三部分 function Watcher(vm, node, name, nodeType){ Dep.target = this; this.vm = vm; this.node = node; this.name = name; this.nodeType = nodeType; this.update(); Dep.target = null; } Watcher.prototype = { update: function(){ this.get(); if (this.nodeType === 'text') { this.node.nodeValue = this.value; } if (this.nodeType === 'input') { this.node.value = this.value; } }, get: function(){ this.value = this.vm[this.name]; } } function Dep(){ this.subs = []; } Dep.prototype = { addSub: function(sub){ this.subs.push(sub); }, notify: function(){ this.subs.forEach(function(sub){ sub.update(); }); } } //第二部分 function defineProperty(vm, key, val){ var dep = new Dep(); Object.defineProperty(vm, key, { get: function (){ if(Dep.target){ dep.addSub(Dep.target); } return val; }, set: function (newValue){ if(newValue === val){ return; } val = newValue; dep.notify(); } }); } function observe(data, vm){ //Object.keys(data)返回data的key数组 Object.keys(data).forEach(function(key){ defineProperty(vm, key, data[key]); }); } //第一部分 function compile(node, vm){ if(node.nodeType === 1){ var attr = node.attributes; for(let i = 0; i<attr.length; i++){ if(attr[i].nodeName === 'v-model'){ let name = attr[i].nodeValue; node.addEventListener('keyup', function(e){ vm[name] = e.target.value; }); node.value = vm[name]; node.removeAttribute('v-model'); new Watcher(vm, node, name, "input"); } } if (child = node.firstChild) { compile(child, vm); } } if(node.nodeType === 3){ let reg = /\{\{(.*)\}\}/; if(reg.test(node.nodeValue)){ let name = RegExp.$1; name = name.trim(); // node.nodeValue = vm.data[name]; new Watcher(vm, node, name, "text"); } } } function nodeToFragment(node, vm){ var flag = document.createDocumentFragment(); var child; while(child = node.firstChild){ compile(child, vm); flag.appendChild(child); } return flag; } function Vue(options){ var id = options.el; var data = options.data; observe(data, this); var dom = nodeToFragment(document.getElementById(id), this); document.getElementById(id).appendChild(dom); } var vm = new Vue({ el: 'app', data: { input: 'hello' } });
原文连接:https://blog.csdn.net/orangecsy/article/details/79759663
以上适用于类vue的mvvm框架,angular2使用的是自己实现的双向绑定
发表评论: