雙向數據綁定的概念,相信你們都耳熟能詳,簡單來講,數據變化更新視圖,視圖變化更新數據。爲了實現這一效果,在 Vue 中,採用了 數據劫持結合發佈訂閱者模式
的方式來實現。html
經過 Object.defineProperty()
實現數據劫持,監聽數據的變化。node
經過 發佈者Dep()
訂閱者Watcher
實現發佈訂閱者模式,達到視圖與數據之間相互更新的解耦。瀏覽器
關於如何實現一個簡單的數據雙向綁定,網上有不少例子,我就不列舉了。我這裏將我理解的雙向綁定原理圖畫了一下:app
而後,我分模塊貼下代碼(代碼不是我寫的,我也是找了別人的學習)dom
function observer(data) { if(!data || typeof data !== 'object') { return; } Object.keys(data).forEach(key => { var dep = new Dep(); var value = data[key]; observer(value); Object.defineProperty(data, key, { configurable: true, enumerable: true, get() { if(Dep.target) { dep.addSub(Dep.target) } return value; }, set(newValue) { if (value === newValue) { return; } value = newValue; dep.notify(); }, }) }) }
function Dep() { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(sub => { sub.update(); }); } } Dep.target = null;
function Watcher(vm, exp, cb) { this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); } Watcher.prototype = { get: function() { Dep.target = this; var value = this.vm._data; this.exp.split('.').forEach(key => { value = value[key] }) Dep.target = null; return value; }, update: function() { this.run(); }, run: function() { var value = this.vm._data; this.exp.split('.').forEach(key => { value = value[key] }) if (value !== this.value) { this.value = value; this.cb.call(this.vm, value); } } }
function Compile(el, vm) { this.el = document.querySelector(el); this.vm = vm; this.init(); } Compile.prototype = { init: function() { this.fragment = this.node2Fragment(this.el); this.compile(this.fragment); this.el.appendChild(this.fragment); }, node2Fragment: function(el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while(child) { fragment.appendChild(child); child = el.firstChild; } return fragment; }, compile: function(el) { var childNodes = el.childNodes; var that = this; Array.prototype.slice.call(childNodes).forEach(node => { // if(that.isElementNode(node)) { // that.compileElement(node) // } if (that.isTextNode(node)) { that.compileText(node) } if(node.childNodes && node.childNodes.length) { that.compile(node) } }) }, compileElement: function(node) { var attributes = node.attributes; var that = this; Array.prototype.forEach.call(attributes, function(attr) { if(that.isDirective(attr)) { if(that.isModelDirective) { that.compileModel() } if(that.isHtmlDirective) { that.compileHtml() } if(that.isEventDirective) { that.compileEvnet() } } // if(that.isShortEventDirective(attr)) { // that.compileEvnet() // } }); }, compileText: function(node) { var that = this; var reg = /\{\{(.*)\}\}/; if(reg.test(node.textContent)) { var exp = reg.exec(node.textContent)[1].trim() var val = that.vm._data; exp.split('.').forEach(key => { val = val[key] }) that.updateText(node, val); new Watcher(that.vm, exp, function(value) { that.updateText(node, value) }) } }, compileModel: function() {}, compileHtml: function() {}, compileEvnet: function() {}, updateText: function(node, value) { node.textContent = value }, isDirective: function(attr) { return attr.indexof('v-') === 0; }, isEventDirective: function(attr) { return attr.indexof('on:') === 0; }, isShortEventDirective: function(attr) { return attr.indexof('@') === 0; }, isHtmlDirective: function(dir) { return dir.indexof('html') === 0; }, isModelDirective: function(dir) { return dir.indexof('model') === 0; }, isElementNode: function(node) { return node.nodeType === 1; }, isTextNode: function(node) { return node.nodeType === 3; } }
function Vue(options = {}) { this.$options = options; this.$el = document.getElementById(options.el); this._data = options.data; let data = this._data; this._proxyData(options.data); observer(data); this.methods = options.methods; new Compile(options.el, this) } Vue.prototype = { _proxyData: function(data) { var that = this; Object.keys(data).forEach(key => { Object.defineProperty(this, key, { configurable: true, enumerable: false, get() { return that._data[key]; }, set(newValue) { that._data[key] = newValue; }, }) }) } }
<div id="app"> <div> <span>{{ msg }}</span> <br> <span>{{ f.name }}</span> </div> </div>
var vm = new Vue({ el: '#app', data: { msg: '11', f: { name: 1 }, dom: '<strong></strong>' }, methods: { clickMe() { console.log(123); } } })
在 瀏覽器控制檯 輸入學習
vm.msg = 333; vm.f.name = 'xxxxx'
能夠看到數據變化了測試