什麼是雙向數據綁定?Vue是一個MVVM框架,數據綁定簡單來講,就是當數據發生變化時,相應的視圖會進行更新,當視圖更新時,數據也會跟着變化。html
實現數據綁定的方式大體有如下幾種:vue
- 一、發佈者-訂閱者模式(backbone.js) - 二、髒值檢查(angular.js) - 三、數據劫持(vue.js)
通常經過sub, pub的方式實現數據和視圖的綁定監聽,更新數據方式一般作法是 vm.set('property', value),有興趣可參考這裏node
咱們更但願能夠經過 vm.property = value 這種方式進行數據更新,同時自動更新視圖。
angular是經過髒值檢查方式來對比數據是否變化,來決定是否更新視圖,最多見的方式是經過setInterval()來監測數據變化,固然,只會在某些指定事件觸發時下才進行髒值檢查。大體以下:git
- DOM事件,譬如用戶輸入文本,點擊按鈕等。( ng-click ) - XHR響應事件 ( $http ) - 瀏覽器Location變動事件 ( $location ) - Timer事件( $timeout , $interval ) - 執行 $digest() 或 $apply()
Vue.js則是經過數據劫持以及結合發佈者-訂閱者來實現的,數據劫持是利用ES5的Object.defineProperty(obj, key, val)來劫持各個屬性的的setter以及getter,在數據變更時發佈消息給訂閱者,從而觸發相應的回調來更新視圖。github
<input type="text" id="in"/> 輸入的值爲:<span id="out"></span> <script> var int = document.getElementById('in'); var out = document.getElementById('out'); var obj = {}; Object.defineProperty(obj, 'msg', { enumerable: true, configurable: true, set (newVal) { out.innerHTML = newVal; } }) int.addEventListener('input', function(e) { obj.msg = e.target.value; }) </script>
上面的只是簡單的使用了Object.defineProperty(),並非咱們最終想要的效果,最終想要的效果以下:瀏覽器
<div id="app"> <input type="text" v-model="text"> 輸入的值爲:{{text}} <div> <input type="text" v-model="text"> </div> </div> <script> var vm = new MVue({ el: '#app', data: { text: 'hello world' } }) </script>
實現思路:
一、輸入框以及文本節點和data中的數據進行綁定
二、輸入框內容變化時,data中的對應數據同步變化,即 view => model
三、data中數據變化時,對應的文本節點內容同步變化 即 model => viewbash
上述流程如圖所示:app
一、實現一個數據監聽器Obverser,對data中的數據進行監聽,如有變化,通知相應的訂閱者。
二、實現一個指令解析器Compile,對於每一個元素上的指令進行解析,根據指令替換數據,更新視圖。
三、實現一個Watcher,用來鏈接Obverser和Compile, 併爲每一個屬性綁定相應的訂閱者,當數據發生變化時,執行相應的回調函數,從而更新視圖。
四、構造函數 (new MVue({}))框架
在初始化MVue實例時,對data中每一個屬性劫持監聽,同時進行模板編譯,指令解析,最後掛載到相應的DOM中。dom
function MVue (options) { this.$el = options.el; this.$data = options.data; // 初始化操做,後面會說 // ... }
一、實現 view => model
vue進行編譯時,將掛載目標的全部子節點劫持到DocumentFragment中,通過一份解析等處理後,再將DocumentFragment總體掛載到目標節點上。
function nodeToFragment (node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { compile(child, vm); if (child.firstChild) { var dom = nodeToFragment(child, vm); child.appendChild(dom); } flag.appendChild(child); } return flag; }
編譯過程圖
代碼以下:
function compile (node, vm) { let reg = /\{\{(.*)\}\}/; // 元素節點 if (node.nodeType === 1) { var attrs = node.attributes; for (let attr of attrs) { if (attr.nodeName === 'v-model') { // 獲取v-model指令綁定的data屬性 var name = attr.nodeValue; // 綁定事件 node.addEventListener('input', function(e) { vm.$data[name] = e.target.value; }) // 初始化數據綁定 node.value = vm.$data[name]; // 移除v-model 屬性 node.removeAttribute('v-model') } } } // 文本節點 if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1 && (RegExp.$1.trim()); // 綁定數據到文本節點中 node.nodeValue = node.nodeValue.replace(new RegExp('\\{\\{\\s*(' + name + ')\\s*\\}\\}'), vm.$data[name]); } } }
如今,咱們修改下MVue構造函數,增長模板編譯,以下:
function MVue (options) { this.$el = options.el; this.$data = options.data; // 模板編譯 let elem = document.querySelector(this.$el); elem.appendChild(nodeToFragment(elem, this)) }
那麼,咱們的view => model 已經實現了,包括初始化綁定默認值,只要修改了input中的值,data中對應的值相應變化,並觸發了setter, 更新屬性值等(能夠自行在set方法中打印看效果,或者在控制檯手動輸入vm.$data.text也會看到效果)。
二、實現 model => view
上面能夠看出,雖然咱們實現了初始化數據綁定,以及輸入框變化時,data中text也會變化,可是文本節點仍然沒有任何變化,那麼若是作到文本節點也同步變化呢,這裏用的是發佈者-訂閱者模式。
發佈者-訂閱者模式又稱爲觀察者模式,讓多個觀察者同時監聽某個主題對象,當主題對象發生變化時,會通知全部的觀察者對象,即:發佈者發出通知給主題對象 => 主題對象接收到通知後推送給全部訂閱者 => 訂閱者執行相應的操做。
1)首先,定義一個主題對象,用來收集全部的訂閱者,並提供notify方法,用來調用訂閱者的update方法,從而執行相應的操做。
function Dep () { this.subs = []; } Dep.prototype = { addSub (sub) { this.subs.push(sub); }, notify () { this.subs.forEach(sub => { // 執行訂閱者的update方法 sub.update(); }) } }
不難看出,當text屬性變化時,會觸發set方法,做爲發佈者,將數據更新消息經過主題對象發送給訂閱者, 那麼該如何通知呢?
咱們知道,在new一個vue時,會執行兩個操做,一個事編譯模板,一個監聽data數據,在監聽data時,vue爲data的每一個屬性都生成一個主題對象Dep,而在編譯模板時,會爲每一個與數據綁定的節點生成一個Watcher,那麼只要關聯了Dep與Watcher,是否是就實現了消息通知呢,關鍵邏輯是實現兩者關聯。
已實現:輸入框變化 => 觸發相應的事件,修改值 => 觸發set方法
須要實現:發出通知dep.notify() => 觸發訂閱者update方法 => 更新視圖
咱們修改下compile中文本節點內容(只修改部分)
// 文本節點 if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1 && (RegExp.$1.trim()); // 綁定數據到文本節點中 // node.nodeValue = node.nodeValue.replace(new RegExp('\\{\\{\\s*(' + name + ')\\s*\\}\\}'), vm.$data[name]); new Watcher(vm, node, name); } }
2)其次、實現訂閱者Watcher
function Watcher (vm, node, name) { // 全局的、惟一 Dep.target = this; this.node = node; this.name = name; this.vm = vm; this.index = index; this.update(); Dep.target = null; } Watcher.prototype = { update () { this.get(); this.node.nodeValue = this.value; }, get () { this.value = this.vm.$data[this.name] } }
首先,定義了一個全局的Dep.target,而後執行了update方法,進而執行了get方法,都去了this.vm的訪問器屬性, 從而將訂閱的消息保存在該屬性的主題對象中,並最終將Dep.target設置爲空,全局變量,是watcher和dep之間的惟一橋樑
,必須保證Dep.target只有一個值。
3)接着、實現一個obverser給data中每一個屬性添加一個主題對象
遍歷data中的全部屬性,包括子屬性對象的屬性
function obverser (obj) { Object.keys(obj).forEach(key => { if (obj.hasOwnProperty(key)) { if (obj[key].constructor === 'Object') { obverser(obj[key]) } defineReactive(obj, key); } }) }
使用Object.definePeoperty()來監聽屬性變更,給屬性添加setter和getter
function defineReactive (obj, key) { var _value= obj[key]; // new一個主題對象 var dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, set (newVal) { if (_value= newVal) { return; } _value= newVal; console.log(value) // 做爲發佈者發出通知給主題對象 dep.notify(); }, get () { // 若是訂閱者存在,添加到主題對象中 if (Dep.target) { dep.addSub(Dep.target); } return _value } }) }
最後,咱們須要再次修改構造函數MVue
function MVue (options) { this.$el = options.el; this.$data = options.data; // 數據監聽 obverser(this.$data); // 模板編譯 let elem = document.querySelector(this.$el); elem.appendChild(nodeToFragment(elem, this)) }
如今,已經實現了model => view的變化
當輸入框值變化時 => text也會變化 => 文本節點值變化
但若是細心的話,會發現還有一個問題,當咱們手動改變text的值時(如在控制檯上輸入vm.$data.text = 'xxx'),會發現,文本節點值已經變化了,可是輸入框的值沒有變化。
若是給輸入框也添加一個Watcher,是否是也就和文本節點同樣實現了呢,但須要注意的是,輸入框、文本框、下拉框等,是經過value改變值的,而不是nodeValuefa,由於能夠作以下修改:
compile中:
// 初始化數據綁定 // node.value = vm.$data[name]; new Watcher(vm, node, name); // 移除v-model 屬性 node.removeAttribute('v-model')
wather中:
Watcher.prototype = { update () { this.get(); let _name; if (this.index === 1) { _name = this.name; } else { _name = this.value; } if (this.node.nodeName === 'INPUT') { // 能夠添加TEXTAREA、SELECT等 this.node.value = this.value; } else { // this.node.nodeValue = this.value; this.node.nodeValue = this.node.nodeValue.replace(new RegExp('\\{?\\{?\\s*(' + _name + ')\\s*\\}?\\}?'), this.value); } ++this.index; }, get () { this.value = this.vm.$data[this.name] } }
OK,基本上完工。
獲取完整代碼,猛戳這裏
我的博客也能夠獲取完整代碼(https://jefferye.github.io)