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