摘要:所以無需太過介懷是實現的單向或雙向綁定。響應事件瀏覽器變更事件事件執行或數據劫持則是采用數據劫持結合發布者訂閱者模式的方式,通過來劫持各個屬性的,,在數據變動時發布消息給訂閱者,觸發相應的監聽回調。
剖析Vue實現原理 - 如何實現雙向綁定mvvm
本文能幫你做什么?幾種實現雙向綁定的做法
1、了解vue的雙向數據綁定原理以及核心代碼模塊
2、緩解好奇心的同時了解如何實現雙向綁定
目前幾種主流的mvc(vm)框架都實現了單向數據綁定,而我所理解的雙向數據綁定無非就是在單向綁定的基礎上給可輸入元素(input、textare等)添加了change(input)事件,來動態修改model和 view,并沒有多高深。所以無需太過介懷是實現的單向或雙向綁定。
實現數據綁定的做法有大致如下幾種:
發布者-訂閱者模式(backbone.js)臟值檢查(angular.js)
數據劫持(vue.js)
發布者-訂閱者模式: 一般通過sub, pub的方式實現數據和視圖的綁定監聽,更新數據方式通常做法是 vm.set("property", value),不太熟悉去問一下度娘
這種方式現在畢竟太low了,我們更希望通過 vm.property = value這種方式更新數據,同時自動更新視圖,于是有了下面兩種方式
臟值檢查: angular.js 是通過臟值檢測的方式比對數據是否有變更,來決定是否更新視圖,最簡單的方式就是通過 setInterval() 定時輪詢檢測數據變動,當然Google不會這么low,angular只有在指定的事件觸發時進入臟值檢測,大致如下:
DOM事件,譬如用戶輸入文本,點擊按鈕等。( ng-click )
XHR響應事件 ( $http )
瀏覽器Location變更事件 ( $location )
Timer事件( $timeout , $interval )
執行 $digest() 或 $apply()
數據劫持: vue.js 則是采用數據劫持結合發布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數據變動時發布消息給訂閱者,觸發相應的監聽回調。
思路整理已經了解到vue是通過數據劫持的方式來做數據綁定的,其中最核心的方法便是通過Object.defineProperty()來實現對屬性的劫持,達到監聽數據變動的目的,無疑這個方法是本文中最重要、最基礎的內容之一,如果不熟悉defineProperty,猛戳這里 整理了一下,要實現mvvm的雙向綁定,就必須要實現以下幾點:
實現一個數據監聽器Observer,能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值并通知訂閱者
實現一個指令解析器Compile,對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數
實現一個Watcher,作為連接Observer和Compile的橋梁,能夠訂閱并收到每個屬性變動的通知,執行指令綁定的相應回調函數,從而更新視圖
mvvm入口函數,整合以上三者
不多贅述,一言不合就上圖
大家可去下載去具體文件里面看,我寫了詳盡的注釋,每個模塊的功能,分工,每個方法任務,等等
上圖為小編我根據自己的理解后重新繪制,本打算繪制再細一些,感覺會讓人理解更復雜而后就有了上圖,代碼中如果問題,歡迎指正,一起學習,你們的start是小編的動力
下面為具體代碼實現,為了大家方便我還是粘貼在readme里面,每個文件不多說了,前面做了文案及腦圖思路梳理,文件里我也了詳盡的注釋
MVVM.htmlMVVM.jsmvvm {{msg}}
/** * ----------------------------------------------------- * 1、實現數據代理 * 2、模版解析 * 3、劫持監所有的屬性 * ----------------------------------------------------- */ class MVVM { /** *Creates an instance of MVVM. * @param {*} options 當前實例傳遞過來的參數 * @memberof MVVM */ constructor(options){ this.$opt = options|| {} this.$data = options.data; // 實現數據代理 Object.keys(this.$data).forEach((key)=>{ this._proxyData(key) }) // 劫持監所有的屬性 observe(this.$data,this) // 模版編譯 new Compile(options.el || document.body,this) } _proxyData(key){ Object.defineProperty(this,key,{ configurable:false, enumerable:true, get(){ return this.$data[key] }, set(newVal){ this.$data[key] = newVal } }) } }Observer.js
/** * ----------------------------------------------------- * 1、實現一個數據監聽器Observer * 2、通知和添加訂閱者 * ----------------------------------------------------- */ class Observer { /** *Creates an instance of Observer. * @param {*} data 需要劫持監聽的數據 * @memberof Observer */ constructor(data){ this.$data = data || {} this.init() } init(){ Object.keys(this.$data).forEach(key=>{ this.defineReative(key,this.$data[key]) }) } defineReative(key,val){ // 創建發布者-訂閱者 let dep = new Dep() // 再去觀察子對象 observe(val) Object.defineProperty(this.$data,key,{ configurable:false, enumerable:true, get(){ // 添加訂閱者 Dep.target && dep.addSub(Dep.target) return val }, set(newVal){ if( newVal == val ) return false; val = newVal // 新的值是object的話,進行監聽 observe(newVal) // 通知訂閱者 dep.notfiy() } }) } } /** * 是否進行劫持監聽 * * @param {*} value 監聽對象 * @param {*} vm 當前實例 * @returns 返回 監聽實例 */ function observe(value, vm) { if (!value || typeof value !== "object") { return; } return new Observer(value); }; class Dep{ constructor(){ this.subs = [] } /** *維護訂閱者數組 * * @param {*} sub 訂閱實例 * @memberof Dep */ addSub(sub){ this.subs.push(sub) } notfiy(){ this.subs.forEach(sub=>{ // 通知數據更新 sub.update() }) } }Compile.js
/** * ----------------------------------------------------- * 1、取真實dom節點 * 2、我們fragment 創建文檔碎片,將真是dmo,移動指緩存 * 3、編譯虛擬dom,解析模版語法 * 4、回填至真是dom,實現模版語法解析,更新試圖 * ----------------------------------------------------- */ class Compile{ /** * *Creates an instance of Compile. * @param {*} el dmo選擇器 * @param {*} vm 當前實例 * @memberof Compile */ constructor(el,vm){ this.$vm = vm; this.$el = this.isElementNode(el) ? el : document.querySelector(el) if(this.$el){ this.$fragment = this.node2Fragment(this.$el) this.init() this.$el.appendChild(this.$fragment) } } init(){ this.compileElement(this.$fragment) } /** * * 編譯element * @param {*} el dmo節點 * @memberof Compile */ compileElement(el){ // 1、取所有子節點 let childNodes = el.childNodes // 2、循環子節點 Array.from(childNodes).forEach((node)=>{ // 判斷是文本節點還是dom節點 if(this.isElementNode(node)){ this.compileDom(node) }else if (this.isTextNode(node)){ this.compileText(node) } // 判斷當前節點是否有子節點,如果有,遞歸查找 if(node.childNodes && node.childNodes.length){ this.compileElement(node) } }) } /** * * 編譯元素節點 * @param {*} node 需要編譯的當前節點 * @memberof Compile */ compileDom(node){ // 取當前節點的屬性集合 let attrs = node.attributes // 循環屬性數組 Array.from(attrs).forEach(attr => { let attrName = attr.name // 判斷當前屬性是否是指令 if(this.isDirective(attrName)){ let [,dir] = attrName.split("-") let expr = attr.value //判斷當前屬性是普通指令還是事件指令 if(this.isEventDirective(dir)){ compileUtil.eventHandler(node,expr,dir,this.$vm) }else{ compileUtil[dir] && compileUtil[dir](node,expr,this.$vm) } } }); } /** * * 編譯文本節點 * @param {*} node 需要編譯的當前節點 * @memberof Compile */ compileText(node){ var text = node.textContent; var reg = /{{(.*)}}/; if(reg.test(text)){ compileUtil.text(node,RegExp.$1,this.$vm) } } /** * 判斷是否是元素節點 * * @param {*} el 節點 * @returns 是否 * @memberof Compile */ isElementNode(el){ return el.nodeType == 1 } /** * 過濾是否是指令 * * @param {*} name 屬性名 * @returns 是否 * @memberof Compile */ isDirective(name){ return name.indexOf("v-") == 0 } /** * 判斷是否是事件指令 * * @param {*} dir 指令,on:click * @returns 是否 * @memberof Compile */ isEventDirective(dir){ return dir.indexOf("on") == 0 } /** * 判斷是否是文本節點 * * @param {*} el 節點 * @returns 是否 * @memberof Compile */ isTextNode(el){ return el.nodeType == 3 } /** * 將真實dom拷貝到內存中 * * @param {*} el 真實dom * @returns 文檔碎片 * @memberof Compile */ node2Fragment(el){ let fragment = document.createDocumentFragment(); let children while(children = el.firstChild){ fragment.appendChild(el.firstChild) } return fragment } } // 指令處理工具 let compileUtil = { /** * 處理文本節點 * * @param {*} node 當前節點 * @param {*} expr 表達式 * @param {*} vm 當前實例 */ text(node,expr,vm){ this.buid(node,expr,vm,"text") }, /** * 處理表單元素節點 * * @param {*} node 當前節點 * @param {*} expr 表達式 * @param {*} vm 當前實例 */ model(node,expr,vm){ this.buid(node,expr,vm,"model") var me = this, val = this.getVMVal(vm, expr); node.addEventListener("input", function(e) { var newValue = e.target.value; if (val === newValue) { return; } me.setVMVal(vm, expr, newValue); val = newValue; }); }, /** * 事件處理 * * @param {*} node 當前節點 * @param {*} expr 表達式 * @param {*} dir 指令 * @param {*} vm 當前實例 */ eventHandler(node,expr,dir,vm){ let [,eventType] = dir.split(":"); let fn = vm.$opt.methods && vm.$opt.methods[expr] if(eventType && fn){ node.addEventListener(eventType,fn.bind(vm),false) } }, /** * 綁定事件統一處理方法抽離,添加watcher * * @param {*} node 當前節點 * @param {*} expr 表達式 * @param {*} vm 當前實例 * @param {*} dir 指令 */ buid(node,expr,vm,dir){ let updateFn = update[dir+"Update"] updateFn && updateFn(node,this.getVMVal(vm,expr)) new Watcher(vm, expr, function(value, oldValue) { updateFn && updateFn(node, value, oldValue); }); }, /** * 獲取表達式代表的值 * * @param {*} vm 當前實例 * @param {*} expr 表達式 * @returns */ getVMVal(vm,expr){ // return vm[expr] 要考慮,a.b.c的情況 let exp = expr.split("."); let val = vm exp.forEach((k)=>{ val = val[k] }) return val }, /** * 設置更新數據里對應的表達式的值 * * @param {*} vm * @param {*} expr * @param {*} newValue */ setVMVal(vm,expr,newValue){ let exp = expr.split("."); let val = vm exp.forEach((key,i)=>{ if(iWatcher.js /** * ----------------------------------------------------- * 1、實現一個Watcher,作為連接Observer和Compile的橋梁 * 2、通知和添加訂閱者 * ----------------------------------------------------- */ class Watcher { /** *Creates an instance of Watcher. * @param {*} vm 當前實例 * @param {*} expOrFn 表達式 * @param {*} cb 更新回調用 * @memberof Watcher */ constructor(vm,expOrFn,cb){ this.$vm = vm this.$expOrFn = expOrFn this.$cb = cb this.value = this.get() } get(){ // 添加訂閱者 Dep.target = this; // let dep = new Dep() // 去modal中取值,這個時候必然會觸發defineProperty的getter,真正的push訂閱者 let value = this.getVMVal(this.$vm,this.$expOrFn) // 用完了,重置回去 Dep.target = null return value } /** * 取modal里的值 * * @param {*} vm 當前實例 * @param {*} expr 表達式 * @returns 返回指 * @memberof Watcher */ getVMVal(vm,expr){ // return vm[expr] 要考慮,a.b.c的情況 let exp = expr.split("."); let val = vm exp.forEach((k)=>{ val = val[k] }) return val } // 對外暴露的跟新方法,比較新老值,得到訂閱通知進行更新 update(){ let oldVal = this.value; let newVal = this.getVMVal(this.$vm,this.$expOrFn) if (newVal !== oldVal) { this.value = newVal; this.$cb(newVal, oldVal); } } }最后感謝您的閱讀
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96407.html
摘要:暫時沒有指令和。當前模塊內的組件可以使用來自根模塊和當前模塊的任何服務及組件,也可以使用被導入模塊中導出的組件。作為一個前端菜雞,還是在深知自己眾多不足以及明白好記性不如爛筆頭的道理下,多造輪子總歸不會錯的。 有個同事跟我說:需求還是不夠多,都有時間造輪子了。。。 前言 這個輪子從18年4月22造到18年10月12日,本來就是看了一個文章講前端框架的路由實現原理之后,想試著擼一個路由試...
摘要:微信搜索小程序查一查物流,或者掃一掃下圖,歡迎來回復分享哦。小程序用框架開發的,方便快捷,寫法類似,支持相關操作,已可以引入包,不過在微信開發者工具有以下注意事項。對應關閉轉選項,關閉。對應關閉上傳代碼時樣式自動補全選項,關閉。 微信搜索小程序 查一查物流,或者掃一掃下圖,歡迎來回復分享哦。 showImg(https://segmentfault.com/img/bVbiR2p?w=...
摘要:微信搜索小程序查一查物流,或者掃一掃下圖,歡迎來回復分享哦。小程序用框架開發的,方便快捷,寫法類似,支持相關操作,已可以引入包,不過在微信開發者工具有以下注意事項。對應關閉轉選項,關閉。對應關閉上傳代碼時樣式自動補全選項,關閉。 微信搜索小程序 查一查物流,或者掃一掃下圖,歡迎來回復分享哦。 showImg(https://segmentfault.com/img/bVbiR2p?w=...
閱讀 725·2021-11-17 09:33
閱讀 3757·2021-09-01 10:46
閱讀 1751·2019-08-30 11:02
閱讀 3280·2019-08-29 15:05
閱讀 1396·2019-08-26 11:39
閱讀 2272·2019-08-23 17:04
閱讀 1973·2019-08-23 15:43
閱讀 1371·2019-08-23 14:12