先看看咱們要實現的目標是什麼,以下動圖:html
單向數據綁定:把Model 綁定到View上,當咱們用js修改模型 Model 時候,視圖View上對應的內容也會改動,這就是 數據動,頁面動 。前端
雙向數據綁定:簡言之 數據動 頁面動,頁面動,數據動, 典型的應用就是在作表單時候,輸入框的內容改動後,跟該輸入框的value 的值改動。vue
看vue 官網上的這個V-model 的演示案例:git
要說出這個好處的時候,也只有在實際場景中才能對應的顯示出來。好比咱們須要實時顯示數據,咱們一邊說話,一邊實時顯示咱們說的話的文字內容,等等。這讓我想起了去年參加雲棲大會,臺上的大佬一邊說話,下面的字幕實時更新。(固然實現這個技術有不少技術點,咱們不討論這個內容,小編也才疏學淺,搞不懂)github
以上的都是廢話,咱們直接看看怎麼實現這個雙向數據綁定。緩存
Vue實現雙向數據綁定的原理:數據劫持 + 發佈訂閱模式(有的也稱爲觀察者模式)app
數據劫持的核心技術: Object.defineProperty()dom
**vue 3.0 已經用的不是這個技術了,採用是 原生的 Proxy,聽說速度可以提高100%,截張尤大的ppt,** 2018-11-21 修改本篇筆記ide
(香菇,剛研究會一點,就立馬變了,這就是前端世界),Proxy 的方式將會在本系列筆記結束後,再記錄這個技術點的使用函數
先上一個參照代碼,它長這個樣子:
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function(){
console.log('訪問year了,返回_year') return this._year; }, set: function(newValue){ if (newValue > 2004) { this._year = newValue;
console.log('從新設置_year了,並返回edition') this.edition += newValue - 2004; } } }); book.year = 2005; alert(book.edition); //2
---摘自 JavaScript高級程序設計
Object.defineProperty() 的具體介紹,咱們本文不作具體展開,查看我這裏的一篇文章,Object.defineProperty。 咱們這裏先要知道這麼一個事情。這個方法要傳入三個參數,傳入的數據對象data,屬性key,描述符對象。其中,描述符(descriptor)對象的屬 性必須是:configurable、enumerable、writable 和 value。設置其中的一或多個值,能夠修改 對應的特性值。咱們須要用的是這訪問器屬性。當咱們在讀取訪問器屬性時,會調用 getter 函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用 setter 函數並傳入新值,這個函數負責決定如何處理數據。上文代碼上的 get 方法,在讀取屬性時調用的函數,set方法,在寫入屬性時調用的函數。
我畫了一個圖,來理解這個模式,以下圖:
代碼解釋:
//下面封裝一個單例模式,內容是發佈訂閱模式 let event = { eventList: [], listener: function (key, fn) { if (!this.eventList[key]) { //沒有訂閱過此類消息,建立一個緩存列表 this.eventList[key] = []; } this.eventList[key].push(fn) }, trigger: function () { let key = Array.prototype.shift.call(arguments); // marry let fns = this.eventList[key]; if (!fns || fns.length == 0) { //沒有訂閱 則返回 return false; } for (let i = 0, fn; fn = fns[i++];) { fn.apply(this, arguments) // 調用 event.listen 裏面的 fn 方法,經過apply將當前執行的對象指向當前的this,arguments 傳進 fn 函數 } }, remove: function (key, fn) { // 取消訂閱 let fns = this.eventList[key]; if (!fns) { return false; } if(!fn) { fns && (fns.length = 0) } else { for (let l = fns.length-1; l>=0; l--) { let _fn = fns[l]; if( _fn === fn) { fns.splice(l, 1) } } } }, install: function (obj) { for (let i in this) { if (i === 'install') { return false } obj[i] = this[i]; } } } let testMsg = {} event.install(testMsg) // 上面方法 就會將event的方法 淺拷貝給 testMsg, 這樣testMsg就有 event的方法和屬性 testMsg.listener('rich', fn1 = (name) => { console.log(`${name}知道你有錢了`) }) testMsg.listener('borrowMoney', fn2 = (name) => { console.log(`${name}想問你借錢`) }) // listen方法將事件 放進隊列 // testMsg.remove('rich', fn1) // 取消訂閱 // trigger方法,處理事件隊列的方法,調用listen的函數的裏面的回調函數 fn testMsg.trigger('rich', '張三') testMsg.trigger('rich', '張三2') testMsg.trigger('borrowMoney', '李四') testMsg.trigger('borrowMoney', '李四2') // 代碼總結: // 訂閱的事件具備對應的key // 經過listener方法,將具體的事件隊列保存到 eventList ,能夠理解爲緩存列表也能夠是事件隊列; // 執行trigger 方法,將事件隊列拿出來執行調用 // remove 方法根據對應的key值,刪除對應的訂閱事件 // 模式總結:封裝一個單例event, 執行installEvent方法,將想要event對象拷貝到某個對象中去, // 發佈者trigger方法,監聽者listen方法 ,trigger 發佈一個東西,listen立馬知道你要發佈的東西
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) }
<script> const vm = new Myvue({ el: "#app", data: { name: "vue 雙向數據綁定test1" } }); </script>
未完待續,錯誤之處,敬請指出,共同進步!
文章參考:
http://www.javashuo.com/article/p-zupmyrcc-dw.html
https://github.com/youngwind/blog/issues/87
http://www.javashuo.com/article/p-rnawiolt-hw.html
後記:代碼只作基本實現,不作代碼健壯性處理,一些錯誤處理已經忽略