双向数据绑定是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);
}
}