摘要:監聽器構造函數被監聽數據屬性遍歷監聽函數屬性被監聽了,現在值為監聽器被監聽對象構造函數所有入參監聽數據更新視圖實現在流程介紹中,我們需要創建一個可以訂閱者的訂閱器,主要負責手機訂閱者,屬性變化的時候執行相應的訂閱者,更新函數。
1、目標實現
理解雙向數據綁定原理;
實現{{}}、v-model和基本事件指令v-bind(:)、v-on(@);
新增屬性的雙向綁定處理;
PS:實例源碼https://github.com/wuwhs/imit...,歡迎隨手給個start,就此謝過!
2、雙向數據綁定原理vue實現對數據的雙向綁定,通過對數據劫持結合發布者-訂閱者模式實現的。
2.1 Object.definePropertyvue通過Object.defineProperty來實現數據劫持,會對數據對象每個屬性添加對應的get和set方法,對數據進行讀取和賦值操作就分別調用get和set方法。
Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { // do something return val; }, set: function(newVal) { // do something } });
我們可以將一些方法放到里面,從而完成對數據的監聽(劫持)和視圖的同步更新。
2.2 過程說明實現雙向數據綁定,首先要對數據進行數據監聽,需要一個監聽器Observer,監聽所有屬性。如果屬性發生變化,會調用setter和getter,再去告訴訂閱者Watcher是否需要更新。由于訂閱者有很多個,我們需要一個消息訂閱器Dep來專門收集這些訂閱者,然后在監聽器Observer和訂閱者Watcher之間進行統一管理。還有,我們需要一個指令解析器Complie,對每個元素進行掃描和解析,將相關指令對應初始化成一個訂閱者Watcher,并替換模板數據或綁定相應函數。當訂閱者Watcher接收到相應屬性的變化,就會執行對應的更新函數,從而更新視圖。
3、實現ObserverObserver是一個數據監聽器,核心方法是我們提到過的Object.defineProperty。如果要監聽所有屬性的話,則需要通過遞歸遍歷,對每個子屬性都defineProperty。
/** * 監聽器構造函數 * @param {Object} data 被監聽數據 */ function Observer(data) { if (!data || typeof data !== "object") { return; } this.data = data; this.walk(data); } Observer.prototype = { /** * 屬性遍歷 */ walk: function(data) { var self = this; Object.keys(data).forEach(function(key) { self.defineReactive(data, key, data[key]); }); }, /** * 監聽函數 */ defineReactive: function(data, key, val) { observe(val); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; console.log("屬性:" + key + "被監聽了,現在值為:" + newVal); updateView(newVal); } }); updateView(val); } } /** * 監聽器 * @param {Object} data 被監聽對象 */ function observe(data) { return new Observer(data); } /** * vue構造函數 * @param {Object} options 所有入參 */ function MyVue(options) { this.vm = this; this.data = options.data; // 監聽數據 observe(this.data); return this; } /** * 更新視圖 * @param {*} val */ function updateView(val) { var $name = document.querySelector("#name"); $name.innerHTML = val; } var myvm = new MyVue({ el: "#demo", data: { name: "hello word" } });4、實現Dep
在流程介紹中,我們需要創建一個可以訂閱者的訂閱器Dep,主要負責手機訂閱者,屬性變化的時候執行相應的訂閱者,更新函數。下面稍加改造Observer,就可以插入我們的訂閱器。
Observer.prototype = { // ... /** * 監聽函數 */ defineReactive: function(data, key, val) { var dep = new Dep(); observe(val); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { // 判斷是否需要添加訂閱者 什么時候添加訂閱者呢? 與實際頁面DOM有關聯的data屬性才添加相應的訂閱者 if (Dep.target) { // 添加一個訂閱者 dep.addSub(Dep.target); } return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; console.log("屬性:" + key + "被監聽了,現在值為:" + newVal); // 通知所有訂閱者 dep.notify(newVal); } }); updateView(val); // 訂閱器標識本身實例 Dep.target = dep; // 強行執行getter,往訂閱器中添加訂閱者 var v = data[key]; // 釋放自己 Dep.target = null; } } /** * 監聽器 * @param {Object} data 被監聽對象 */ function observe(data) { return new Observer(data); } /** * 訂閱器 */ function Dep() { this.subs = []; this.target = null; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); console.log("this.subs:", this.subs); }, notify: function(data) { this.subs.forEach(function(sub) { sub.update(data); }); }, update: function(val) { updateView(val) } }; // ...
PS:將訂閱器Dep添加到一個訂閱者設計到getter里面,是為了讓Watcher初始化進行觸發。
5、實現Watcher訂閱者Watcher在初始化的時候需要將自己添加到訂閱器Dep中,那該如何添加呢?我們已經知道監聽器Observer是在get函數執行添加了訂閱者Watcher的操作,所以我們只要在訂閱者Watcher初始化的時候觸發對應的get函數去執行添加訂閱者操作。那么,怎樣去觸發get函數?很簡單,只需獲取對應的屬性值就可以觸發了,因為我們已經用Object.defineProperty監聽了所有屬性。vue在這里做了個技巧處理,就是咋我們添加訂閱者的時候,做一個判斷,判斷是否是事先緩存好的Dep.target,在訂閱者添加成功后,把target重置null即可。
// ... /** * 訂閱者 * @param {Object} vm vue對象 * @param {String} exp 屬性值 * @param {Function} cb 回調函數 */ function Watcher(vm, exp, cb) { this.vm = vm; this.exp = exp; this.cb = cb; // 將自己添加到訂閱器 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; // 強制執行監聽器里的get函數 // this.vm.data[this.exp] 調用getter,添加一個訂閱者sub,存入到全局變量subs var value = this.vm.data[this.exp]; // 釋放自己 Dep.target = null; return value; } }; /** * vue構造函數 * @param {Object} options 所有入參 */ function MyVue(options) { this.vm = this; this.data = options.data; observe(this.data); var $name = document.querySelector("#name"); // 給name屬性添加一個訂閱者到訂閱器中,當屬性發生變化后,觸發回調 var w = new Watcher(this, "name", function(val) { $name.innerHTML = val; }); return this; }
到這里,其實已經實現了我們的雙向數據綁定:能夠根據初始數據初始化頁面特定元素,同時當數據改變也能更新視圖。
5、實現Compile步驟4整個過程都能有去解析DOM節點,而是直接固定節點進行替換。接下來我們就來實現一個解析器,完成一些解析和綁定工作。
獲取頁面的DOM節點,遍歷存入到文檔碎片對象中;
解析出文本節點,匹配{{}}(暫時只做"{{}}"的解析),用初始化數據替換,并添加相應訂閱者;
分離出節點的指令v-on、v-bind和v-model,綁定相應的事件和函數;
// ... /** * 編譯器構造函數 * @param {String} el 根元素 * @param {Object} vm vue對象 */ function Compile(el, vm) { this.vm = vm; this.el = document.querySelector(el); this.fragment = null; this.init(); } Compile.prototype = { /** * 初始 */ init: function() { if (this.el) { console.log("this.el:", this.el); // 移除頁面元素生成文檔碎片 this.fragment = this.nodeToFragment(this.el); // 編譯文檔碎片 this.compileElement(this.fragment); this.el.appendChild(this.fragment); } else { console.log("DOM Selector is not exist"); } }, /** * 頁面DOM節點轉化成文檔碎片 */ nodeToFragment: function(el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; // 此處添加打印,出來的不是頁面中原始的DOM,而是編譯后的? // NodeList是引用關系,在編譯后相應的值被替換了,這里打印出來的NodeList是后來被引用更新了的 console.log("el:", el); // console.log("el.firstChild:", el.firstChild.nodeValue); while (child) { // append后,原el上的子節點被刪除了,掛載在文檔碎片上 fragment.appendChild(child); child = el.firstChild; } return fragment; }, /** * 編譯文檔碎片,遍歷到當前是文本節點則去編譯文本節點,如果當前是元素節點,并且存在子節點,則繼續遞歸遍歷 */ compileElement: function(fragment) { var childNodes = fragment.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { // var reg = /{{s*(.+)s*}}/g; var reg = /{{s*((?:.| )+?)s*}}/g; var text = node.textContent; if (self.isElementNode(node)) { self.compileAttr(node); } else if (self.isTextNode(node) && reg.test(text)) { reg.lastIndex = 0 /* var match; while(match = reg.exec(text)) { self.compileText(node, match[1]); } */ self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); } }); }, /** * 編譯屬性 */ compileAttr: function(node) { var nodeAttrs = node.attributes; var self = this; Array.prototype.forEach.call(nodeAttrs, function(attr) { var attrName = attr.name; // 只對vue本身指令進行操作 if (self.isDirective(attrName)) { var exp = attr.value; // v-on指令 if (self.isOnDirective(attrName)) { self.compileOn(node, self.vm, exp, attrName); } // v-bind指令 if(self.isBindDirective(attrName)) { self.compileBind(node, self.vm, exp, attrName); } // v-model else if (self.isModelDirective(attrName)) { self.compileModel(node, self.vm, exp, attrName); } node.removeAttribute(attrName); } }) }, /** * 編譯文檔碎片節點文本,即對標記替換 */ compileText: function(node, exp) { var self = this; var exps = exp.split("."); var initText = this.vm.data[exp]; // 初始化視圖 this.updateText(node, initText); // 添加一個訂閱者到訂閱器 var w = new Watcher(this.vm, exp, function(val) { self.updateText(node, val); }); }, /** * 編譯v-on指令 */ compileOn: function(node, vm, exp, attrName) { // @xxx v-on:xxx var onRE = /^@|^v-on:/; var eventType = attrName.replace(onRE, ""); var cb = vm.methods[exp]; if (eventType && cb) { node.addEventListener(eventType, cb.bind(vm), false); } }, /** * 編譯v-bind指令 */ compileBind: function (node, vm, exp, attrName) { // :xxx v-bind:xxx var bindRE = /^:|^v-bind:/; var attr = attrName.replace(bindRE, ""); var val = vm.data[exp]; node.setAttribute(attr, val); }, /** * 編譯v-model指令 */ compileModel: function(node, vm, exp, attrName) { var self = this; var val = this.vm.data[exp]; // 初始化視圖 this.modelUpdater(node, val); // 添加一個訂閱者到訂閱器 new Watcher(this.vm, exp, function(value) { self.modelUpdater(node, value); }); // 綁定input事件 node.addEventListener("input", function(e) { var newVal = e.target.value; if (val === newVal) { return; } self.vm.data[exp] = newVal; // val = newVal; }); }, /** * 更新文檔碎片相應的文本節點 */ updateText: function(node, val) { node.textContent = typeof val === "undefined" ? "" : val; }, /** * model更新節點 */ modelUpdater: function(node, val, oldVal) { node.value = typeof val == "undefined" ? "" : val; }, /** * 屬性是否是vue指令,包括v-xxx:,:xxx,@xxx */ isDirective: function(attrName) { var dirRE = /^v-|^@|^:/; return dirRE.test(attrName); }, /** * 屬性是否是v-on指令 */ isOnDirective: function(attrName) { var onRE = /^v-on:|^@/; return onRE.test(attrName); }, /** * 屬性是否是v-bind指令 */ isBindDirective: function (attrName) { var bindRE = /^v-bind:|^:/; return bindRE.test(attrName); }, /** * 屬性是否是v-model指令 */ isModelDirective: function(attrName) { var mdRE = /^v-model/; return mdRE.test(attrName); }, /** * 判斷元素節點 */ isElementNode: function(node) { return node.nodeType == 1; }, /** * 判斷文本節點 */ isTextNode: function(node) { return node.nodeType == 3; } }; /** * vue構造函數 * @param {Object} options 所有入參 */ function MyVue(options) { this.vm = this; this.data = options.data; this.methods = options.methods; observe(this.data); new Compile(options.el, this.vm); return this; }
這樣我們就可以調用指令v-bind、v-on和v-model。
5、其他 5.1 proxy代理data{{ name }}
可能注意到了,我們不管是在賦值還是取值,都是在myvm.data.someAttr上操作的,而在vue上我們習慣直接myvm.someAttr這種形式。怎樣實現呢?同樣,我們可以用Object.defineProperty對data所有屬性做一個代理,即訪問vue實例屬性時,代理到data上。很簡單,實現如下:
/** * 將數據拓展到vue的根,方便讀取和設置 */ MyVue.prototype.proxy = function(key) { var self = this; Object.defineProperty(this, key, { enumerable: true, configurable: true, get: function proxyGetter() { return self.data[key]; }, set: function proxySetter(newVal) { self.data[key] = newVal; } }); }5.2 parsePath
上面對于data的操作只是到對于簡單的基本類型屬性,對于對象屬性的改變該怎么更新到位呢?其實,只要深度遍歷對象屬性路徑,就可以找到要訪問屬性值。
/** * 根據對象屬性路徑,最終獲取值 * @param {Object} obj 對象 * @param {String} path 路徑 * return 值 */ function parsePath(obj, path) { var bailRE = /[^w.$]/; if (bailRE.test(path)) { return } var segments = path.split("."); for (var i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; } return obj; }
用這個方法替換我們的所有取值操作
vm[exp] => parsePath(vm, exp)
Vue 不允許在已經創建的實例上動態添加新的根級響應式屬性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法將響應屬性添加到嵌套的對象上。
也就是我們需要在Vue原型上添加一個set方法去設置新添加的屬性,新屬性同樣要進行監聽和添加訂閱者。
/** * vue的set方法,用于外部新增屬性 Vue.$set(target, key, val) * @param {Object} target 數據 * @param {String} key 屬性 * @param {*} val 值 */ function set(target, key, val) { if (Array.isArray(target)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); return val; } if (target.hasOwnProperty(key)) { target[key] = val; return val } var ob = (target).$Observer; if (!ob) { target[key] = val; return val } // 對新增屬性定義監聽 ob.defineReactive(target, key, val); ob.dep.notify(); return val; } MyVue.prototype.$set = set;6.1 給數組對象添加屬性
把數組看成一個特殊的對象,就很容易理解了,對于unshift、push和splice變異方法是添加了對象的屬性的,需要對新加的屬性進行監聽和添加訂閱者。
var arrKeys = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"]; var extendArr = []; arrKeys.forEach(function(key) { def(extendArr, key, function() { var result, arrProto = Array.prototype, ob = this.$Observer, arr = arrProto.slice.call(arguments), inserted, index; switch (key) { case "push": inserted = arr; index = this.length; break; case "unshift": inserted = arr; index = 0; break; case "splice": inserted = arr.slice(2); index = arr[0]; break; } result = arrProto[key].apply(this, arguments); // 監聽新增數組對象屬性 if (inserted) { ob.observeArray(inserted); } ob.dep.notify(); return result; }); }); var arrayKeys = Object.getOwnPropertyNames(extendArr); /** * 監聽器構造函數 * @param {Object} data 被監聽數據 */ function Observer(data) { this.dep = new Dep(); if (!data || typeof data !== "object") { return; } // 在每個object上添加一個observer def(data, "$Observer", this); // 繼承變異方法 if (Array.isArray(data)) { // 把數組變異方法的處理,添加到原型鏈上 data.__proto__ = extendArr; // 監聽數組對象屬性 this.observeArray(data); } else { this.data = data; this.walk(data); } } Observer.prototype = { // ... /** * 監聽數組 */ observeArray: function(items) { console.log("items:", items); for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } } };
本文是在查看vue源碼及大神相關博客而成,只為加深自己的學習印象,拿出來和大家一起學習,有什么不對的地方歡迎提出,參考文章:
http://www.cnblogs.com/giggle...
http://www.cnblogs.com/canfoo...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107453.html
摘要:源代碼響應式原理數據觀察數據遞歸無反應無反應無反應包括無反應講解只所以能實現雙向綁定,是利用里面的這就是為什么只支持及以上從以上代碼可以看出,對象屬性的刪除和添加新屬性,不會觸發對應的方法對于初始化沒有定義的屬性,設置值不能觸發視圖層渲染在 源代碼1 // 響應式原理 defineProperty //數據 const data = { obj: { a: 4, ...
摘要:數據驅動的三維圖形可視化在信息暴漲的年間,冷暴力的扁平化確實有效降低用戶的信息焦慮感,使有限的精力更高效處理過多的信息流。 數據驅動的三維圖形可視化 在信息暴漲的2010-2016年間,冷暴力的扁平化確實有效降低用戶的信息焦慮感,使有限的精力更高效處理過多的信息流。二維平面化扁平化在蘋果等大頭引領下,成為大眾用戶機器交流默認的語言。然后,隨著PC、平板、手機、智能家居等用戶持有終端的性...
摘要:作為目前最熱門最具前景的前端框架之一,其提供了一種幫助我們快速構建并開發前端項目的新的思維模式。的新版本,的簡稱。的包管理工具,用于同一管理我們前端項目中需要用到的包插件工具命令等,便于開發和維護。 Vue.js作為目前最熱門最具前景的前端框架之一,其提供了一種幫助我們快速構建并開發前端項目的新的思維模式。本文旨在幫助大家認識Vue.js,了解Vue.js的開發流程,并進一步理解如何通...
摘要:由于是需要兼容的后臺系統,該項目并不能使用到等技術,因此我在上的經驗大都是使用原生的編寫的,可以看見一個組件分為兩部分視圖部分,和數據部分。 在公司里幫項目組里開發后臺系統的前端項目也有一段時間了。 vue這種數據驅動,組件化的框架和react很像,從一開始的快速上手基本的開發,到后來開始自定義組件,對element UI的組件二次封裝以滿足項目需求,期間也是踩了不少坑。由于將來很長一...
在公司做了一次vue相關的培訓,自己整理了一些大綱。供大家參考學習!當然 優先要先看官方文檔 1. 項目構成及原理 Vue 主流框架見解及差別 react ALL IN JS 靈活 angular 架構清晰 層級多 重 vue 類似react并吸收了angular的一些優點 Node運行在服務端的JS 谷歌V8引擎 使JS語言能在服務器端運行 Webpack—一個前端的打包工具 ...
閱讀 2490·2021-11-25 09:43
閱讀 2584·2021-11-16 11:50
閱讀 3279·2021-10-09 09:44
閱讀 3192·2021-09-26 09:55
閱讀 2833·2019-08-30 13:50
閱讀 1025·2019-08-29 13:24
閱讀 2068·2019-08-26 11:44
閱讀 2789·2019-08-26 11:37