在目前的前端面試中,vue的雙向數據綁定已經成爲了一個很是容易考到的點,即便不能當場寫出來,至少也要能說出原理。本篇文章中我將會仿照vue寫一個雙向數據綁定的實例,名字就叫myVue吧。結合註釋,但願能讓你們有所收穫。html
Vue的雙向數據綁定的原理相信你們也都十分了解了,主要是經過Object對象的defineProperty屬性,重寫data的set和get函數來實現的
,這裏對原理不作過多描述,主要仍是來實現一個實例。爲了使代碼更加的清晰,這裏只會實現最基本的內容,主要實現v-model,v-bind 和v-click三個命令,其餘命令也能夠自行補充。前端
添加網上的一張圖vue
頁面結構很簡單,以下node
1 <div id="app"> 2 <form> 3 <input type="text" v-model="number"> 4 <button type="button" v-click="increment">增長</button> 5 </form> 6 <h3 v-bind="number"></h3> 7 </div>
包含:面試
1. 一個input,使用v-model指令
2. 一個button,使用v-click指令
3. 一個h3,使用v-bind指令。
咱們最後會經過相似於vue的方式來使用咱們的雙向數據綁定,結合咱們的數據結構添加註釋數據結構
1 var app = new myVue({ 2 el:'#app', 3 data: { 4 number: 0 5 }, 6 methods: { 7 increment: function() { 8 this.number ++; 9 }, 10 } 11 })
首先咱們須要定義一個myVue構造函數:app
1 function myVue(options) { 2 3 }
爲了初始化這個構造函數,給它添加一 個_init屬性函數
1 function myVue(options) { 2 this._init(options); 3 } 4 myVue.prototype._init = function (options) { 5 this.$options = options; // options 爲上面使用時傳入的結構體,包括el,data,methods 6 this.$el = document.querySelector(options.el); // el是 #app, this.$el是id爲app的Element元素 7 this.$data = options.data; // this.$data = {number: 0} 8 this.$methods = options.methods; // this.$methods = {increment: function(){}} 9 }
接下來實現_obverse函數,對data進行處理,重寫data的set和get函數post
並改造_init函數this
1 myVue.prototype._obverse = function (obj) { // obj = {number: 0} 2 var value; 3 for (key in obj) { //遍歷obj對象 4 if (obj.hasOwnProperty(key)) { 5 value = obj[key]; 6 if (typeof value === 'object') { //若是值仍是對象,則遍歷處理 7 this._obverse(value); 8 } 9 Object.defineProperty(this.$data, key, { //關鍵 10 enumerable: true, 11 configurable: true, 12 get: function () { 13 console.log(`獲取${value}`); 14 return value; 15 }, 16 set: function (newVal) { 17 console.log(`更新${newVal}`); 18 if (value !== newVal) { 19 value = newVal; 20 } 21 } 22 }) 23 } 24 } 25 } 26 27 myVue.prototype._init = function (options) { 28 this.$options = options; 29 this.$el = document.querySelector(options.el); 30 this.$data = options.data; 31 this.$methods = options.methods; 32 33 this._obverse(this.$data); 34 }
接下來咱們寫一個指令類Watcher,用來綁定更新函數,實現對DOM元素的更新
1 function Watcher(name, el, vm, exp, attr) { 2 this.name = name; //指令名稱,例如文本節點,該值設爲"text" 3 this.el = el; //指令對應的DOM元素 4 this.vm = vm; //指令所屬myVue實例 5 this.exp = exp; //指令對應的值,本例如"number" 6 this.attr = attr; //綁定的屬性值,本例爲"innerHTML" 7 8 this.update(); 9 } 10 11 Watcher.prototype.update = function () { 12 this.el[this.attr] = this.vm.$data[this.exp]; //好比 H3.innerHTML = this.data.number; 當number改變時,會觸發這個update函數,保證對應的DOM內容進行了更新。 13 }
更新_init函數以及_obverse函數
1 myVue.prototype._init = function (options) { 2 //... 3 this._binding = {}; //_binding保存着model與view的映射關係,也就是咱們前面定義的Watcher的實例。當model改變時,咱們會觸發其中的指令類更新,保證view也能實時更新 4 //... 5 } 6 7 myVue.prototype._obverse = function (obj) { 8 //... 9 if (obj.hasOwnProperty(key)) { 10 this._binding[key] = { // 按照前面的數據,_binding = {number: _directives: []} 11 _directives: [] 12 }; 13 //... 14 var binding = this._binding[key]; 15 Object.defineProperty(this.$data, key, { 16 //... 17 set: function (newVal) { 18 console.log(`更新${newVal}`); 19 if (value !== newVal) { 20 value = newVal; 21 binding._directives.forEach(function (item) { // 當number改變時,觸發_binding[number]._directives 中的綁定的Watcher類的更新 22 item.update(); 23 }) 24 } 25 } 26 }) 27 } 28 } 29 }
那麼如何將view與model進行綁定呢?接下來咱們定義一個_compile函數,用來解析咱們的指令(v-bind,v-model,v-clickde)等,並在這個過程當中對view與model進行綁定。
1 myVue.prototype._init = function (options) { 2 //... 3 this._complie(this.$el); 4 } 5 6 myVue.prototype._complie = function (root) { root 爲 id爲app的Element元素,也就是咱們的根元素 7 var _this = this; 8 var nodes = root.children; 9 for (var i = 0; i < nodes.length; i++) { 10 var node = nodes[i]; 11 if (node.children.length) { // 對全部元素進行遍歷,並進行處理 12 this._complie(node); 13 } 14 15 if (node.hasAttribute('v-click')) { // 若是有v-click屬性,咱們監聽它的onclick事件,觸發increment事件,即number++ 16 node.onclick = (function () { 17 var attrVal = nodes[i].getAttribute('v-click'); 18 return _this.$methods[attrVal].bind(_this.$data); //bind是使data的做用域與method函數的做用域保持一致 19 })(); 20 } 21 22 if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // 若是有v-model屬性,而且元素是INPUT或者TEXTAREA,咱們監聽它的input事件 23 node.addEventListener('input', (function(key) { 24 var attrVal = node.getAttribute('v-model'); 25 //_this._binding['number']._directives = [一個Watcher實例] 26 // 其中Watcher.prototype.update = function () { 27 // node['vaule'] = _this.$data['number']; 這就將node的值保持與number一致 28 // } 29 _this._binding[attrVal]._directives.push(new Watcher( 30 'input', 31 node, 32 _this, 33 attrVal, 34 'value' 35 )) 36 37 return function() { 38 _this.$data[attrVal] = nodes[key].value; // 使number 的值與 node的value保持一致,已經實現了雙向綁定 39 } 40 })(i)); 41 } 42 43 if (node.hasAttribute('v-bind')) { // 若是有v-bind屬性,咱們只要使node的值及時更新爲data中number的值便可 44 var attrVal = node.getAttribute('v-bind'); 45 _this._binding[attrVal]._directives.push(new Watcher( 46 'text', 47 node, 48 _this, 49 attrVal, 50 'innerHTML' 51 )) 52 } 53 } 54 }
至此,咱們已經實現了一個簡單vue的雙向綁定功能,包括v-bind, v-model, v-click三個指令。效果以下圖
附上所有代碼,不到150行
1 <!DOCTYPE html> 2 <head> 3 <title>myVue</title> 4 </head> 5 <style> 6 #app { 7 text-align: center; 8 } 9 </style> 10 <body> 11 <div id="app"> 12 <form> 13 <input type="text" v-model="number"> 14 <button type="button" v-click="increment">增長</button> 15 </form> 16 <h3 v-bind="number"></h3> 17 <form> 18 <input type="text" v-model="count"> 19 <button type="button" v-click="incre">增長</button> 20 </form> 21 <h3 v-bind="count"></h3> 22 </div> 23 </body> 24 25 <script> 26 function myVue(options) { 27 this._init(options); 28 } 29 30 myVue.prototype._init = function (options) { 31 this.$options = options; 32 this.$el = document.querySelector(options.el); 33 this.$data = options.data; 34 this.$methods = options.methods; 35 36 this._binding = {}; 37 this._obverse(this.$data); 38 this._complie(this.$el); 39 } 40 41 myVue.prototype._obverse = function (obj) { 42 var _this = this; 43 Object.keys(obj).forEach(function (key) { 44 if (obj.hasOwnProperty(key)) { 45 _this._binding[key] = { 46 _directives: [] 47 }; 48 console.log(_this._binding[key]) 49 var value = obj[key]; 50 if (typeof value === 'object') { 51 _this._obverse(value); 52 } 53 var binding = _this._binding[key]; 54 Object.defineProperty(_this.$data, key, { 55 enumerable: true, 56 configurable: true, 57 get: function () { 58 console.log(`${key}獲取${value}`); 59 return value; 60 }, 61 set: function (newVal) { 62 console.log(`${key}更新${newVal}`); 63 if (value !== newVal) { 64 value = newVal; 65 binding._directives.forEach(function (item) { 66 item.update(); 67 }) 68 } 69 } 70 }) 71 } 72 }) 73 } 74 75 myVue.prototype._complie = function (root) { 76 var _this = this; 77 var nodes = root.children; 78 for (var i = 0; i < nodes.length; i++) { 79 var node = nodes[i]; 80 if (node.children.length) { 81 this._complie(node); 82 } 83 84 if (node.hasAttribute('v-click')) { 85 node.onclick = (function () { 86 var attrVal = nodes[i].getAttribute('v-click'); 87 return _this.$methods[attrVal].bind(_this.$data); 88 })(); 89 } 90 91 if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) { 92 node.addEventListener('input', (function(key) { 93 var attrVal = node.getAttribute('v-model'); 94 _this._binding[attrVal]._directives.push(new Watcher( 95 'input', 96 node, 97 _this, 98 attrVal, 99 'value' 100 )) 101 102 return function() { 103 _this.$data[attrVal] = nodes[key].value; 104 } 105 })(i)); 106 } 107 108 if (node.hasAttribute('v-bind')) { 109 var attrVal = node.getAttribute('v-bind'); 110 _this._binding[attrVal]._directives.push(new Watcher( 111 'text', 112 node, 113 _this, 114 attrVal, 115 'innerHTML' 116 )) 117 } 118 } 119 } 120 121 function Watcher(name, el, vm, exp, attr) { 122 this.name = name; //指令名稱,例如文本節點,該值設爲"text" 123 this.el = el; //指令對應的DOM元素 124 this.vm = vm; //指令所屬myVue實例 125 this.exp = exp; //指令對應的值,本例如"number" 126 this.attr = attr; //綁定的屬性值,本例爲"innerHTML" 127 128 this.update(); 129 } 130 131 Watcher.prototype.update = function () { 132 this.el[this.attr] = this.vm.$data[this.exp]; 133 } 134 135 window.onload = function() { 136 var app = new myVue({ 137 el:'#app', 138 data: { 139 number: 0, 140 count: 0, 141 }, 142 methods: { 143 increment: function() { 144 this.number ++; 145 }, 146 incre: function() { 147 this.count ++; 148 } 149 } 150 }) 151 } 152 </script>
附上原文地址