vue 雙向資料繫結的實現學習(二)- 監聽器的實現
廢話:上一篇 ofollow,noindex">https://www.cnblogs.com/adouwt/p/9928278.html
提到了vue實現的基本實現原理:Object.defineProperty() -資料劫持 和 釋出訂閱者模式(觀察者),下面講的就是資料劫持在程式碼中的具體實現。
1.先看如何呼叫
new一個物件,傳入我們的引數,這個Myvue ,做了啥?
上面看到了在例項化一個Myvue 物件的時候,會執行init方法, init 方法做了兩個事,呼叫了observer 方法,和 例項化呼叫了 compile 方法。 到這裡我們就明白了,例項化一個Myvue後,我們要做的就是監聽資料變化和編譯模板 。
上面Object.key() 方法,例項化時傳入的data裡面對應的變數快取到 Myvue 物件的 $prop上,這樣方便在後續處理資料。怎麼個方便法呢!...
2.observer 的實現
observer ,模式裡面的角色定位 他是一個釋出者,也可以理解為是一個觀察者
function observer (data) { if(!data || typeof data !== 'object') { return; } Object.keys(data).forEach(key => { // 對每個屬性監聽處理 defineReactive(data, key, data[key]); }) }
defineReactive
function defineReactive (data,key,value) { // 每次訪問/修改屬性的時候 例項化一個排程中心Dep var dep = new Dep(); Object.defineProperty(data,key,{ get: function() { // 新增到watcher 的Dep 排程中心 if (Dep.target) { // Dep.target 是個什麼鬼? 轉到watcher.js 它是某個訂閱者 watcher dep.addSub(Dep.target);//這個程式碼段的意思: 如果有訂閱者(訪問/修改屬性的時候) 就將這個訂閱者統一放進 Dep 排程中心中 } // console.log(`${key}屬性被訪問了`) return value }, set: function (newValue) { if (value != newValue) { // console.log(`${key}屬性被重置了`) value = newValue dep.notify(); //我這裡有做改動了,通知排程中心的notify方法 } } }) // 遞迴呼叫,observe 這個value observer(value) }
Dep: 這裡是所有訂閱者的一個排程中心,它不是直接監聽 釋出者的資訊,釋出者將要釋出的資訊 釋出到 一箇中介、排程中心(Dep),由這個Dep 來排程資訊給哪個訂閱者(Watcher)
// 統一管理watcher訂閱者的Dep (排程中心)Dispatch center function Dep () { // 所有的watcher 放進這裡統一管理 this.subs = [] } Dep.target = null; // 通知檢視更新dom的 notify的方法 Dep.prototype.notify= function () { // this.subs 是上面訂閱器watcher 的集合 this.subs.forEach(sub => { // sub 是某個Watcher 具體呼叫某個Watcher的update 方法 sub.update() }) } // 新增訂閱者的方法 Dep.prototype.addSub= function (sub) { this.subs.push(sub) }
3.訂閱器Watcher
// 具體的訂閱器Watcher // 傳入一個vue 的例項, 監聽的屬性, 以及處理的回撥函式 function Watcher (vm,prop,callback) { this.vm=vm; this.$prop = prop; this.value = this.get(); this.callback = callback; // 具體watcher所具有的方法,不同的watcher 不同的回撥函式,處理不同的業務邏輯 } // 新增watcher 獲得屬性的get 方法,當有屬性訪問/設定 的時候,就產生訂閱者 將這個訂閱者放進排程中心 Watcher.prototype.get = function () { Dep.target = this; // 獲得屬性值 const value = this.vm.$data[this.$prop]; return value } // 新增watcher的更新檢視的方法 Watcher.prototype.update = function () { // 當屬性值有變化的時候,執行方法,更新試圖 const value = this.vm.$data[this.$prop]; const oldValue = this.value; // update 執行的時候,先獲取 vm 中data實時更新的屬性值,this.value 是vm data中之前的老值 if (oldValue != value) { // console.log('人家通知了,我要改變了') // 把剛剛獲取的更新值賦給之前vm data 中的值 this.value =value // 執行回撥函式 具體怎麼處理這個,看實際呼叫時候 callback 的處理 this.callback(this.value) } }
4.模板編譯
(為了直接看到頁面資料變化的效果,在模板編譯的核心資料處理上做了dom 操作,下一篇將講模板編譯的一些細節處理)
// dom模板編譯 vm 就是我們最上面的Myvue 物件 function Compile (vm) { this.vm = vm; this.$el = vm.el; // this.data = vm.data; this.fragment = null; // 用作後面模板引擎 建立文件片段 this.init() } Compile.prototype = { // init 方法簡單處理,直接做dom 操作,後面會用詳細的模板引擎的學習 init: function () { let value = this.vm.$data.name // 初始化獲取到的值 放進dom節點中 document.querySelector('.form-control').value = value; document.querySelector('.template').textContent= value // 通知訂閱者更新dom new Watcher(this.vm,this.vm.$prop, (value) => { document.querySelector('.form-control').value = value; document.querySelector('.template').textContent= value }) document.querySelector('.form-control').addEventListener('input',(e) => { let targetValue = e.target.value if(value !== targetValue) { this.vm.$data.name = e.target.value // 將修改的值 更新到 vm的data中 document.querySelector('.form-control').value = targetValue; // 更新dom 節點 document.querySelector('.template').textContent= targetValue } },false) } }
這樣就可以看到 在表單中,資料的雙向綁定了。
未完待續,錯誤之處,敬請指出,共同進步!
下一篇 vue 雙向資料繫結的實現學習(三)- 模板編譯
附:演示程式碼:
js:
function Myvue (options) { this.$options = options this.$el = document.querySelector(options.el); this.$data = options.data; Object.keys(this.$data).forEach(key => { this.$prop = key; }) this.init() } Myvue.prototype.init = function () { // 監聽資料變化 observer(this.$data); // 獲得值 // let value = this.$data[this.$prop]; // 不經過模板編譯直接 通知訂閱者更新dom // new Watcher(this,this.$prop,value => { //console.log(`watcher ${this.$prop}的改動,要有動靜了`) //this.$el.textContent = value // }) //通知模板編譯來執行頁面上模板變數替換 new Compile(this) } function observer (data) { if(!data || typeof data !== 'object') { return; } Object.keys(data).forEach(key => { // 對每個屬性監聽處理 defineReactive(data, key, data[key]); }) } function defineReactive (data,key,value) { // 每次訪問/修改屬性的時候 例項化一個排程中心Dep var dep = new Dep(); Object.defineProperty(data,key,{ get: function() { // 新增到watcher 的Dep 排程中心 if (Dep.target) { // Dep.target 是個什麼鬼? 轉到watcher.js 它是某個訂閱者 watcher dep.addSub(Dep.target);//這個程式碼段的意思: 如果有訂閱者(訪問/修改屬性的時候) 就將這個訂閱者統一放進 Dep 排程中心中 } // console.log(`${key}屬性被訪問了`) return value }, set: function (newValue) { if (value != newValue) { // console.log(`${key}屬性被重置了`) value = newValue dep.notify(); //我這裡有做改動了,通知排程中心的notify方法 } } }) // 遞迴呼叫,observe 這個value observer(value) } // 統一管理watcher訂閱者的Dep (排程中心)Dispatch center function Dep () { // 所有的watcher 放進這裡統一管理 this.subs = [] } Dep.target = null; // 通知檢視更新dom的 notify的方法 Dep.prototype.notify= function () { // this.subs 是上面訂閱器watcher 的集合 this.subs.forEach(sub => { // sub 是某個Watcher 具體呼叫某個Watcher的update 方法 sub.update() }) } // 新增訂閱者的方法 Dep.prototype.addSub= function (sub) { this.subs.push(sub) } // 具體的訂閱器Watcher // 傳入一個vue 的示例, 監聽的屬性, 以及處理的回撥函式 function Watcher (vm,prop,callback) { this.vm=vm; this.$prop = prop; this.value = this.get(); this.callback = callback; // 具體watcher所具有的方法,不同的watcher 不同的回撥函式,處理不同的業務邏輯 } // 新增watcher 獲得屬性的get 方法,當有屬性訪問/設定 的時候,就產生訂閱者 將這個訂閱者放進排程中心 Watcher.prototype.get = function () { Dep.target = this; // 獲得屬性值 const value = this.vm.$data[this.$prop]; return value } // 新增watcher的更新檢視的方法 Watcher.prototype.update = function () { // 當屬性值有變化的時候,執行方法,更新試圖 const value = this.vm.$data[this.$prop]; const oldValue = this.value; // update 執行的時候,先獲取 vm 中data實時更新的屬性值,this.value 是vm data中之前的老值 if (oldValue != value) { // console.log('人家通知了,我要改變了') // 把剛剛獲取的更新值賦給之前vm data 中的值 this.value =value // 執行回撥函式 具體怎麼處理這個,看實際呼叫時候 callback 的處理 this.callback(this.value) } } // dom模板編譯 vm 就是我們最上面的Myvue 物件 function Compile (vm) { this.vm = vm; this.$el = vm.el; // this.data = vm.data; this.fragment = null; // 用作後面模板引擎 建立文件片段 this.init() } Compile.prototype = { // init 方法簡單處理,直接做dom 操作,後面會用詳細的模板引擎的學習 init: function () { let value = this.vm.$data.name // 初始化獲取到的值 放進dom節點中 document.querySelector('.form-control').value = value; document.querySelector('.template').textContent= value // 通知訂閱者更新dom new Watcher(this.vm,this.vm.$prop, (value) => { document.querySelector('.form-control').value = value; document.querySelector('.template').textContent= value }) document.querySelector('.form-control').addEventListener('input',(e) => { let targetValue = e.target.value if(value !== targetValue) { this.vm.$data.name = e.target.value // 將修改的值 更新到 vm的data中 document.querySelector('.form-control').value = targetValue; // 更新dom 節點 document.querySelector('.template').textContent= targetValue } },false) } } View Code
html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Vue雙向繫結原理及實現</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <style> #app { margin: 20px auto; width: 400px; padding: 50px; text-align: center; border: 2px solid #ddd; } </style> </head> <body> <div id="app"> <input class="form-control" v-model="name" type="text"> <h1 class="template">{{name}}</h1> </div> <script src="./js/index1.js"></script> <script> const vm = new Myvue({ el: "#app", data: { name: "vue 雙向資料繫結test1" } }); </script> </body> </html> View Code