用戶最滿意的,無非就是界面的操做能實事反應到數據。而實現這種的能夠有雙向數據綁定、單向數據流的形式。雙向數據綁定是,ui行爲改變model層的數據,model層的數據變了也能反映到ui上面。好比點擊按鈕,數字data+1,若是咱們本身在控制檯再給data+1,那麼v層也能立刻看見這個變化。而單向數據流就不一樣了,咱們只有ui行爲改變,data就改變並立刻反饋到v層,而咱們本身在控制檯改變data這個值,v層竟然不變(model是已經變了並無反應),只能等到下一次ui行爲改變,帶上這個data結果一塊兒處理。僅僅在V層的單向數據,真的能知足用戶需求?數據很龐大的時候,雙綁性能如何?其實,每一種都有每一種的適用場景,仍是那句話,脫離實際場景談性能,就是扯淡javascript
通常的過程:ui行爲→觸發action→改變數據state→mumtation再次渲染ui界面,一般就是基於view層,一個很簡單的例子:
html部分:html
<input id="ipt" type="text" name=""> <p id="a"></p>
js部分:vue
var str = '' a.innerHTML = str//初始化 ipt.oninput = function(){//點擊觸發action str = ipt.value//改變state狀態值 a.innerHTML = str//從新渲染 }
可是若是在控制檯獲取input這個dom,在設置value,不會立刻反映,只能等下一次帶着這個結果一塊兒做用。這僅僅是V->M的過程java
咱們再作一個超級簡單的雙綁:
html部分:react
<input id="ipt" type="text" name=""> <p id="a"></p>
js部分:git
var $scope = { data:'' } a.innerHTML = '' setInterval(function(){ a.innerHTML = $scope.data },60) ipt.oninput = function(){ $scope.data = ipt.value }
這裏除了單向數據綁定,當你改變$scope.data,p標籤的內容也是會立刻改變。由於用了定時器,他會異步地將數據反映上去。github
首先,咱們先訂閱事件,好比事件‘a’,回調函數是function (){console.log(1)},訂閱後,若是事件‘a’被觸發了,就調用回調函數。ajax
function Event(){ this.list=[], this.on=function(key,cb){//訂閱事件 if(!this.list[key]){ this.list[key] = [] } this.list[key].push(cb) }, this.emit = function(){//觸發事件 var key = Array.prototype.shift.call(arguments) var e = this.list[key] if(!e){ return } var args = Array.prototype.slice.call(arguments) for(var i = 0;i<e.length;i++){ e[i].apply(null,args) } } }
嘗試一下:數組
var a = new Event() a.on('a',function(x){console.log(x)}) a.emit('a',1)//1
這樣子,在1中單向數據的小例子,首先咱們on裏面加入事件a,回調是a.innerHTML = str,而後咱們能夠在改變model層的時候,順便觸發一下(emit(‘a’)),不就能夠作到M->V的反映了嗎?瀏覽器
對的,是行得通,但是這都是死的,也不能自動讓他雙向數據綁定,因此咱們借用js底層的Object.defineproperty。
在第二篇文章已經講過,這裏再重複一次:
var obj = {name:'pp'} console.log(obj.name)//pp Object.defineProperty(obj,'name',{ get:function(){ return 1 }, set:function(newVal){ console.log(newVal) } }) console.log(obj.name)//1 obj.name = 2;//2 console.log(obj.name)//1
這是vue雙綁的核心思想,v層能讓m層變了,m層也能讓v層變了,只是不能互相關聯起來,不能作到改變一個層另外一個層也能改變。可是,如今就能夠了。
html部分:
<input id="ipt" type="text" name=""> <p id="a"></p>
//js: var data = { str:'' } a.innerHTML = data.str//初始化 function E (){ this.list=[], this.on=function(key,cb){//訂閱事件 if(!this.list[key]){ this.list[key] = [] } this.list[key].push(cb) }, this.emit = function(){//觸發事件 var key = Array.prototype.shift.call(arguments) var e = this.list[key] if(!e){ return } var args = Array.prototype.slice.call(arguments) for(var i = 0;i<e.length;i++){ e[i].apply(null,args) } } } var e = new E()//實例化 e.on('change',function(x){//訂閱change這個事件 a.innerHTML = x }) Object.defineProperty(data,'str',{ set:function(newval){//當data.str被設置的時候,觸發事件change e.emit('change',newval) return newval } }) ipt.oninput = function(){ data.str = ipt.value//用戶的action }
這下,不只僅是有改變input的內容的單向的數據綁定,並且你還能夠去控制檯改變data.str=1,p標籤的內容立刻變成1,實現了雙向數據綁定。
咱們的例子其實不用觀察者模式均可以實現雙綁,可是在實際應用中確定也不能夠不用觀察者模式,爲了代碼可讀性和可維護性以及拓展性。具體的v-model實如今前面文章已經講過
到這裏,你大概比較深刻理解雙向數據綁定是什麼了。網上有不少人有vue雙綁demo,可是他們有一部分是僅僅單向綁定的,不妨手動去控制檯改一下那個核心綁定的數據,V層的顯示內容能立刻變化的就是雙綁、不能立刻有變化的只是單向數據
前面特意埋了個坑,關於Angular髒檢查,並非一些人想象的那樣子用定時器週期性進行髒檢測(我前面寫的那個超級簡單的雙綁就是人們傳聞的angular)
只有當UI事件,ajax請求或者 timeout 等異步事件,纔會觸發髒檢查。而咱們前面的vue,當咱們在控制檯改了數據,就能夠立刻反映到v層。angular並無這個操做,也沒有意義。由於雙綁的M->V通常就是基於ui行爲、定時器、ajax這些異步動做,因此這就知道爲何ng-model只能對錶單有效了。想作到像vue那樣的極致雙綁,可以在控制檯改個數據就改變視圖的,大概就只有defineproperty和定時器輪詢了吧。
在angular1中,私有變量以$$開頭,$$watch是一個存放不少個綁定的對象的數組,用$watch
方法來添加的,每個被綁定的對象屬性是:變量名、變量舊值、一個函數(用來返回變量新值)、檢測變化的回調函數。
對於爲何使用一個函數來記錄新值(相似vue的computed)?這樣子能夠每次調用都獲得數據上最新的值,若是把這個值寫死,不就是不會變化了嗎?這是監控函數的通常形式:從做用域獲取值再返回。
接着咱們對$scope
的非函數數據進行綁定,再到 核心的$digest
循環,對於每個$$watch
裏面的每個watch,咱們使用 getNewValue()
而且把scope實例 傳遞進去,獲得數據最新值。而後和上一次值進行比較,若是不一樣,那就調用 getListener,同時把新值和舊值一併傳遞進去。 最終,咱們把last屬性設置爲新返回的值,也就是最新值。$digest
裏會調用每一個getNewValue(),所以,最好關注監聽器的數量,還有每一個獨立的監控函數或者表達式的性能。
在做用域上添加數據自己不會有性能問題。若是沒有監聽器在監控某個屬性,它在不在做用域上都無所謂。$digest
並不會遍歷做用域的屬性,它遍歷的是監聽器。一旦將數據綁定到UI上,就會添加一個監聽器。
最後,咱們須要將新的變量值更新到DOM上,只要加上ng的指令,並解釋,觸發$digest
循環便可
html:
<input type="text" ng-bind="s" /> <div ng-bind="s"></div>
js:
function Scope(){ this.$$watchers=[]; //監聽器 } Scope.prototype.$watch=function(name,exp,listener){ this.$$watchers.push({ name:name, //數據變量名 last:'', //數據變量舊值 newVal:exp, //返回數據變量新值的函數 listener:listener || function(){} //監聽回調函數,變量「髒」時觸發 }) } Scope.prototype.$digest=function(){ var bindList = document.querySelectorAll("[ng-bind]"); //獲取全部含ng-bind的DOM節點 var dirty=true; while(dirty){ dirty=false; for(var i=0;i<this.$$watchers.length;i++){ var newVal=this.$$watchers[i].newVal(); var oldVal=this.$$watchers[i].last; if(newVal!==oldVal && !isNaN(newVal) && !isNaN(oldVal)){ dirty=true; this.$$watchers[i].listener(oldVal,newVal); this.$$watchers[i].last=newVal; for (var j = 0; j < bindList.length; j++) { //獲取DOM上的數據變量的名稱 var modelName=bindList[j].getAttribute("ng-bind"); //數據變量名相同的DOM才更新 if(modelName==this.$$watchers[i].name) { if (bindList[j].tagName == "INPUT") { //更新input的輸入值 bindList[j].value = this[modelName]; } else { //更新非input的值 bindList[j].innerHTML = this[modelName]; } } } } } } }; var $scope=new Scope(); $scope.count=0; var inputList=document.querySelectorAll("input[ng-bind]"); for(var i=0;i<inputList.length;i++){ inputList[i].addEventListener("input",(function(index){ return function(){ $scope[inputList[index].getAttribute("ng-bind")]=inputList[index].value; $scope.$digest(); //調用函數時觸發$digest } })(i)); } //綁定非函數數據 for(var key in $scope){ if(key!="$$watchers" && typeof $scope[key]!="function") { $scope.$watch(key, (function (index) { return function(){ return $scope[index]; } })(key)) } } $scope.$digest();//第一次digest
固然,還會有一個問題,當有兩個$watch
循環監聽(watch1
監聽watch2
,watch2
監聽watch1
),一個$digest
循環執行不少次,並且是多餘操做(而且可能把瀏覽器炸了)。
var scope = new $scope(); scope.a = 5; scope.b = 1; scope.$watch('a', function(scope) { return scope[this.name] }, function(newValue, oldValue) { scope.b ++; }) scope.$watch('b', function(scope) { return scope[this.name] }, function(newValue, oldValue) { scope.a ++; })
angular有一個概念叫迭代的最大值:TTL(short for Time To Live)。這個值默認是10。由於digest常常被執行,並且每一個digest運行了全部的$watch,再加上用戶通常不會建立10個以上鍊狀的監聽器。
angular的處理辦法是
$scope.prototype.$digest = function() { var dirty = true; var checkTimes = 0; while(dirty) { dirty = this.$$digestOnce(); checkTimes++; if(checkTimes>10 &&dirty){ throw new Error(); } }; };
對於雙綁,若是是大循環,循環改變一個值,vue的setter這種即時性的雙綁就會在每一次循環都跑一次,而angular1的髒檢測這種慢性雙綁你能夠控制在循環後才一次跑一次,性能取捨就看實際場景吧。
單向數據流,你得按照他的順序辦事。好比咱們假設有一個這樣的生命週期:1.從data裏面讀取數據2.ui行爲(若是沒有ui行爲就停在這裏等他有了爲止)3.觸發data更新4.再回到步驟1
改了一個數,v層不能反回頭來找他來更新v層視圖(從步驟2跳回去1),你得等下一個循環(轉了一圈)的步驟1才能更新視圖。react都是這樣子,你得setState觸發更新,若是你this.state = {...},是沒用的,他一直不變。
單向數據綁定,就是綁定事件,好比綁定oninput、onchange、storage這些事件,只要觸發事件,馬上執行對應的函數。因此,不要再說一個input綁一個oninput,而後回調改變一個視圖層數據就叫他雙向數據綁定了。