首先,什么是雙向數據綁定?Vue是三大MVVM框架之一,數據綁定簡單來說,就是當數據發生變化時,相應的視圖會進行更新,當視圖更新時,數據也會跟着變化。
在分析其原理和代碼的時候,大家首先了解如下幾個js函數的作用:
1. [].slice.call(lis): 將偽數組轉換為真數組
2. node.nodeType: 得到節點類型
3. Object.defineProperty(obj, propertyName, {}): 給對象添加/修改屬性(指定描述符)
configurable: true/false 是否可以重新define
enumerable: true/false 是否可以枚舉(for..in / keys())
value: 指定初始值
writable: true/false value是否可以修改存取(訪問)描述符
get: 函數, 用來得到當前屬性值
set: 函數, 用來監視當前屬性值的變化
4. Object.keys(obj): 得到對象自身可枚舉的屬性名的數組
5. DocumentFragment: 文檔碎片(高效批量更新多個節點)
6. obj.hasOwnProperty(prop): 判斷prop是否是obj自身的屬性
如果想了解這些函數具體使用:請點擊這里
首先,我來看一下如何實現最基礎的數據綁定:

<body> <div>請輸入:<input type="text" id="inputId"/></div> <div>輸入的值為:<span id="showId"></span></div> </body> <script> var inputValue = document.getElementById('inputId'); var showValue = document.getElementById('showId'); var obj = {}; Object.defineProperty(obj, 'msg', { enumerable: true, configurable: true, set (newVal) { showValue.innerHTML = newVal; } }) inputValue.addEventListener('input', function(e) { obj.msg = e.target.value; }) </script>
對於vue來說,Vue.js則是通過數據劫持以及結合發布者-訂閱者來實現的數據綁定,數據劫持是利用ES5的Object.defineProperty(obj, key, val)來劫持各個屬性的的setter以及getter,在數據變動時發布消息給訂閱者,從而觸發相應的回調來更新視圖。
我們來看一下數據雙向綁定的流程圖:
1、實現一個數據監聽器Obverser,對data中的數據進行監聽,若有變化,通知相應的訂閱者。
2、實現一個指令解析器Compile,對於每個元素上的指令進行解析,根據指令替換數據,更新視圖。
3、實現一個Watcher,用來連接Obverser和Compile, 並為每個屬性綁定相應的訂閱者,當數據發生變化時,執行相應的回調函數,從而更新視圖。
4、構造函數 (new MVue({}))
我們來看一下對應的js代碼:
一、Obverser.js

function Observer(data) { // 保存data對象 this.data = data; // 走起 this.walk(data); } Observer.prototype = { walk: function(data) { var me = this; // 遍歷data中所有屬性 Object.keys(data).forEach(function(key) { // 針對指定屬性進行處理 me.convert(key, data[key]); }); }, convert: function(key, val) { // 對指定屬性實現響應式數據綁定 this.defineReactive(this.data, key, val); }, defineReactive: function(data, key, val) { // 創建與當前屬性對應的dep對象 var dep = new Dep(); // 間接遞歸調用實現對data中所有層次屬性的劫持 var childObj = observe(val); // 給data重新定義屬性(添加set/get) Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: false, // 不能再define get: function() { // 建立dep與watcher的關系 if (Dep.target) { dep.depend(); } // 返回屬性值 return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; // 新的值是object的話,進行監聽 childObj = observe(newVal); // 通過dep dep.notify(); } }); } }; function observe(value, vm) { // value必須是對象, 因為監視的是對象內部的屬性 if (!value || typeof value !== 'object') { return; } // 創建一個對應的觀察都對象 return new Observer(value); }; var uid = 0; function Dep() { // 標識屬性 this.id = uid++; // 相關的所有watcher的數組 this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, depend: function() { Dep.target.addDep(this); }, removeSub: function(sub) { var index = this.subs.indexOf(sub); if (index != -1) { this.subs.splice(index, 1); } }, notify: function() { // 通知所有相關的watcher(一個訂閱者) this.subs.forEach(function(sub) { sub.update(); }); } }; Dep.target = null;
1). Observer
* 用來對data所有屬性數據進行劫持的構造函數
* 給data中所有屬性重新定義屬性描述(get/set)
* 為data中的每個屬性創建對應的dep對象
2). Dep(Depend)
* data中的每個屬性(所有層次)都對應一個dep對象
* 創建的時機:
* 在初始化define data中各個屬性時創建對應的dep對象
* 在data中的某個屬性值被設置為新的對象時
* 對象的結構
{
id, // 每個dep都有一個唯一的id
subs //包含n個對應watcher的數組(subscribes的簡寫)
}
* subs屬性說明
* 當一個watcher被創建時, 內部會將當前watcher對象添加到對應的dep對象的subs中
* 當此data屬性的值發生改變時, 所有subs中的watcher都會收到更新的通知, 從而最終更新對應的界面
二、模板解析(Compile.js)

