vuejs實現雙向綁定的原則:利用資料劫持和發布訂閱模式,透過「Object.defineProperty()」來劫持各個屬性的setter、getter,在資料變動時發布訊息給訂閱者,觸發對應的監聽回調,進而對檢視進行更新。
本教學操作環境:windows7系統、vue2.9.6版,DELL G3電腦。
Vue實作資料雙向綁定主要利用的是: 資料劫持和發佈訂閱模式,利用的Object.defineProperty()
方法進行的資料劫持,然後通知發布者(主題對象)去通知所有觀察者,觀察者收到通知後,就會對視圖進行更新。
https://jsrun.net/RMIKp/embedded/all/light
MVVM 框架主要包含兩個方面,資料變更更新視圖,視圖變更更新資料。
視圖變更更新數據,如果是像input 這種標籤,可以使用oninput 事件..
資料變更更新視圖可以使用Object.definProperty()
的set方法可以偵測資料變化,當資料改變就會觸發這個函數,然後更新視圖。
我們知道如何實現雙向綁定了,首先要對資料進行劫持監聽,所以我們需要設定一個 Observer 函數,用來監聽所有屬性的變化。
如果屬性發生了變化,那就要告訴訂閱者watcher 看是否需要更新數據,如果訂閱者有多個,則需要一個Dep 來收集這些訂閱者,然後在監聽器observer 和watcher 之間進行統一管理。
還需要一個指令解析器 compile,對需要監聽的節點和屬性進行掃描和解析。
因此,流程大概是這樣的:
實作一個監聽器 Observer,用來劫持並監聽所有屬性,如果發生變動,則通知訂閱者。
實作一個訂閱者 Watcher,當接到屬性變化的通知時,執行對應的函數,然後更新視圖,使用 Dep 來收集這些 Watcher。
實作一個解析器 Compile,用於掃描和解析的節點的相關指令,並根據初始化模板以及初始化對應的訂閱器。
#Observer 是一個資料監聽器,核心方法是利用Object.defineProperty()
透過遞歸的方式對所有屬性都添加setter、getter 方法進行監聽。
var library = { book1: { name: "", }, book2: "", }; observe(library); library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南” library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍” // 为数据添加检测 function defineReactive(data, key, val) { observe(val); // 递归遍历所有子属性 let dep = new Dep(); // 新建一个dep Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { // 判断是否需要添加订阅者,仅第一次需要添加,之后就不用了,详细看Watcher函数 dep.addSub(Dep.target); // 添加一个订阅者 } return val; }, set: function(newVal) { if (val == newVal) return; // 如果值未发生改变就return val = newVal; console.log( "属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”" ); dep.notify(); // 如果数据发生变化,就通知所有的订阅者。 }, }); } // 监听对象的所有属性 function observe(data) { if (!data || typeof data !== "object") { return; // 如果不是对象就return } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); } // Dep 负责收集订阅者,当属性发生变化时,触发更新函数。 function Dep() { this.subs = {}; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach((sub) => sub.update()); }, };
思路分析中,需要有一個可以容納訂閱者訊息訂閱器 Dep,用於收集訂閱者,在屬性發生變化時執行對應的更新函數。
從程式碼上看,將訂閱器 Dep 加入在 getter 裡,是為了讓 Watcher 初始化時觸發,,因此,需要判斷是否需要訂閱者。
在 setter 中,如果有資料發生變化,則通知所有的訂閱者,然後訂閱者就會更新對應的函數。
到此為止,一個比較完整的Observer 就完成了,接下來開始設計Watcher.
訂閱者Watcher 需要在初始化的時候將自己添加到訂閱器Dep 中,我們已經知道監聽器Observer 是在get 時執行的Watcher 操作,所以只需要在Watcher 初始化的時候觸發對應的get 函數去添加對應的訂閱者操作即可。
那給如何觸發 get 呢?因為我們已經設定了 Object.defineProperty(),所以只需要取得對應的屬性值就可以觸發了。
我們只需要在訂閱者 Watcher 初始化的時候,在 Dep.target 上快取下訂閱者,添加成功之後在將其去掉就可以了。
function Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp = exp; this.value = this.get(); // 将自己添加到订阅器的操作 } Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { Dep.target = this; // 缓存自己,用于判断是否添加watcher。 var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; }, };
到此為止, 簡單的額 Watcher 設計完畢,然後將 Observer 和 Watcher 關聯起來,就可以實現一個簡單的的雙向綁定了。
因為還沒有設計解析器 Compile,所以可以先將模板資料寫死。
將程式碼轉換為 ES6 建構子的寫法,預覽試試。
https://jsrun.net/8SIKp/embedded/all/light
#這段程式碼因為沒有實作編譯器而是直接傳入了所綁定的變量,我們只在一個節點上設定一個資料(name)進行綁定,然後在頁面上進行new MyVue,就可以實現雙向綁定了。
並兩秒後進行值得改變,可以看到,頁面也發生了變化。
// MyVue proxyKeys(key) { var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function proxyGetter() { return self.data[key]; }, set: function proxySetter(newVal) { self.data[key] = newVal; } }); }
上面這段程式碼的功能是將 this.data 的 key 代理到 this 上,使得我可以方便的使用 this.xx 就可以取到 this.data.xx。
雖然上面實作了雙向資料綁定,但是整個過程都沒有解析DOM 節店,而是固定替換的,所以接下來要實作一個解析器來做數據的解析和綁定工作。
解析器 compile 的實作步驟:
解析模板指令,並取代模板數據,初始化視圖。
将模板指定对应的节点绑定对应的更新函数,初始化相应的订阅器。
为了解析模板,首先需要解析 DOM 数据,然后对含有 DOM 元素上的对应指令进行处理,因此整个 DOM 操作较为频繁,可以新建一个 fragment 片段,将需要的解析的 DOM 存入 fragment 片段中在进行处理。
function nodeToFragment(el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 将Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild; } return fragment; }
接下来需要遍历各个节点,对含有相关指令和模板语法的节点进行特殊处理,先进行最简单模板语法处理,使用正则解析“{{变量}}”这种形式的语法。
function compileElement (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /\{\{(.*)\}\}/; // 匹配{{xx}} var text = node.textContent; if (self.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令 self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); // 继续递归遍历子节点 } }); }, function compileText (node, exp) { var self = this; var initText = this.vm[exp]; updateText(node, initText); // 将初始化的数据初始化到视图中 new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数 self.updateText(node, value); }); }, function updateText (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }
获取到最外层的节点后,调用 compileElement 函数,对所有的子节点进行判断,如果节点是文本节点切匹配{{}}这种形式的指令,则进行编译处理,初始化对应的参数。
然后需要对当前参数生成一个对应的更新函数订阅器,在数据发生变化时更新对应的 DOM。
这样就完成了解析、初始化、编译三个过程了。
接下来改造一个 myVue 就可以使用模板变量进行双向数据绑定了。
https://jsrun.net/K4IKp/embedded/all/light
添加完 compile 之后,一个数据双向绑定就基本完成了,接下来就是在 Compile 中添加更多指令的解析编译,比如 v-model、v-on、v-bind 等。
添加一个 v-model 和 v-on 解析:
function compile(node) { var nodeAttrs = node.attributes; var self = this; Array.prototype.forEach.call(nodeAttrs, function(attr) { var attrName = attr.name; if (isDirective(attrName)) { var exp = attr.value; var dir = attrName.substring(2); if (isEventDirective(dir)) { // 事件指令 self.compileEvent(node, self.vm, exp, dir); } else { // v-model 指令 self.compileModel(node, self.vm, exp, dir); } node.removeAttribute(attrName); // 解析完毕,移除属性 } }); } // v-指令解析 function isDirective(attr) { return attr.indexOf("v-") == 0; } // on: 指令解析 function isEventDirective(dir) { return dir.indexOf("on:") === 0; }
上面的 compile 函数是用于遍历当前 dom 的所有节点属性,然后判断属性是否是指令属性,如果是在做对应的处理(事件就去监听事件、数据就去监听数据..)
在 MyVue 中添加 mounted 方法,在所有操作都做完时执行。
class MyVue { constructor(options) { var self = this; this.data = options.data; this.methods = options.methods; Object.keys(this.data).forEach(function(key) { self.proxyKeys(key); }); observe(this.data); new Compile(options.el, this); options.mounted.call(this); // 所有事情处理好后执行mounted函数 } proxyKeys(key) { // 将this.data属性代理到this上 var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function getter() { return self.data[key]; }, set: function setter(newVal) { self.data[key] = newVal; }, }); } }
然后就可以测试使用了。
https://jsrun.net/Y4IKp/embedded/all/light
总结一下流程,回头在哪看一遍这个图,是不是清楚很多了。
可以查看的代码地址:Vue2.x 的双向绑定原理及实现
相关推荐:《vue.js教程》
以上是vuejs實作雙向綁定的原理是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!