做者:佳傑本文原創,轉載請註明做者及出處javascript
視圖(view)和數據(model)之間的綁定
不用手動調用方法渲染視圖,提升開發效率;統一處理數據,便於維護
視圖(view):說白了就是html中dom元素的展現 數據(model):用於保存數據的引用類型
view > model的數據綁定:view改變,致使model改變 model > view的數據綁定:model改變,致使view改變
view > model的數據綁定實現方法 修改dom元素(input,textarea,select)的數據,致使model產生變化, 只要給dom元素綁定change事件,觸發事件的時候修改model便可,不細講 model > view的數據綁定實現方法 1.發佈訂閱模式(backbone.js用到); 2.數據劫持(vue.js用到); 3.髒值檢查(angular.js用到);
簡易思路 > 1.經過defineProperty來監控model中的全部屬性(對每個屬性都監控) > 2.編譯template生成DOM樹,同時綁定dom節點和model(例如<div id="{{model.name}}"></div>), defineProperty中已經給「model.name」綁定了對應的function, 一旦model.name改變,該funciton就操做上面這個dom節點,改變view 主要js模塊:Observer,Compile,ViewModel 1.Observer 用到了發佈訂閱模式和數據監控,defineProperty用於「監控model", dom元素執行"訂閱"操做,給model中 的屬性綁定function;model中屬性變化的時候,執行"發佈"這個操做,執行以前綁定的那個function 源碼以下: var Observer = function(opts) { this.id = (opts && opts.id) ? opts.id : +new Date(); this.opts = opts; this.subs = []; //觀察者數組 /*this.subs包含了全部觀察者,每一個觀察者的結構以下: { key:"person.age.range",//這個key表明model.person.age.range這個屬性 /* 和key綁定的函數數組,每一個函數操做一個dom節點, 一個key對應多個dom節點,因此actionList是個function數組; */ actionList:[function(){},function(){}] }*/ } Observer.prototype = { //遍歷model中全部的屬性,每一個屬性用defineKey來監控全部屬性 monit: function(data, baseUrl) { var me = this; baseUrl = baseUrl || ""; var isTypeMatch = (data && typeof data === "object"); if (isTypeMatch) { Object.keys(data).forEach(function(key) { var base = baseUrl ? (baseUrl + "." + key) : key; me.defineKey(data, key, data[key], baseUrl); //定義本身 me.monit(data[key], base); //遞歸【定義的是下一層】 }); } }, //用到了Object.defineProperty來定義屬性,這樣屬性改變的時候,就會自動執行裏面的set方法 defineKey: function(data, key, val, baseUrl) { var me = this; var base = baseUrl ? (baseUrl + "." + key) : key; Object.defineProperty(data, key, { enumerable: true, configurable: false, get: function() { return val; }, //更新並監控新的值,執行publish函數 set: function(newVal) { if (newVal !== val) { val = newVal; //設置新值須要從新監控 me.monit(newVal, base); //(baseUrl+"."+key)做爲觀察者模式中的監聽的那個key,也能夠說是監聽的那個事件 me.publish(base, newVal); } } }); }, /* 根據key來執行綁定在這個key上的全部函數,好比說person.age.range這個key, 它變更的時候,publish會執行綁定在person.age.range這個key上全部的function */ publish: function(key, newVal) { (this.subs || []).forEach(function(sub) { if (sub.key == key) { (sub.actionList || []).forEach(function(action) { action(newVal); }); } }); }, //給model中的某個key(例如person.age.range)添加綁定的function subscribe: function(key, callback) { var tgIdx; var hasExist = this.subs.some(function(unit, idx) { tgIdx = (unit.key === key) ? idx : -1; return (unit.key === key) }); if (hasExist) { if (Object.prototype.toString.call(this.subs[tgIdx].actionList)=="[object Array]"){ this.subs[tgIdx].actionList.push(callback); } else { this.subs[tgIdx].actionList = [callback]; } } else { this.subs.push({ key: key, actionList: [callback] }); } }, //取消訂閱 remove: function(key) { var removeIdx; this.subs.forEach(function(sub, idx) { removeIdx = sub.key === key ? idx : -1; return sub.key === key }); if (removeIdx !== -1) { this.subs.splice(removeIdx, 1); } }, isObject: function(data) { return data && typeof data === "object" } }; 2.Compile: 模板編譯器 var Compile = function(opts) { this.opts = opts; this.data = this.opts.data; this.observer = this.opts.observer; this.regExp = /\{\{([\s\S]*)\}\}/; this.ele = document.createElement("div"); this.ele.innerHTML = opts.template; //渲染頁面 this.fragment = this.transToFrament(this.ele); this.travelAllNodes(this.fragment); this.ele.appendChild(this.fragment); }; Compile.prototype = { //把頁面上的dom節點轉化成文檔碎片,防止dom頻繁操做影響頁面性能 transToFrament: function(el) { var fragment = document.createDocumentFragment(), child; // 將原生節點拷貝到fragment while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }, //遍歷文檔碎片節點下全部的node節點(用到了函數遞歸調用),執行compileNode travelAllNodes: function(ele) { this.compileNode(ele); ([].slice.call(ele.childNodes) || []).forEach(function(node) { this.compileNode(node); if (node.childNodes && node.childNodes.length) { this.travelAllNodes(node); } }.bind(this)); }, /*包含功能 1.渲染node節點 2.給key設置callback函數,函數內操做node節點 */ compileNode: function(node) { if (this.isElement(node)) { this.compileElementNode(node); } else if (this.isText(node)) { this.compileTextNode(node); } }, /* 編譯element類型的node節點, 須要處理屬性綁定v-bind="{{data.name}}"和 事件v-event="{{data.event}}" */ compileElementNode: function(node) { var me = this, nodeAttrs = node.attributes; [].slice.call(nodeAttrs).forEach(function(attr) { var attrName = attr.name; var attrValue = attr.value; var key = me.getKey(attrValue); me.bindKeyToNode(key, attr); attr.value = me.compileString(attrValue); //渲染node }); }, //編譯文本類型的node節點,裏面放了對應的"{{data.name}}"這種數據格式 compileTextNode: function(ele) { var key = this.getKey(ele.textContent); this.bindKeyToNode(key, ele); ele.textContent = this.compileString(ele.textContent); }, //解析「{{}}」,把它變成對應的數據值 compileString: function(str) { var key = this.getKey(str); return str.replace(this.regExp, this.getValueByKey(key)); }, //綁定key和node節點,key一旦改變,就會觸發對應的函數,修改node節點 bindKeyToNode: function(key, node) { if (!!key.trim()) { console.log(key); var nodeType = node.nodeType; var regExp = new RegExp("\\{\\{" + key + "\\}\\}"); var originTextConetnt; if (nodeType === 2) { originTextConetnt = node.value; } else if (nodeType === 3) { originTextConetnt = node.textContent; } this.observer.subscribe(key, function(newVal) { var tgValue = originTextConetnt.replace(regExp, newVal); if (nodeType === 2) { node.value = tgValue; } else if (nodeType === 3) { node.textContent = tgValue; } }); } }, //從{{name.age.sex}}中獲取name.age.sex getKey: function(str) { return str.match(this.regExp) ? str.match(this.regExp)[1] : ""; }, //獲取key對應的value值 getValueByKey: function(key) { var arr = key ? key.split(".") : []; var temp = this.data; for (var i = 0; i < arr.length; i++) { if (temp) { temp = temp[arr[i]]; } else { temp = undefined; break } } return temp; }, isElement: function(ele) { return ele.nodeType === 1 ? true : false; }, isText: function(ele) { return ele.nodeType === 3 ? true : false; }, getElement: function() { return this.ele; } } 3.ViewModel:結合Observer與Compile,實現model > view的數據單向綁定 var ViewModel = function(opts) { this.opts = opts; this.data = opts.data; this.wrapper = opts.wrapper; this.template = opts.template; this.Observer = (typeof Observer != undefined) ? Observer : opts.Observer; this.Compile = (typeof Compile != undefined) ? Compile : opts.Compile; this.init(); } ViewModel.prototype = { init: function() { var opts = this.opts; this.observer = new this.Observer(opts); this.observer.monit(this.data); //監控數據變化,數據已經改變了 this.compiler = new this.Compile(Object.assign(opts, { observer: this.observer })); //編譯生成節點 if (this.wrapper) { this.wrapper.appendChild(this.compiler.getElement()); } }, get: function() { return this.compiler.getElement(); } };
簡單地調用new ViewModel({data:data,template:template}),完成了model和view的綁定, ViewModel內部大體執行順序是: 1. 建立數據監控對象this.observer,該對象監控data(監控之後,data的屬性改變, 就會執行defineProperty中的set函數,set函數裏面添加了publish發佈函數) 2. 建立模板編譯器對象this.compiler,該對象編譯template,生成最終的dom樹, 而且給每一個須要綁定數據的dom節點添加了subscribe訂閱函數 3. 最後,改變data裏面的屬性,會自動觸發defineProperty中的set函數,set函數調用publish函數, publish會根據key的名稱,找到對應的須要執行的函數列表,依次執行全部函數
https://github.com/devil1989/databind/
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <link rel="stylesheet" type="text/css" href="demo.css"> <script type="text/javascript" src="./observe.js"></script> </head> <body> <template id="inner" type="text/template"> <div title="{{des}}"> <div> <ul id="list"> <li > <span >age:</span> <input type="text" name="" value="{{age}}" > <span id="age" style="float: left;">+</span> </li> <li> <span>name:</span> <input id="firstName" type="text" name="" value="{{name}}"> </li> <li><span>{{name}}</span></li> </ul> </div> </div> </template> <script type="text/javascript"> (function(){ window.data={name:"jeffrey",age:28,des:"測試"}; var vm=new VM({ data:data, template:document.getElementById("inner").innerHTML /* wrapper:document.body//能夠指定對應容器,也能夠不指定容器, 直接獲取元素,再手動插入對應dom元素*/ }); document.body.appendChild(vm.get()); document.getElementById("age").addEventListener("click",function(){ data.age++;//只須要修改屬性,html就會從新渲染 }); document.getElementById("firstName").addEventListener("keyup",function(e){ data.name=this.value;//只須要修改屬性,html就會從新渲染 }); })(); </script> </body> </html>
當咱們想要修改頁面某個元素的信息,但又不想費勁地查找dom元素再去修改元素的值, 這種狀況下,能夠用demo中的數據綁定,只需修改數據的值,就實現了頁面元素從新渲染 請看下面的gif動畫中展現的,只要修改data.age和data.name,頁面元素就自動從新渲染了
本demo只是簡單實現數據綁定,不少功能並未實現,只是提供一種思路,拋磚引玉;
若是對上述代碼中的Observer類的代碼不是很理解,能夠先了解下觀察者模式以及實現原理;
最後,感謝你們的閱讀!!css