談起當前前端最熱門的 js 框架,必少不了 Vue、React、Angular,對於大多數人來講,咱們更多的是在使用框架,對於框架解決痛點背後使用的基本原理每每關注很少,近期在研讀 Vue.js 源碼,也在寫源碼解讀的系列文章。和多數源碼解讀的文章不一樣的是,我會嘗試從一個初級前端的角度入手,由淺入深去講解源碼實現思路和基本的語法知識,經過一些基礎事例一步步去實現一些小功能。javascript
本場 Chat 是系列 Chat 的開篇,我會首先講解一下數據雙向綁定的基本原理,介紹對比一下三大框架的不一樣實現方式,同時會一步步完成一個簡單的mvvm示例。讀源碼不是目的,只是一種學習的方式,目的是在讀源碼的過程當中提高本身,學習基本原理,拓展編碼的思惟方式。html
對於頁面渲染,通常分爲服務器端渲染和瀏覽器端渲染。通常來講服務器端吐html頁面的方式渲染速度更快、更利於SEO,可是瀏覽器端渲染更利於提升開發效率和減小維護成本,是一種相關舒服的先後端協做模式,後端提供接口,前端作視圖和交互邏輯。前端經過Ajax請求數據而後拼接html字符串或者使用js模板引擎、數據驅動的框架如Vue進行頁面渲染。前端
在ES6和Vue這類框架出現之前,前端綁定數據的方式是動態拼接html字符串和js模板引擎。模板引擎起到數據和視圖分離的做用,模板對應視圖,關注如何展現數據,在模板外頭準備的數據, 關注那些數據能夠被展現。模板引擎的工做原理能夠簡單地分紅兩個步驟:模板解析 / 編譯(Parse / Compile)和數據渲染(Render)兩部分組成,當今主流的前端模板有三種方式:vue
基於字符串的模板引擎,本質上依然是字符串拼接的形式,只是通常的庫作了封裝和優化,提供了更多方便的語法簡化了咱們的工做。基本原理以下:html5
典型的庫:java
以前的一篇文章中我介紹了js模板引擎的實現思路,感興趣的朋友能夠看看這裏:JavaScript進階學習(一)—— 基於正則表達式的簡單js模板引擎實現。這篇文章中咱們利用正則表達式實現了一個簡單的js模板引擎,利用正則匹配查找出模板中{{}}
之間的內容,而後替換爲模型中的數據,從而實現視圖的渲染。node
var template = function(tpl, data) { var re = /{{(.+?)}}/g, cursor = 0, reExp = /(^( )?(var|if|for|else|switch|case|break|{|}|;))(.*)?/g, code = 'var r=[];\n'; // 解析html function parsehtml(line) { // 單雙引號轉義,換行符替換爲空格,去掉先後的空格 line = line.replace(/('|")/g, '\\$1').replace(/\n/g, ' ').replace(/(^\s+)|(\s+$)/g,""); code +='r.push("' + line + '");\n'; } // 解析js代碼 function parsejs(line) { // 去掉先後的空格 line = line.replace(/(^\s+)|(\s+$)/g,""); code += line.match(reExp)? line + '\n' : 'r.push(' + 'this.' + line + ');\n'; } // 編譯模板 while((match = re.exec(tpl))!== null) { // 開始標籤 {{ 前的內容和結束標籤 }} 後的內容 parsehtml(tpl.slice(cursor, match.index)); // 開始標籤 {{ 和 結束標籤 }} 之間的內容 parsejs(match[1]); // 每一次匹配完成移動指針 cursor = match.index + match[0].length; } // 最後一次匹配完的內容 parsehtml(tpl.substr(cursor, tpl.length - cursor)); code += 'return r.join("");'; return new Function(code.replace(/[\r\t\n]/g, '')).apply(data); }
源代碼:http://jsfiddle.net/zhaomengh...react
如今ES6支持了模板字符串,咱們能夠用比較簡單的代碼就能夠實現相似的功能:android
const template = data => ` <p>name: ${data.name}</p> <p>age: ${data.profile.age}</p> <ul> ${data.skills.map(skill => ` <li>${skill}</li> `).join('')} </ul>` const data = { name: 'zhaomenghuan', profile: { age: 24 }, skills: ['html5', 'javascript', 'android'] } document.body.innerHTML = template(data)
Dom-based templating 則是從DOM的角度去實現數據的渲染,咱們經過遍歷DOM樹,提取屬性與DOM內容,而後將數據寫入到DOM樹中,從而實現頁面渲染。一個簡單的例子以下:git
function MVVM(opt) { this.dom = document.querySelector(opt.el); this.data = opt.data || {}; this.renderDom(this.dom); } MVVM.prototype = { init: { sTag: '{{', eTag: '}}' }, render: function (node) { var self = this; var sTag = self.init.sTag; var eTag = self.init.eTag; var matchs = node.textContent.split(sTag); if (matchs.length){ var ret = ''; for (var i = 0; i < matchs.length; i++) { var match = matchs[i].split(eTag); if (match.length == 1) { ret += matchs[i]; } else { ret = self.data[match[0]]; } node.textContent = ret; } } }, renderDom: function(dom) { var self = this; var attrs = dom.attributes; var nodes = dom.childNodes; Array.prototype.forEach.call(attrs, function(item) { self.render(item); }); Array.prototype.forEach.call(nodes, function(item) { if (item.nodeType === 1) { return self.renderDom(item); } self.render(item); }); } } var app = new MVVM({ el: '#app', data: { name: 'zhaomenghuan', age: '24', color: 'red' } });
源代碼:http://jsfiddle.net/zhaomengh...
頁面渲染的函數 renderDom 是直接遍歷DOM樹,而不是遍歷html字符串。遍歷DOM樹節點屬性(attributes)和子節點(childNodes),而後調用渲染函數render。當DOM樹子節點的類型是元素時,遞歸調用遍歷DOM樹的方法。根據DOM樹節點類型一直遍歷子節點,直到文本節點。
render的函數做用是提取{{}}
中的關鍵詞,而後使用數據模型中的數據進行替換。咱們經過textContent獲取Node節點的nodeValue,而後使用字符串的split方法對nodeValue進行分割,提取{{}}
中的關鍵詞而後替換爲數據模型中的值。
DOM 的相關基礎
注:元素類型對應NodeType
元素類型 | NodeType |
---|---|
元素 | 1 |
屬性 | 2 |
文本 | 3 |
註釋 | 8 |
文檔 | 9 |
childNodes 屬性返回包含被選節點的子節點的 NodeList。childNodes包含的不只僅只有html節點,全部屬性,文本、註釋等節點都包含在childNodes裏面。children只返回元素如input, span, script, div等,不會返回TextNode,註釋。
js模板引擎能夠認爲是一個基於MVC的結構,咱們經過創建模板做爲視圖,而後經過引擎函數做爲控制器實現數據和視圖的綁定,從而實現實現數據在頁面渲染,可是當數據模型發生變化時,視圖不能自動更新;當視圖數據發生變化時,模型數據不能實現更新,這個時候雙向數據綁定應運而生。檢測視圖數據更新實現數據綁定的方法有不少種,目前主要分爲三個流派,Angular使用的是髒檢查,只在特定的事件下才會觸發視圖刷新,Vue使用的是Getter/Setter機制,而React則是經過 Virtual DOM 算法檢查DOM的變更的刷新機制。
本文限於篇幅和內容在此只探討一下 Vue.js 數據綁定的實現,對於 angular 和 react 後續再作說明,讀者也能夠自行閱讀源碼。Vue 監聽數據變化的機制是把一個普通 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter。Vue 2.x 對 Virtual DOM 進行了支持,這部份內容後續咱們再作探討。
爲了更好的理解Vue中視圖和數據更新的機制,咱們先看一個簡單的例子:
var o = { a: 0 } Object.defineProperty(o, "b", { get: function () { return this.a + 1; }, set: function (value) { this.a = value / 2; } }); console.log(o.a); // "0" console.log(o.b); // "1" // 更新o.a o.a = 5; console.log(o.a); // "5" console.log(o.b); // "6" // 更新o.b o.b = 10; console.log(o.a); // "5" console.log(o.b); // "6"
這裏咱們能夠看出對象o的b屬性的值依賴於a屬性的值,同時b屬性值的變化又能夠改變a屬性的值,這個過程相關的屬性值的變化都會影響其餘相關的值進行更新。反過來咱們看看若是不使用Object.defineProperty()方法,上述的問題經過直接給對象屬性賦值的方法實現,代碼以下:
var o = { a: 0 } o.b = o.a + 1; console.log(o.a); // "0" console.log(o.b); // "1" // 更新o.a o.a = 5; o.b = o.a + 1; console.log(o.a); // "5" console.log(o.b); // "6" // 更新o.b o.b = 10; o.a = o.b / 2; o.b = o.a + 1; console.log(o.a); // "5" console.log(o.b); // "6"
很顯然使用Object.defineProperty()
方法能夠更方便的監聽一個對象的變化。當咱們的視圖和數據任何一方發生變化的時候,咱們但願可以通知對方也更新,這就是所謂的數據雙向綁定。既然明白這個道理咱們就能夠看看Vue源碼中相關的處理細節。
Object.defineProperty()方法能夠直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 並返回這個對象。
語法:Object.defineProperty(obj, prop, descriptor)
參數:
返回值:返回傳入函數的對象,即第一個參數obj
該方法重點是描述,對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。數據描述符是一個擁有可寫或不可寫值的屬性。存取描述符是由一對 getter-setter 函數功能來描述的屬性。描述符必須是兩種形式之一;不能同時是二者。
數據描述符和存取描述符均具備如下可選鍵值:
數據描述符同時具備如下可選鍵值:
存取描述符同時具備如下可選鍵值:
咱們能夠經過Object.defineProperty()方法精確添加或修改對象的屬性。好比,直接賦值建立的屬性默認狀況是能夠枚舉的,可是咱們能夠經過Object.defineProperty()方法設置enumerable屬性爲false爲不可枚舉。
var obj = { a: 0, b: 1 } for (var prop in obj) { console.log(`obj.${prop} = ${obj[prop]}`); } 結果: "obj.a = 0" "obj.b = 1"
咱們經過Object.defineProperty()修改以下:
var obj = { a: 0, b: 1 } Object.defineProperty(obj, 'b', { enumerable: false }) for (var prop in obj) { console.log(`obj.${prop} = ${obj[prop]}`); } 結果: "obj.a = 0"
這裏須要說明的是咱們使用Object.defineProperty()默認狀況下是enumerable屬性爲false,例如:
var obj = { a: 0 } Object.defineProperty(obj, 'b', { value: 1 }) for (var prop in obj) { console.log(`obj.${prop} = ${obj[prop]}`); } 結果: "obj.a = 0"
其餘描述屬性使用方法相似,不作贅述。Vue源碼core/util/lang.js
S中定義了這樣一個方法:
/** * Define a property. */ export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
Object.getOwnPropertyDescriptor() 返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不須要從原型鏈上進行查找的屬性)
語法:Object.getOwnPropertyDescriptor(obj, prop)
參數:
返回值:若是指定的屬性存在於對象上,則返回其屬性描述符(property descriptor),不然返回 undefined。能夠訪問「屬性描述符」內容,例如前面的例子:
var o = { a: 0 } Object.defineProperty(o, "b", { get: function () { return this.a + 1; }, set: function (value) { this.a = value / 2; } }); var des = Object.getOwnPropertyDescriptor(o,'b'); console.log(des); console.log(des.get);
本次咱們主要分析一下Vue 數據綁定的源碼,這裏我直接將 Vue.js 1.0.28 版本的代碼稍做刪減拿過來進行,2.x 的代碼基於 flow 靜態類型檢查器書寫的,代碼除了編碼風格在總體結構上基本沒有太大改動,因此依然基於 1.x 進行分析,對於存在差別的部分加以說明。
// 觀察者構造函數 function Observer (value) { this.value = value this.walk(value) } // 遞歸調用,爲對象綁定getter/setter Observer.prototype.walk = function (obj) { var keys = Object.keys(obj) for (var i = 0, l = keys.length; i < l; i++) { this.convert(keys[i], obj[keys[i]]) } } // 將屬性轉換爲getter/setter Observer.prototype.convert = function (key, val) { defineReactive(this.value, key, val) } // 建立數據觀察者實例 function observe (value) { // 當值不存在或者不是對象類型時,不須要繼續深刻監聽 if (!value || typeof value !== 'object') { return } return new Observer(value) } // 定義對象屬性的getter/setter function defineReactive (obj, key, val) { var property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 保存對象屬性預先定義的getter/setter var getter = property && property.get var setter = property && property.set var childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val console.log("訪問:"+key) return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val if (newVal === value) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } // 對新值進行監聽 childOb = observe(newVal) console.log('更新:' + key + ' = ' + newVal) } }) }
定義一個對象做爲數據模型,並監聽這個對象。
let data = { user: { name: 'zhaomenghuan', age: '24' }, address: { city: 'beijing' } } observe(data) console.log(data.user.name) // 訪問:user // 訪問:name data.user.name = 'ZHAO MENGHUAN' // 訪問:user // 更新:name = ZHAO MENGHUAN
效果以下:
上面咱們經過Object.defineProperty把對象的屬性所有轉爲 getter/setter 從而實現監聽對象的變更,可是對於數組對象沒法經過Object.defineProperty實現監聽。Vue 包含一組觀察數組的變異方法,因此它們也將會觸發視圖更新。
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) function def(obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) } // 數組的變異方法 ;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // 緩存數組原始方法 var original = arrayProto[method] def(arrayMethods, method, function mutator () { var i = arguments.length var args = new Array(i) while (i--) { args[i] = arguments[i] } console.log('數組變更') return original.apply(this, args) }) })
Vue.js 1.x 在Array.prototype原型對象上添加了$set
和 $remove
方法,在2.X後移除了,使用全局 API Vue.set
和 Vue.delete
代替了,後續咱們再分析。
定義一個數組做爲數據模型,並對這個數組調用變異的七個方法實現監聽。
let skills = ['JavaScript', 'Node.js', 'html5'] // 原型指針指向具備變異方法的數組對象 skills.__proto__ = arrayMethods skills.push('java') // 數組變更 skills.pop() // 數組變更
效果以下:
咱們將須要監聽的數組的原型指針指向咱們定義的數組對象,這樣咱們的數組在調用上面七個數組的變異方法時,可以監聽到變更從而實現對數組進行跟蹤。
對於__proto__
屬性,在ES2015中正式被加入到規範中,標準明確規定,只有瀏覽器必須部署這個屬性,其餘運行環境不必定須要部署,因此 Vue 是先進行了判斷,當__proto__
屬性存在時將原型指針__proto__
指向具備變異方法的數組對象,不存在時直接將具備變異方法掛在須要追蹤的對象上。
咱們能夠在上面Observer觀察者構造函數中添加對數組的監聽,源碼以下:
const hasProto = '__proto__' in {} const arrayKeys = Object.getOwnPropertyNames(arrayMethods) // 觀察者構造函數 function Observer (value) { this.value = value if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } // 觀察數組的每一項 Observer.prototype.observeArray = function (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]) } } // 將目標對象/數組的原型指針__proto__指向src function protoAugment (target, src) { target.__proto__ = src } // 將具備變異方法掛在須要追蹤的對象上 function copyAugment (target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i] def(target, key, src[key]) } }
原型鏈
對於不瞭解原型鏈的朋友能夠看一下我這裏畫的一個基本關係圖:
Object.create
Object.create 使用指定的原型對象和其屬性建立了一個新的對象。
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto)
這一步是經過 Object.create 建立了一個原型對象爲Array.prototype的空對象。而後經過Object.defineProperty方法對這個對象定義幾個變異的數組方法。有些新手可能會直接修改 Array.prototype 上的方法,這是很危險的行爲,這樣在引入的時候會全局影響Array 對象的方法,而使用Object.create實質上是徹底了一份拷貝,新生成的arrayMethods對象的原型指針__proto__
指向了Array.prototype,修改arrayMethods 對象不會影響Array.prototype。
基於這種原理,咱們一般會使用Object.create 實現類式繼承。
// 實現繼承 var extend = function(Child, Parent) { // 拷貝Parent原型對象 Child.prototype = Object.create(Parent.prototype); // 將Child構造函數賦值給Child的原型對象 Child.prototype.constructor = Child; } // 實例 var Parent = function () { this.name = 'Parent'; } Parent.prototype.getName = function () { return this.name; } var Child = function () { this.name = 'Child'; } extend(Child, Parent); var child = new Child(); console.log(child.getName())
在上面一部分咱們經過Object.defineProperty把對象的屬性所有轉爲 getter/setter 以及 數組變異方法實現了對數據模型變更的監聽,在數據變更的時候,咱們經過console.log打印出來提示了,可是對於框架而言,咱們相關的邏輯若是直接寫在那些地方,天然是不夠優雅和靈活的,這個時候就須要引入經常使用的設計模式去實現,vue.js採用了發佈-訂閱模式。發佈-訂閱模式主要是爲了達到一種「高內聚、低耦合"的效果。
Vue的Watcher訂閱者做爲Observer和Compile之間通訊的橋樑,可以訂閱並收到每一個屬性變更的通知,執行指令綁定的相應回調函數,從而更新視圖。
/** * 觀察者對象 */ function Watcher(vm, expOrFn, cb) { this.vm = vm this.cb = cb this.depIds = {} if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = this.parseExpression(expOrFn) } this.value = this.get() } /** * 收集依賴 */ Watcher.prototype.get = function () { // 當前訂閱者(Watcher)讀取被訂閱數據的最新更新後的值時,通知訂閱者管理員收集當前訂閱者 Dep.target = this // 觸發getter,將自身添加到dep中 const value = this.getter.call(this.vm, this.vm) // 依賴收集完成,置空,用於下一個Watcher使用 Dep.target = null return value } Watcher.prototype.addDep = function (dep) { if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this) this.depIds[dep.id] = dep } } /** * 依賴變更更新 * * @param {Boolean} shallow */ Watcher.prototype.update = function () { this.run() } Watcher.prototype.run = function () { var value = this.get() if (value !== this.value) { var oldValue = this.value this.value = value // 將newVal, oldVal掛載到MVVM實例上 this.cb.call(this.vm, value, oldValue) } } Watcher.prototype.parseExpression = function (exp) { if (/[^\w.$]/.test(exp)) { return } var exps = exp.split('.') return function(obj) { for (var i = 0, len = exps.length; i < len; i++) { if (!obj) return obj = obj[exps[i]] } return obj } }
Dep 是一個數據結構,其本質是維護了一個watcher隊列,負責添加watcher,更新watcher,移除watcher,通知watcher更新。
let uid = 0 function Dep() { this.id = uid++ this.subs = [] } Dep.target = null /** * 添加一個訂閱者 * * @param {Directive} sub */ Dep.prototype.addSub = function (sub) { this.subs.push(sub) } /** * 移除一個訂閱者 * * @param {Directive} sub */ Dep.prototype.removeSub = function (sub) { let index = this.subs.indexOf(sub); if (index !== -1) { this.subs.splice(index, 1); } } /** * 將自身做爲依賴添加到目標watcher */ Dep.prototype.depend = function () { Dep.target.addDep(this) } /** * 通知數據變動 */ Dep.prototype.notify = function () { var subs = toArray(this.subs) // stablize the subscriber list first for (var i = 0, l = subs.length; i < l; i++) { // 執行訂閱者的update更新函數 subs[i].update() } }
compile主要作的事情是解析模板指令,將模板中的變量替換成數據,而後初始化渲染頁面視圖,並將每一個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變更,收到通知,更新視圖。
function Compile(el, value) { this.$vm = value this.$el = this.isElementNode(el) ? el : document.querySelector(el) if (this.$el) { this.compileElement(this.$el) } } Compile.prototype.compileElement = function (el) { let self = this let childNodes = el.childNodes ;[].slice.call(childNodes).forEach(node => { let text = node.textContent let reg = /\{\{((?:.|\n)+?)\}\}/ // 處理element節點 if (self.isElementNode(node)) { self.compile(node) } else if (self.isTextNode(node) && reg.test(text)) { // 處理text節點 self.compileText(node, RegExp.$1.trim()) } // 解析子節點包含的指令 if (node.childNodes && node.childNodes.length) { self.compileElement(node) } }) } Compile.prototype.compile = function (node) { let nodeAttrs = node.attributes let self = this ;[].slice.call(nodeAttrs).forEach(attr => { var attrName = attr.name if (self.isDirective(attrName)) { let exp = attr.value let dir = attrName.substring(2) if (self.isEventDirective(dir)) { compileUtil.eventHandler(node, self.$vm, exp, dir) } else { compileUtil[dir] && compileUtil[dir](node, self.$vm, exp) } node.removeAttribute(attrName) } }); } Compile.prototype.compileText = function (node, exp) { compileUtil.text(node, this.$vm, exp); } Compile.prototype.isDirective = function (attr) { return attr.indexOf('v-') === 0 } Compile.prototype.isEventDirective = function (dir) { return dir.indexOf('on') === 0; } Compile.prototype.isElementNode = function (node) { return node.nodeType === 1 } Compile.prototype.isTextNode = function (node) { return node.nodeType === 3 } // 指令處理集合 var compileUtil = { text: function (node, vm, exp) { this.bind(node, vm, exp, 'text') }, html: function (node, vm, exp) { this.bind(node, vm, exp, 'html') }, model: function (node, vm, exp) { this.bind(node, vm, exp, 'model') let self = this, val = this._getVMVal(vm, exp) node.addEventListener('input', function (e) { var newValue = e.target.value if (val === newValue) { return } self._setVMVal(vm, exp, newValue) val = newValue }); }, bind: function (node, vm, exp, dir) { var updaterFn = updater[dir + 'Updater'] updaterFn && updaterFn(node, this._getVMVal(vm, exp)) new Watcher(vm, exp, function (value, oldValue) { updaterFn && updaterFn(node, value, oldValue) }) }, eventHandler: function (node, vm, exp, dir) { var eventType = dir.split(':')[1], fn = vm.$options.methods && vm.$options.methods[exp]; if (eventType && fn) { node.addEventListener(eventType, fn.bind(vm), false); } }, _getVMVal: function (vm, exp) { var val = vm exp = exp.split('.') exp.forEach(function (k) { val = val[k] }) return val }, _setVMVal: function (vm, exp, value) { var val = vm; exp = exp.split('.') exp.forEach(function (k, i) { // 非最後一個key,更新val的值 if (i < exp.length - 1) { val = val[k] } else { val[k] = value } }) } } var updater = { textUpdater: function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value }, htmlUpdater: function (node, value) { node.innerHTML = typeof value == 'undefined' ? '' : value }, modelUpdater: function (node, value, oldValue) { node.value = typeof value == 'undefined' ? '' : value } }
這種實現和咱們講到的Dom-based templating相似,只是更加完備,具備自定義指令的功能。在遍歷節點屬性和文本節點的時候,能夠編譯具有{{}}
表達式或v-xxx
的屬性值的節點,而且經過添加 new Watcher()
及綁定事件函數,監聽數據的變更從而對視圖實現雙向綁定。
在數據綁定初始化的時候,咱們須要經過new Observer()
來監聽數據模型變化,經過new Compile()
來解析編譯模板指令,並利用Watcher搭起Observer和Compile之間的通訊橋樑。
/** * @class 雙向綁定類 MVVM * @param {[type]} options [description] */ function MVVM(options) { this.$options = options || {} // 簡化了對data的處理 let data = this._data = this.$options.data // 監聽數據 observe(data) new Compile(options.el || document.body, this) } MVVM.prototype.$watch = function (expOrFn, cb) { new Watcher(this, expOrFn, cb) }
爲了可以直接經過實例化對象操做數據模型,咱們須要爲MVVM實例添加一個數據模型代理的方法:
MVVM.prototype._proxy = function (key) { Object.defineProperty(this, key, { configurable: true, enumerable: true, get: () => this._data[key], set: (val) => { this._data[key] = val } }) }
至此咱們能夠經過一個小例子來講明本文的內容:
<div id="app"> <h3>{{user.name}}</h3> <input type="text" v-model="modelValue"> <p>{{modelValue}}</p> </div> <script> let vm = new MVVM({ el: '#app', data: { modelValue: '', user: { name: 'zhaomenghuan', age: '24' }, address: { city: 'beijing' }, skills: ['JavaScript', 'Node.js', 'html5'] } }) vm.$watch('modelValue', val => console.log(`watch modelValue :${val}`)) </script>
本文目的不是爲了造一個輪子,而是在學習優秀框架實現的過程當中去提高本身,搞清楚框架發展的來龍去脈,由淺及深去學習基礎,本文參考了網上不少優秀博主的文章,因爲時間關係,有些內容沒有作深刻探討,以爲仍是有些遺憾,在後續的學習中會更多的獨立思考,提出更多本身的想法。
本文的完整代碼及圖片能夠在這裏下載:learn-javascript/mvvm
原文首發於 GitChat :http://gitbook.cn/books/593fa...,歡迎關注個人新話題:JavaScript 進階之 Vue.js + Node.js 入門實戰開發。
我在segmentfault上有兩期講座,歡迎來圍觀:
html5+ App開發工程化實踐之路
html5+ App開發之 Android 平臺離線集成 5+ SDK