Vue2.x的雙向綁定原理及實現(xiàn)
Vue 數(shù)據(jù)雙向綁定原理
Vue 是利用的 Object.defineProperty() 方法進行的數(shù)據(jù)劫持,利用 set、get 來檢測數(shù)據(jù)的讀寫。
https://jsrun.net/RMIKp/embed...
MVVM 框架主要包含兩個方面,數(shù)據(jù)變化更新視圖,視圖變化更新數(shù)據(jù)。
視圖變化更新數(shù)據(jù),如果是像 input 這種標簽,可以使用 oninput 事件..
數(shù)據(jù)變化更新視圖可以使用 Object.definProperty() 的 set 方法可以檢測數(shù)據(jù)變化,當數(shù)據(jù)改變就會觸發(fā)這個函數(shù),然后更新視圖。
實現(xiàn)過程
我們知道了如何實現(xiàn)雙向綁定了,首先要對數(shù)據(jù)進行劫持監(jiān)聽,所以我們需要設置一個 Observer 函數(shù),用來監(jiān)聽所有屬性的變化。
如果屬性發(fā)生了變化,那就要告訴訂閱者 watcher 看是否需要更新數(shù)據(jù),如果訂閱者有多個,則需要一個 Dep 來收集這些訂閱者,然后在監(jiān)聽器 observer 和 watcher 之間進行統(tǒng)一管理。
還需要一個指令解析器 compile,對需要監(jiān)聽的節(jié)點和屬性進行掃描和解析。
因此,流程大概是這樣的:
- 實現(xiàn)一個監(jiān)聽器 Observer,用來劫持并監(jiān)聽所有屬性,如果發(fā)生變動,則通知訂閱者。
- 實現(xiàn)一個訂閱者 Watcher,當接到屬性變化的通知時,執(zhí)行對應的函數(shù),然后更新視圖,使用 Dep 來收集這些 Watcher。
- 實現(xiàn)一個解析器 Compile,用于掃描和解析的節(jié)點的相關指令,并根據(jù)初始化模板以及初始化相應的訂閱器。
顯示一個 Observer
Observer 是一個數(shù)據(jù)監(jiān)聽器,核心方法是利用 Object.defineProperty() 通過遞歸的方式對所有屬性都添加 setter、getter 方法進行監(jiān)聽。
- var library = {
- book1: {
- name: "",
- },
- book2: "",
- };
- observe(library);
- library.book1.name = "vue權威指南"; // 屬性name已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“vue權威指南”
- library.book2 = "沒有此書籍"; // 屬性book2已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“沒有此書籍”
- // 為數(shù)據(jù)添加檢測
- 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函數(shù)
- dep.addSub(Dep.target); // 添加一個訂閱者
- }
- return val;
- },
- set: function(newVal) {
- if (val == newVal) return; // 如果值未發(fā)生改變就return
- val = newVal;
- console.log(
- "屬性" + key + "已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“" + newVal.toString() + "”"
- );
- dep.notify(); // 如果數(shù)據(jù)發(fā)生變化,就通知所有的訂閱者。
- },
- });
- }
- // 監(jiān)聽對象的所有屬性
- function observe(data) {
- if (!data || typeof data !== "object") {
- return; // 如果不是對象就return
- }
- Object.keys(data).forEach(function(key) {
- defineReactive(data, key, data[key]);
- });
- }
- // Dep 負責收集訂閱者,當屬性發(fā)生變化時,觸發(fā)更新函數(shù)。
- function Dep() {
- this.subs = {};
- }
- Dep.prototype = {
- addSub: function(sub) {
- this.subs.push(sub);
- },
- notify: function() {
- this.subs.forEach((sub) => sub.update());
- },
- };
思路分析中,需要有一個可以容納訂閱者消息訂閱器 Dep,用于收集訂閱者,在屬性發(fā)生變化時執(zhí)行對應的更新函數(shù)。
從代碼上看,將訂閱器 Dep 添加在 getter 里,是為了讓 Watcher 初始化時觸發(fā),,因此,需要判斷是否需要訂閱者。
在 setter 中,如果有數(shù)據(jù)發(fā)生變化,則通知所有的訂閱者,然后訂閱者就會更新對應的函數(shù)。
到此為止,一個比較完整的 Observer 就完成了,接下來開始設計 Watcher.
實現(xiàn) Watcher
訂閱者 Watcher 需要在初始化的時候?qū)⒆约禾砑拥接嗛喥?Dep 中,我們已經(jīng)知道監(jiān)聽器 Observer 是在 get 時執(zhí)行的 Watcher 操作,所以只需要在 Watcher 初始化的時候觸發(fā)對應的 get 函數(shù)去添加對應的訂閱者操作即可。
那給如何觸發(fā) get 呢?因為我們已經(jīng)設置了 Object.defineProperty(),所以只需要獲取對應的屬性值就可以觸發(fā)了。
我們只需要在訂閱者 Watcher 初始化的時候,在 Dep.target 上緩存下訂閱者,添加成功之后在將其去掉就可以了。
- function Watcher(vm, exp, cb) {
- this.cb = cb;
- this.vm = vm;
- this.exp = exp;
- thisthis.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]; // 強制執(zhí)行監(jiān)聽器里的get函數(shù)
- Dep.target = null; // 釋放自己
- return value;
- },
- };
到此為止, 簡單的額 Watcher 設計完畢,然后將 Observer 和 Watcher 關聯(lián)起來,就可以實現(xiàn)一個簡單的的雙向綁定了。
因為還沒有設計解析器 Compile,所以可以先將模板數(shù)據(jù)寫死。
將代碼轉化為 ES6 構造函數(shù)的寫法,預覽試試。
https://jsrun.net/8SIKp/embed...
這段代碼因為沒有實現(xiàn)編譯器而是直接傳入了所綁定的變量,我們只在一個節(jié)點上設置一個數(shù)據(jù)(name)進行綁定,然后在頁面上進行 new MyVue,就可以實現(xiàn)雙向綁定了。
并兩秒后進行值得改變,可以看到,頁面也發(fā)生了變化。
- // 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。
實現(xiàn) Compile
雖然上面實現(xiàn)了雙向數(shù)據(jù)綁定,但是整個過程都沒有解析 DOM 節(jié)店,而是固定替換的,所以接下來要實現(xiàn)一個解析器來做數(shù)據(jù)的解析和綁定工作。
解析器 compile 的實現(xiàn)步驟:
- 解析模板指令,并替換模板數(shù)據(jù),初始化視圖。
- 將模板指定對應的節(jié)點綁定對應的更新函數(shù),初始化相應的訂閱器。
為了解析模板,首先需要解析 DOM 數(shù)據(jù),然后對含有 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;
- }
接下來需要遍歷各個節(jié)點,對含有相關指令和模板語法的節(jié)點進行特殊處理,先進行最簡單模板語法處理,使用正則解析“{{變量}}”這種形式的語法。
- 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); // 繼續(xù)遞歸遍歷子節(jié)點
- }
- });
- },
- function compileText (node, exp) {
- var self = this;
- var initText = this.vm[exp];
- updateText(node, initText); // 將初始化的數(shù)據(jù)初始化到視圖中
- new Watcher(this.vm, exp, function (value) { // 生成訂閱器并綁定更新函數(shù)
- self.updateText(node, value);
- });
- },
- function updateText (node, value) {
- node.textContent = typeof value == 'undefined' ? '' : value;
- }
獲取到最外層的節(jié)點后,調(diào)用 compileElement 函數(shù),對所有的子節(jié)點進行判斷,如果節(jié)點是文本節(jié)點切匹配{{}}這種形式的指令,則進行編譯處理,初始化對應的參數(shù)。
然后需要對當前參數(shù)生成一個對應的更新函數(shù)訂閱器,在數(shù)據(jù)發(fā)生變化時更新對應的 DOM。
這樣就完成了解析、初始化、編譯三個過程了。
接下來改造一個 myVue 就可以使用模板變量進行雙向數(shù)據(jù)綁定了。
https://jsrun.net/K4IKp/embed...
添加解析事件
添加完 compile 之后,一個數(shù)據(jù)雙向綁定就基本完成了,接下來就是在 Compile 中添加更多指令的解析編譯,比如 v-model、v-on、v-bind 等。
添加一個 v-model 和 v-on 解析:
- function compile(node) {
- var nodenodeAttrs = node.attributes;
- var self = this;
- Array.prototype.forEach.call(nodeAttrs, function(attr) {
- var attrattrName = 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 函數(shù)是用于遍歷當前 dom 的所有節(jié)點屬性,然后判斷屬性是否是指令屬性,如果是在做對應的處理(事件就去監(jiān)聽事件、數(shù)據(jù)就去監(jiān)聽數(shù)據(jù)..)
完整版 myVue
在 MyVue 中添加 mounted 方法,在所有操作都做完時執(zhí)行。
- 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); // 所有事情處理好后執(zhí)行mounted函數(shù)
- }
- 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/embed...
總結一下流程,回頭在哪看一遍這個圖,是不是清楚很多了。
可以查看的代碼地址:Vue2.x 的雙向綁定原理及實現(xiàn)