雙向數據綁定是angularJS的核心理念之一。
單向數據綁定:
單向數據即是將用於生成界面的模板與從服務器取得的數據結合,生成用於顯示的html標籤。
比如El表達式中常見 ${變量名}以及{{}} ,它只提供從數據源到視圖的單方向的數據展示。
單向數據綁定的缺點是界面一旦生成,就不能更改,如果數據有更改,只能再來一遍,替換掉原來的html。
再看雙向數據綁定:
雙向數據綁定中,視圖和數據是對應的,一方發生變化, 另一方立刻跟着變化。
雙向數據綁定最大的好處就是當數據改變後,不需要在代碼中手動更新視圖,簡化開發,增加代碼內聚性,代碼邏輯、可讀性更強。
缺點是雙向數據綁定完全契合的場景比較少,當綁定的數據層次深、數據量大時,實現雙向數據綁定會有一定性能開銷。
angular雙向數據綁定的性能問題與angular實現雙向數據綁定的機制有關。
angular雙向數據綁定的實現
angular實現雙向數據綁定使用了髒檢查機制:
髒檢查機制:Angular將雙向綁定轉換爲一堆watch表達式,然後遞歸這些表達式檢查是否發生過變化,如果變了則執行相應的watcher函數(指view上的指令,如ng-bind,ng-show等或是{{}})。等到model中的值不再發生變化,也就不會再有watcher被觸發,一個完整的循環就完成了。
髒檢查機制的觸發:Angular中在view上聲明的事件指令,如:ng-click、ng-change等,會將瀏覽器的事件轉發給$scope上相應的model的響應函數。等待相應函數改變model,緊接着觸發髒檢查機制刷新view。
watch表達式:可以是一個函數、可以是$scope上的一個屬性名,也可以是一個字符串形式的表達式。$watch函數所監聽的對象叫做watch表達式。
watcher函數:指在view上的指令(ngBind,ngShow、ngHide等)以及{{}}表達式,他們所註冊的函數。每一個watcher對象都包括:監聽函數,上次變化的值,獲取監聽表達式的方法以及監聽表達式,最後還包括是否需要使用深度對比(angular.equals())。
$watch(watchFn,watchAction,deepWatch)
watchFu:angular表達式或函數的字符串;
watchAction(newValue,oldValue,scope):watchFu發生改變會調用;
deepWatch:true/false,是否深度檢查
$watch會返回一個函數,該函數可以註銷watcher。
var watcher = $scope.$watch('data',function(){},true);
//註銷
watcher();
手動觸發髒檢查:
$scope.$digest();
或者:$scope.$apply() //實際上也是調用$digest()
在$digest循環過程中,angular會遍歷每一個watcher,詢問它是否有屬性和值的變化(變髒)。
如果在$digest循環中,watcher中的回調修改了屬性和值怎麼辦?
實際上,$digest循環會循環觸發,直到所有的值都不在改變,所以$digest循環至少會執行兩次,但最多10次,之後則會報錯。當$digest循環結束,修改DOM。
仿寫angular的雙向數據綁定:
class Scope{
construct(){
this.nodes =[]; //綁定的節點
this.watchers = []; //model數據的觀察者
}
/**
* 更新view中綁定節點的值
*/
update(newValue){
let {nodes} = this;
const INPUT_NODE = ['INPUT','TEXTAREA']; //節點是否可輸入
for(let i=0,node;node=nodes[i++];){
if(INPUT_NODE.includes(node.nodeName)){
if(node.value !== newValue){
node.value = newValue;
}
}else{
node.textContent = newValue;
}
}
}
/**
* 爲model中的值新增watcher
* @param statement
* @param listener
*/
watch(statement,listener = function () {}){
this.watchers.push({
statement,
listener
})
}
/**
* 節點與數據綁定
* @param node
*/
bindNode(node){
let key = node.getAttribute('ng-model');
if(!key){
return;
}
this.nodes.push(node);
//綁定新節點直接更新一次數據,使各節點數據一致
this.update(this[key]);
this.watch(()=>{
return this[key];
},(newValue,oldValue)=>{
this.update(newValue);
})
}
/**
* 觸發髒檢查
*/
$digest(){
let {watchers} = this;
//如果髒檢查中更新了數據,需要再循環一次
let dirty = false;
do {
dirty = false;
for (let i = 0, watcher; watcher = watchers[i++];) {
let newValue = watcher.statement();
if (watcher.last !== newValue) {
dirty = true;
watcher.listener(newValue, watcher.last);
watcher.last = newValue;
}
}
}while(dirty);
}
}