mvvm模式即model-view-viewmodel模式簡稱,單項/雙向數據綁定的實現,讓前端開發者們從繁雜的dom事件中解脫出來,很方便的處理數據和ui之間的聯動。
本文將從vue的雙向數據綁定入手,剖析mvvm庫設計的核心代碼與思路。javascript
數據一旦改變則更新數據對應的ui前端
ui改變則觸發事件改變ui對應的數據vue
經過dom節點的指令獲取刷新函數,用來刷新指定的ui。java
實現一個橋接的方法,讓刷新函數和須要的數據關聯起來node
監聽數據變化,數據改變後經過橋接方法調用刷新函數git
ui改變觸發對應的dom事件在改變特定的數據github
實現observer,從新定義data,爲data上每一個屬性增長setter,getter以監聽數據的變化正則表達式
實現compile,掃描模版template,提取每一個dom節點中的指令信息segmentfault
實現directive,經過指令信息是實例化對應的directive實例,不一樣類型的directive擁有不一樣的刷新函數update數組
實現watcher,讓observer的屬性監聽函數與directive的update函數作一一對應,以實現數據變化後更新視圖
MVVM目前劃分爲observer,compile,directive,watcher四個模塊
經過es5規範中的object.defineProperty方式實現對數據的監聽
實現思路:
遞歸遍歷data,將data下面全部屬性都加上set,get方法,以實現對全部屬性的攔截.
注意:對象可能含有數組屬性,數組的內置有push,pop,splice等方法改變內部數據.
此時作法是改變數組的原型鏈,在原型鏈中增長一層自定義的push,pop,splice方法作攔截,這些方法裏面加上咱們本身的回調函數,而後在調用原生的push,pop,splice等方法.
具體能夠看我上一篇文章js對象監聽實現
observer.js代碼
export function Observer(obj) { this.$observe = function(_obj) { var type = Object.prototype.toString.call(_obj); if (type == '[object Object]') { this.$observeObj(_obj); } else if (type == '[object Array]') { this.$cloneArray(_obj); } }; this.$observeObj = function(obj) { var t = this; Object.keys(obj).forEach(function(prop) { var val = obj[prop]; defineProperty(obj, prop, val); if (prop != '__observe__') { t.$observe(val); } }); }; this.$cloneArray = function(a_array) { var ORP = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; var arrayProto = Array.prototype; var newProto = Object.create(arrayProto); ORP.forEach(function(prop) { Object.defineProperty(newProto, prop, { value: function(newVal) { var dep = a_array.__observe__; var re=arrayProto[prop].apply(a_array, arguments); dep.notify(); return re; }, enumerable: false, configurable: true, writable: true }); }); a_array.__proto__ = newProto; }; this.$observe(obj, []); } var addObserve = function(val) { if (!val || typeof val != 'object') { return; } var dep = new Dep(); if (isArray(val)) { val.__observe__ = dep; return dep; } } export function defineProperty(obj, prop, val) { if (prop == '__observe__') { return; } val = val || obj[prop]; var dep = new Dep(); obj.__observe__ = dep; var childDep = addObserve(val); Object.defineProperty(obj, prop, { get: function() { var target = Dep.target; if (target) { dep.addSub(target); if (childDep) { childDep.addSub(target); } } return val; }, set: function(newVal) { if(newVal!=val){ val = newVal; dep.notify(); } } }); }
實現思路:
1.將模版template上的dom遍歷一遍,將其存入文檔碎片frag
2.遍歷frag,經過attributes獲取節點的屬性信息,在經過正則表達式過濾屬性信息,進而拿到元素節點和文檔節點的指令信息
var complieTemplate = function (nodes, model) { if ((nodes.nodeType == 1 || nodes.nodeType == 11) && !isScript(nodes)) { paserNode(model, nodes); if (nodes.hasChildNodes()) { nodes.childNodes.forEach(node=> { complieTemplate(node, model); }) } } }; var paserNode = function (model, node) { var attributes = node.attributes || []; var direct_array = []; var scope = { parentNode: node.parentNode, nextNode: node.nextElementSibling, el: node, model: model, direct_array: direct_array }; attributes = toArray(attributes); var textContent = node.textContent; var attrs = []; var vfor; attributes.forEach(attr => { var name = attr.name; if (isDirective(name)) { if (name == 'v-for') { vfor = attr; } else { attrs.push(attr); } removeAttribute(node, name); } }); //bug nodeType=3 var textValue = stringParse(textContent); if (textValue) { attrs.push({ name: 'v-text', value: textValue }); node.textContent = ''; } if (vfor) { scope.attrs = attrs; attrs = [vfor]; } attrs.forEach(function (attr) { var name = attr.name; var val = attr.value; var directiveType = 'v' + /v-(\w+)/.exec(name)[1]; var Directive = directives[directiveType]; if (Directive) { direct_array.push(new Directive(val, scope)); } }); }; var isDirective = function (attr) { return /v-(\w+)/.test(attr) }; var isScript = function isScript(el) { return el.tagName === 'SCRIPT' && ( !el.hasAttribute('type') || el.getAttribute('type') === 'text/javascript' ) }
指令信息如:v-text,v-for,v-model等。
每種指令信息須要的初始化動做以及指令的刷新函數update均可能不同,因此咱們把它抽象出來單獨作一個模塊。固然也有公用的如公共屬性,統一的watcher實例化,unbind.
update函數則具體定義所屬指令如何渲染ui
如簡單的vtext指令的update函數以下:
vt.update = function (textContent) { this.el.textContent = textContent; };
watcher的功能是讓directive和observer模塊關聯起來。
初始化的時候作兩件事:
將directive模塊的update函數當參數傳入,並將其存入自身update屬性中
調用getValue,從而獲取對象data的特定屬性值,進而觸發一次以前在observer定義的屬性函數的getter方法。
因爲在defineProperty函數中定義的dep變量在setter和getter函數裏有引用,使dep變量處於閉包狀態沒有釋放,此時在getter方法中經過判斷Depend.target的存在,來獲取訂閱者watcher,經過發佈者dep儲存起來。
數據的每一個屬性都有一個惟一的的dep變量,記錄着全部訂閱者watcher的信息,一旦屬性有變化,調用setter函數的時候觸發dep.notify(),通知全部已訂閱的watcher,進而執行全部與該屬性關聯的刷新函數,最後更新指定的ui。
watcher 初始化部分代碼:
Depend.target = this; this.value = this.getValue(); Depend.target = null;
observer.js 屬性定義代碼:
export function defineProperty(obj, prop, val) { if (prop == '__observe__') { return; } val = val || obj[prop]; var dep = new Dep(); obj.__observe__ = dep; var childDep = addObserve(val); Object.defineProperty(obj, prop, { get: function() { var target = Dep.target; if (target) { dep.addSub(target); if (childDep) { childDep.addSub(target); } } return val; }, set: function(newVal) { if(newVal!=val){ val = newVal; dep.notify(); } } }); }
簡單的流程圖以下:
本文基本對mvvm庫的需求整理,拆分,以及對拆分模塊的逐一實現來達到總體雙向綁定功能的實現,固然目前市場上的mvvm庫功能毫不止於此,本文只是略舉我的認爲的核心代碼。若是思路和實現上的問題,也請各位斧正,謝謝閱讀!