function Compile(el, vm) { // 保存vm this.$vm = vm; // 保存el元素 this.$el = this.isElementNode(el) ? el : document.querySelector(el); // 如果el元素存在 if (this.$el) { // 1. 取出el中所有子節點, 封裝在一個framgment對象中 this.$fragment = this.node2Fragment(this.$el); // 2. 編譯fragment中所有層次子節點 this.init(); // 3. 將fragment添加到el中 this.$el.appendChild(this.$fragment); } } Compile.prototype = { node2Fragment: function (el) { var fragment = document.createDocumentFragment(), child; // 將原生節點拷貝到fragment while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }, init: function () { // 編譯fragment this.compileElement(this.$fragment); }, compileElement: function (el) { // 得到所有子節點 var childNodes = el.childNodes, // 保存compile對象 me = this; // 遍歷所有子節點 [].slice.call(childNodes).forEach(function (node) { // 得到節點的文本內容 var text = node.textContent; // 正則對象(匹配大括號表達式) var reg = /\{\{(.*)\}\}/; // {{name}} // 如果是元素節點 if (me.isElementNode(node)) { // 編譯元素節點的指令屬性 me.compile(node); // 如果是一個大括號表達式格式的文本節點 } else if (me.isTextNode(node) && reg.test(text)) { // 編譯大括號表達式格式的文本節點 me.compileText(node, RegExp.$1); // RegExp.$1: 表達式 name } // 如果子節點還有子節點 if (node.childNodes && node.childNodes.length) { // 遞歸調用實現所有層次節點的編譯 me.compileElement(node); } }); }, compile: function (node) { // 得到所有標簽屬性節點 var nodeAttrs = node.attributes, me = this; // 遍歷所有屬性 [].slice.call(nodeAttrs).forEach(function (attr) { // 得到屬性名: v-on:click var attrName = attr.name; // 判斷是否是指令屬性 if (me.isDirective(attrName)) { // 得到表達式(屬性值): test var exp = attr.value; // 得到指令名: on:click var dir = attrName.substring(2); // 事件指令 if (me.isEventDirective(dir)) { // 解析事件指令 compileUtil.eventHandler(node, me.$vm, exp, dir); // 普通指令 } else { // 解析普通指令 compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); } // 移除指令屬性 node.removeAttribute(attrName); } }); }, compileText: function (node, exp) { // 調用編譯工具對象解析 compileUtil.text(node, this.$vm, exp); }, isDirective: function (attr) { return attr.indexOf('v-') == 0; }, isEventDirective: function (dir) { return dir.indexOf('on') === 0; }, isElementNode: function (node) { return node.nodeType == 1; }, isTextNode: function (node) { return node.nodeType == 3; } }; // 指令處理集合 var compileUtil = { // 解析: v-text/{{}} text: function (node, vm, exp) { this.bind(node, vm, exp, 'text'); }, // 解析: v-html html: function (node, vm, exp) { this.bind(node, vm, exp, 'html'); }, // 解析: v-model model: function (node, vm, exp) { this.bind(node, vm, exp, 'model'); var me = this, val = this._getVMVal(vm, exp); node.addEventListener('input', function (e) { var newValue = e.target.value; if (val === newValue) { return; } me._setVMVal(vm, exp, newValue); val = newValue; }); }, // 解析: v-class class: function (node, vm, exp) { this.bind(node, vm, exp, 'class'); }, // 真正用於解析指令的方法 bind: function (node, vm, exp, dir) { /*實現初始化顯示*/ // 根據指令名(text)得到對應的更新節點函數 var updaterFn = updater[dir + 'Updater']; // 如果存在調用來更新節點 updaterFn && updaterFn(node, this._getVMVal(vm, exp)); // 創建表達式對應的watcher對象 new Watcher(vm, exp, function (value, oldValue) {/*更新界面*/ // 當對應的屬性值發生了變化時, 自動調用, 更新對應的節點 updaterFn && updaterFn(node, value, oldValue); }); }, // 事件處理 eventHandler: function (node, vm, exp, dir) { // 得到事件名/類型: click var eventType = dir.split(':')[1], // 根據表達式得到事件處理函數(從methods中): test(){} fn = vm.$options.methods && vm.$options.methods[exp]; // 如果都存在 if (eventType && fn) { // 綁定指定事件名和回調函數的DOM事件監聽, 將回調函數中的this強制綁定為vm node.addEventListener(eventType, fn.bind(vm), false); } }, // 得到表達式對應的value _getVMVal: function (vm, exp) { var val = vm._data; exp = exp.split('.'); exp.forEach(function (k) { val = val[k]; }); return val; }, _setVMVal: function (vm, exp, value) { var val = vm._data; exp = exp.split('.'); exp.forEach(function (k, i) { // 非最后一個key,更新val的值 if (i < exp.length - 1) { val = val[k]; } else { val[k] = value; } }); } }; // 包含多個用於更新節點方法的對象 var updater = { // 更新節點的textContent textUpdater: function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }, // 更新節點的innerHTML htmlUpdater: function (node, value) { node.innerHTML = typeof value == 'undefined' ? '' : value; }, // 更新節點的className classUpdater: function (node, value, oldValue) { var className = node.className; className = className.replace(oldValue, '').replace(/\s$/, ''); var space = className && String(value) ? ' ' : ''; node.className = className + space + value; }, // 更新節點的value modelUpdater: function (node, value, oldValue) { node.value = typeof value == 'undefined' ? '' : value; } };
1.模板解析的關鍵對象: compile對象
2.模板解析的基本流程:
1). 將el的所有子節點取出, 添加到一個新建的文檔fragment對象中
2). 對fragment中的所有層次子節點遞歸進行編譯解析處理
* 對表達式文本節點進行解析
* 對元素節點的指令屬性進行解析
* 事件指令解析
* 一般指令解析
3). 將解析后的fragment添加到el中顯示
3.解析表達式文本節點: textNode.textContent = value
1). 根據正則對象得到匹配出的表達式字符串: 子匹配/RegExp.$1
2). 從data中取出表達式對應的屬性值
3). 將屬性值設置為文本節點的textContent
4.事件指令解析: elementNode.addEventListener(事件名, 回調函數.bind(vm))
v-on:click="test"
1). 從指令名中取出事件名
2). 根據指令的值(表達式)從methods中得到對應的事件處理函數對象
3). 給當前元素節點綁定指定事件名和回調函數的dom事件監聽
4). 指令解析完后, 移除此指令屬性
5.一般指令解析: elementNode.xxx = value
1). 得到指令名和指令值(表達式)
2). 從data中根據表達式得到對應的值
3). 根據指令名確定需要操作元素節點的什么屬性
* v-text---textContent屬性
* v-html---innerHTML屬性
* v-class--className屬性
4). 將得到的表達式的值設置到對應的屬性上
5). 移除元素的指令屬性
所以Compile可以歸納為幾點:
* 用來解析模板頁面的對象的構造函數(一個實例)
* 利用compile對象解析模板頁面
* 每解析一個表達式(非事件指令)都會創建一個對應的watcher對象, 並建立watcher與dep的關系
* complie與watcher關系: 一對多的關系
三、Watcher.js

