1. 程式人生 > >Vue 雙向繫結的原理

Vue 雙向繫結的原理

Vue雙向繫結的原理

大致思路: 首先Vue會使用documentfragment劫持根元素裡包含的所有節點,這些節點不僅包括標籤元素,還包括文字,甚至換行的回車。 
然後Vue會把data中所有的資料,用defindProperty()變成Vue的訪問器屬性,這樣每次修改這些資料的時候,就會觸發相應屬性的get,set方法。 
接下來編譯處理劫持到的dom節點,遍歷所有節點,根據nodeType來判斷節點型別,根據節點本身的屬性(是否有v-model等屬性)或者文字節點的內容(是否符合{{文字插值}}的格式)來判斷節點是否需要編譯。對v-model,繫結事件當輸入的時候,改變Vue中的資料。對文字節點,將他作為一個觀察者watcher放入觀察者列表,當Vue資料改變的時候,會有一個主題物件,對列表中的觀察者們釋出改變的訊息,觀察者們再更新自己,改變節點中的顯示,從而達到雙向繫結的目的。



defineProperty()方法單獨定義。//訪問器屬性的"值"比較特殊,讀取或設定訪問器屬性的值,實際上是呼叫其內部特性:get和set函式。functiondefineReactive(obj,key,val){//這裡用到了觀察者(訂閱/釋出)模式,它定義了一種一對多的關係,讓多個觀察者監聽一個主題物件,這個主題物件的狀態發生改變時會通知所有觀察者物件,觀察者物件就可以更新自己的狀態。//例項化一個主題物件,物件中有空的觀察者列表var dep =new Dep();//將data的每一個屬性都設定為Vue物件的訪問器屬性,屬性名和data中相同//所以每次修改Vue.data的時候,都會呼叫下邊的get和set方法。然後會監聽v-model的input事件,當改變了input的值,就相應的改變Vue.data的資料,然後觸發這裡的set方法

Object.defineProperty(obj,key,{ get: function(){//Dep.target指標指向watcher,增加訂閱者watcher到主體物件Depif(Dep.target){ dep.addSub(Dep.target); }return val; }, set:function(newVal){if(newVal === val){return } val = newVal;//console.log(val);//給訂閱者列表中的watchers發出通知 dep.notify(); } }); }//主題物件Dep建構函式functionDep(){this.subs = []; }//Dep有兩個方法,增加觀察者 和 釋出訊息
Dep.prototype = { addSub:function(sub){this.subs.push(sub); }, notify:function(){this.subs.forEach(function(sub){ sub.update(); }); } } //DocumentFragment(文件片段)可以看作節點容器,它可以包含多個子節點,當我們將它插入到DOM中時,只有它的子節點會插入目標節點,所以把它看作一組節點的容器。使用DocumentFragment處理節點,速度和效能遠遠優於直接操作DOM。Vue進行編譯時,就是將掛載目標的所有子節點劫持(真的是劫持)到DocumentFragment中,經過一番處理後,再將DocumentFragment整體返回插入掛載目標。//var dom = nodeToFragment(document.getElementById("app"));//console.log(dom);//返回到app中//document.getElementById("app").appendChild(dom);functionnodeToFragment(node,vm){var flag = document.createDocumentFragment();var child;//劫持node的所有子節點(真的在dom樹中消失了,所以要在下邊重新返回搭到app中)while (child = node.firstChild){//先編譯所有的子節點,再劫持到文件片段中 compile(child,vm); flag.appendChild(child); }return flag; }//編譯節點,初始化資料繫結functioncompile(node,vm){//該正則匹配的是 :{{任意內容}}var reg =/\{\{(.*)\}\}/;//節點型別為元素if(node.nodeType ===1){var attr = node.attributes;//解析屬性,不同的屬性不用的處理方式,這裡只寫了v-model屬性for(var i=0;i<attr.length;i++){if (attr[i].nodeName =="v-model") {//獲取節點中v-model屬性的值,也就是繫結的屬性名var name = attr[i].nodeValue; node.addEventListener("input",function(e){//當觸發input事件時改變vue.data中相應的屬性的值,進而觸發該屬性的set方法 vm[name] = e.target.value; });//改變之後,通過屬性名取得資料 node.value = vm.data[name];//用完刪,所以瀏覽器中編譯之後的節點上沒有v-model屬性 node.removeAttribute("v-model"); } } }//節點型別為textif(node.nodeType ===3){//text是否滿足文字插值的寫法:{{任意內容}}if(reg.test(node.nodeValue)){//獲取匹配到的字串:這裡的RegExp.$1是RegExp的一個屬性//該屬性表示正則表示式reg中,第一個()裡邊的內容,也就是//{{任意內容}} 中的 文字【任意內容】 var name = RegExp.$1;//去掉前後空格,並將處理後的資料寫入節點 name = name.trim();//node.nodeValue = vm.data[name];//例項化一個新的訂閱者watchernew Watcher(vm,node,name);return; } } }//觀察者建構函式。//上邊例項化新的觀察者的時候執行這個函式:通過get()取得Vue.data中對應的資料,然後通過update()方法把資料更新到節點中。functionWatcher(vm,node,name){//讓全域性變數Dep的target屬性的指標指向該watcher例項 Dep.target =this;this.vm = vm;this.node = node;this.name = name;//放入Dep.target才能update()?????????????????????????????????????????this.update(); Dep.target =null; } // 觀察者使用update方法,實際上是 Watcher.prototype = { update: function(){this.get();this.node.nodeValue =this.value; },//獲取data中的屬性值get: function(){this.value =this.vm[this.name];//觸發相應屬性的get } }var vm =new Vue({ el:"app", data: { text:"Hello Vue" } })</script></body></html>