function Watcher(vm, exp, cb) { this.cb = cb; // callback this.vm = vm; this.exp = exp; this.depIds = {}; // {0: d0, 1: d1, 2: d2} this.value = this.get(); } Watcher.prototype = { update: function () { this.run(); }, run: function () { // 得到最新的值 var value = this.get(); // 得到舊值 var oldVal = this.value; // 如果不相同 if (value !== oldVal) { this.value = value; // 調用回調函數更新對應的界面 this.cb.call(this.vm, value, oldVal); } }, addDep: function (dep) { if (!this.depIds.hasOwnProperty(dep.id)) { // 建立dep到watcher dep.addSub(this); // 建立watcher到dep的關系 this.depIds[dep.id] = dep; } }, get: function () { Dep.target = this; // 獲取當前表達式的值, 內部會導致屬性的get()調用 var value = this.getVMVal(); Dep.target = null; return value; }, getVMVal: function () { var exp = this.exp.split('.'); var val = this.vm._data; exp.forEach(function (k) { val = val[k]; }); return val; } }; /* const obj1 = {id: 1} const obj12 = {id: 2} const obj13 = {id: 3} const obj14 = {id: 4} const obj2 = {} const obj22 = {} const obj23 = {} // 雙向1對1 // obj1.o2 = obj2 // obj2.o1 = obj1 // obj1: 1:n obj1.o2s = [obj2, obj22, obj23] // obj2: 1:n obj2.o1s = { 1: obj1, 2: obj12, 3: obj13 } */
* 模板中每個非事件指令或表達式都對應一個watcher對象
* 監視當前表達式數據的變化
* 創建的時機: 在初始化編譯模板時
* 對象的組成
{
vm, //vm對象
exp, //對應指令的表達式
cb, //當表達式所對應的數據發生改變的回調函數
value, //表達式當前的值
depIds //表達式中各級屬性所對應的dep對象的集合對象
//屬性名為dep的id, 屬性值為dep
}
四、數據代理(MVVM.js)

/* 相關於Vue的構造函數 */ function MVVM(options) { // 將選項對象保存到vm this.$options = options; // 將data對象保存到vm和datq變量中 var data = this._data = this.$options.data; //將vm保存在me變量中 var me = this; // 遍歷data中所有屬性 Object.keys(data).forEach(function (key) { // 屬性名: name // 對指定屬性實現代理 me._proxy(key); }); // 對data進行監視 observe(data, this); // 創建一個用來編譯模板的compile對象 this.$compile = new Compile(options.el || document.body, this) } MVVM.prototype = { $watch: function (key, cb, options) { new Watcher(this, key, cb); }, // 對指定屬性實現代理 _proxy: function (key) { // 保存vm var me = this; // 給vm添加指定屬性名的屬性(使用屬性描述) Object.defineProperty(me, key, { configurable: false, // 不能再重新定義 enumerable: true, // 可以枚舉 // 當通過vm.name讀取屬性值時自動調用 get: function proxyGetter() { // 讀取data中對應屬性值返回(實現代理讀操作) return me._data[key]; }, // 當通過vm.name = 'xxx'時自動調用 set: function proxySetter(newVal) { // 將最新的值保存到data中對應的屬性上(實現代理寫操作) me._data[key] = newVal; } }); } };
1.通過一個對象代理對另一個對象中屬性的操作(讀/寫)
2.通過vm對象來代理data對象中所有屬性的操作
3.好處: 更方便的操作data中的數據
4.基本實現流程
1). 通過Object.defineProperty()給vm添加與data對象的屬性對應的屬性描述符
2). 所有添加的屬性都包含getter/setter
3). 在getter/setter內部去操作data中對應的屬性數據
五、總結
1.dep與watcher的關系: 多對多
* 一個data中的屬性對應對應一個dep, 一個dep中可能包含多個watcher(模板中有幾個表達式使用到了屬性)
* 模板中一個非事件表達式對應一個watcher, 一個watcher中可能包含多個dep(表達式中包含了幾個data屬性)
* 數據綁定使用到2個核心技術
* defineProperty()
* 消息訂閱與發布
2.雙向數據綁定
1). 雙向數據綁定是建立在單向數據綁定(model==>View)的基礎之上的
2). 雙向數據綁定的實現流程:
* 在解析v-model指令時, 給當前元素添加input監聽
* 當input的value發生改變時, 將最新的值賦值給當前表達式所對應的data屬性