首頁  >  文章  >  web前端  >  實例剖析AngularJS框架中資料的雙向綁定運用

實例剖析AngularJS框架中資料的雙向綁定運用

高洛峰
高洛峰原創
2016-12-24 10:14:48935瀏覽

資料綁定

透過把一個文字輸入框綁定到person.name屬性上,就能把我們的應用變得更有趣一點。這一步驟建立起了文字輸入框跟頁面的雙向綁定。

實例剖析AngularJS框架中資料的雙向綁定運用

在這個語境裡「雙向」意味著如果view改變了屬性值,model就會「看到」這個改變,而如果model改變了屬性值,view也同樣會「看到」這個改變。 Angular.js 為你自動搭建了這個機制。如果你好奇這具體是怎麼實現的,請看我們之後推出的一篇文章,其中深入討論了digest_loop 的運作。

要建立這個綁定,我們在文字輸入框上使用ng-model 指令屬性,像這樣:

<p ng-controller="MyController">
 <input type="text" ng-model="person.name" placeholder="Enter your name" />
 <h5>Hello {{ person.name }}</h5>
</p>

現在我們建立好了一個資料綁定(沒錯,就這麼容易),來看看view怎麼改變model吧:

試試看:

實例剖析AngularJS框架中資料的雙向綁定運用

當你在文字框裡輸入時,下面的名字也自動隨之改變,這就展現了我們資料綁定的一個方向:從view到model。

我們也可以在我們的(客戶端)後台改變model,看這個改變自動在前端體現出來。要展示這個過程,讓我們在  MyController 的model裡寫一個計時器函數, 更新 $scope 上的一個資料。在下面的程式碼裡,我們就來創建這個計時器函數,它會在每秒計時(像鐘錶那樣),並更新$scope 上的clock變數資料:

app.controller(&#39;MyController&#39;, function($scope) {
 $scope.person = { name: "Ari Lerner" };
 var updateClock = function() {
  $scope.clock = new Date();
 };
 var timer = setInterval(function() {
  $scope.$apply(updateClock);
 }, 1000);
 updateClock();
});

   


可以看到,當我們改變model中clock變數的數據,view會自動更新來反映此變化。用大括號我們就可以很簡單地讓clock變數的值顯示在view裡:

<p ng-controller="MyController">
 <h5>{{ clock }}</h5>
</p>



互動

前面我們把資料綁定在了文字輸入框上。請注意, 資料綁定並非僅限於數據,我們還可以利用綁定呼叫 $scope 中的函數(這一點之前已經提到過)。

對按鈕、連結或任何其他的DOM元素,我們都可以用另一個指令屬性來實現綁定:ng-click 。這個 ng-click 指令將DOM元素的滑鼠點擊事件(即 mousedown 瀏覽器事件)綁定到一個方法上,當瀏覽器在該DOM元素上滑鼠觸發點擊事件時,此被綁定的方法就被呼叫。跟上一個例子相似,這個綁定的程式碼如下:

<p ng-controller="DemoController">
 <h4>The simplest adding machine ever</h4>
 <button ng-click="add(1)" class="button">Add</button>
 <button ng-click="subtract(1)" class="button">Subtract</button>
 <h4>Current count: {{ counter }}</h4>
</p>

   


不論是按鈕還是連結都會被綁定到包含它們的DOM元素的controller所有的$scope 物件上,當它們被滑鼠點擊,Angular就會呼叫對應的方法。注意當我們告訴Angular要呼叫什麼方法時,我們將方法名稱寫進帶引號的字串裡。

app.controller(&#39;DemoController&#39;, function($scope) {
 $scope.counter = 0;
 $scope.add = function(amount) { $scope.counter += amount; };
 $scope.subtract = function(amount) { $scope.counter -= amount; };
});

$scope.$watch

$scope.$watch( watchExp, listener, objectEquality );

   


為了監視一個變數的變化,你可以使用$scope.$watch函數。這個函數有三個參數,它指明了」要觀察什麼」(watchExp),」在變化時要發生什麼」(listener),以及你要監視的是一個變數還是一個物件。當我們在檢查一個參數時,我們可以忽略第三個參數。例如下面的例子:

$scope.name = &#39;Ryan&#39;;
 
$scope.$watch( function( ) {
  return $scope.name;
}, function( newValue, oldValue ) {
  console.log(&#39;$scope.name was updated!&#39;);
} );

   


AngularJS將會在$scope中註冊你的監視函數。你可以在控制台中輸出$scope來查看$scope中的註冊項目。

你可以在控制台中看到$scope.name已經發生了變化– 這是因為$scope.name之前的值似乎undefined而現在我們將它賦值為Ryan!

對於$wach的第一個參數,你也可以使用一個字串。這和提供一個函數完全一樣。在AngularJS的原始碼中可以看到,如果你使用了一個字串,將會運行下面的程式碼:

if (typeof watchExp == &#39;string&#39; && get.constant) {
 var originalFn = watcher.fn;
 watcher.fn = function(newVal, oldVal, scope) {
  originalFn.call(this, newVal, oldVal, scope);
  arrayRemove(array, watcher);
 };
}

   


這將會把我們的watchExp設為一個函數,它也會自動回傳作用域中我們已經制定了名字的變數。

$$watchers
$scope中的$$watchers變數保存著我們定義的所有的監視器。如果你在控制台中查看$$watchers,你會發現它是一個物件陣列。

$$watchers = [
  {
    eq: false, // 表明我们是否需要检查对象级别的相等
    fn: function( newValue, oldValue ) {}, // 这是我们提供的监听器函数
    last: &#39;Ryan&#39;, // 变量的最新值
    exp: function(){}, // 我们提供的watchExp函数
    get: function(){} // Angular&#39;s编译后的watchExp函数
  }
];

   


$watch函數將會傳回一個deregisterWatch函數。這意味著如果我們使用$scope.$watch對變數進行監視,我們也可以在以後透過呼叫某個函數來停止監視。

$scope.$apply
當一個控制器/指令/等等東西在AngularJS中運行時,AngularJS內部會運行一個叫做$scope.$apply的函數。這個$apply函數會接收一個函數作為參數並運行它,在這之後才會在rootScope上運行$digest函數。

AngularJS的$apply函數程式碼如下圖所示:

   
$apply: function(expr) {
  try {
   beginPhase(&#39;$apply&#39;);
   return this.$eval(expr);
  } catch (e) {
   $exceptionHandler(e);
  } finally {
   clearPhase();
   try {
    $rootScope.$digest();
   } catch (e) {
    $exceptionHandler(e);
    throw e;
   }
  }
}

   


上面代码中的expr参数就是你在调用$scope.$apply()时传递的参数 – 但是大多数时候你可能都不会去使用$apply这个函数,要用的时候记得给它传递一个参数。

下面我们来看看ng-keydown是怎么来使用$scope.$apply的。为了注册这个指令,AngularJS会使用下面的代码。

var ngEventDirectives = {};
forEach(
 &#39;click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste&#39;.split(&#39; &#39;),
 function(name) {
  var directiveName = directiveNormalize(&#39;ng-&#39; + name);
  ngEventDirectives[directiveName] = [&#39;$parse&#39;, function($parse) {
   return {
    compile: function($element, attr) {
     var fn = $parse(attr[directiveName]);
     return function ngEventHandler(scope, element) {
      element.on(lowercase(name), function(event) {
       scope.$apply(function() {
        fn(scope, {$event:event});
       });
      });
     };
    }
   };
  }];
 }
);

   


上面的代码做的事情是循环了不同的类型的事件,这些事件在之后可能会被触发并创建一个叫做ng-[某个事件]的新指令。在指令的compile函数中,它在元素上注册了一个事件处理器,它和指令的名字一一对应。当事件被出发时,AngularJS就会运行scope.$apply函数,并让它运行一个函数。

只是单向数据绑定吗?
上面所说的ng-keydown只能够改变和元素值相关联的$scope中的值 – 这只是单项数据绑定。这也是这个指令叫做ng-keydown的原因,只有在keydown事件被触发时,能够给与我们一个新值。

但是我们想要的是双向数据绑定!
我们现在来看一看ng-model。当你在使用ng-model时,你可以使用双向数据绑定 – 这正是我们想要的。AngularJS使用$scope.$watch(视图到模型)以及$scope.$apply(模型到视图)来实现这个功能。

ng-model会把事件处理指令(例如keydown)绑定到我们运用的输入元素上 – 这就是$scope.$apply被调用的地方!而$scope.$watch是在指令的控制器中被调用的。你可以在下面代码中看到这一点:

$scope.$watch(function ngModelWatch() {
  var value = ngModelGet($scope);
 
  //如果作用域模型值和ngModel值没有同步
  if (ctrl.$modelValue !== value) {
 
    var formatters = ctrl.$formatters,
      idx = formatters.length;
 
    ctrl.$modelValue = value;
    while(idx--) {
      value = formatters[idx](value);
    }
 
    if (ctrl.$viewValue !== value) {
      ctrl.$viewValue = value;
      ctrl.$render();
    }
  }
 
  return value;
});

   


如果你在调用$scope.$watch时只为它传递了一个参数,无论作用域中的什么东西发生了变化,这个函数都会被调用。在ng-model中,这个函数被用来检查模型和视图有没有同步,如果没有同步,它将会使用新值来更新模型数据。这个函数会返回一个新值,当它在$digest函数中运行时,我们就会知道这个值是什么!

为什么我们的监听器没有被触发?
如果我们在$scope.$watch的监听器函数中停止这个监听,即使我们更新了$scope.name,该监听器也不会被触发。

正如前面所提到的,AngularJS将会在每一个指令的控制器函数中运行$scope.$apply。如果我们查看$scope.$apply函数的代码,我们会发现它只会在控制器函数已经开始被调用之后才会运行$digest函数 – 这意味着如果我们马上停止监听,$scope.$watch函数甚至都不会被调用!但是它究竟是怎样运行的呢?

$digest函数将会在$rootScope中被$scope.$apply所调用。它将会在$rootScope中运行digest循环,然后向下遍历每一个作用域并在每个作用域上运行循环。在简单的情形中,digest循环将会触发所有位于$$watchers变量中的所有watchExp函数,将它们和最新的值进行对比,如果值不相同,就会触发监听器。

当digest循环运行时,它将会遍历所有的监听器然后再次循环,只要这次循环发现了”脏值”,循环就会继续下去。如果watchExp的值和最新的值不相同,那么这次循环就会被认为发现了脏值。理想情况下它会运行一次,如果它运行超10次,你会看到一个错误。

因此当$scope.$apply运行的时候,$digest也会运行,它将会循环遍历$$watchers,只要发现watchExp和最新的值不相等,变化触发事件监听器。在AngularJS中,只要一个模型的值可能发生变化,$scope.$apply就会运行。这就是为什么当你在AngularJS之外更新$scope时,例如在一个setTimeout函数中,你需要手动去运行$scope.$apply():这能够让AngularJS意识到它的作用域发生了变化。

创建自己的脏值检查
到此为止,我们已经可以来创建一个小巧的,简化版本的脏值检查了。当然,相比较之下,AngularJS中实现的脏值检查要更加先进一些,它提供疯了异步队列以及其他一些高级功能。

设置Scope
Scope仅仅只是一个函数,它其中包含任何我们想要存储的对象。我们可以扩展这个函数的原型对象来复制$digest和$watch。我们不需要$apply方法,因为我们不需要在作用域的上下文中执行任何函数 – 我们只需要简单的使用$digest。我们的Scope的代码如下所示:

var Scope = function( ) {
  this.$$watchers = []; 
};
 
Scope.prototype.$watch = function( ) {
 
};
 
Scope.prototype.$digest = function( ) {
 
};

   


我们的$watch函数需要接受两个参数,watchExp和listener。当$watch被调用时,我们需要将它们push进入到Scope的$$watcher数组中。

var Scope = function( ) {
  this.$$watchers = []; 
};
 
Scope.prototype.$watch = function( watchExp, listener ) {
  this.$$watchers.push( {
    watchExp: watchExp,
    listener: listener || function() {}
  } );
};
 
Scope.prototype.$digest = function( ) {
 
};

   


你可能已经注意到了,如果没有提供listener,我们会将listener设置为一个空函数 – 这样一来我们可以$watch所有的变量。

接下来我们将会创建$digest。我们需要来检查旧值是否等于新的值,如果二者不相等,监听器就会被触发。我们会一直循环这个过程,直到二者相等。这就是”脏值”的来源 – 脏值意味着新的值和旧的值不相等!

var Scope = function( ) {
  this.$$watchers = []; 
};
 
Scope.prototype.$watch = function( watchExp, listener ) {
  this.$$watchers.push( {
    watchExp: watchExp,
    listener: listener || function() {}
  } );
};
 
Scope.prototype.$digest = function( ) {
  var dirty;
 
  do {
      dirty = false;
 
      for( var i = 0; i < this.$$watchers.length; i++ ) {
        var newValue = this.$$watchers[i].watchExp(),
          oldValue = this.$$watchers[i].last;
 
        if( oldValue !== newValue ) {
          this.$$watchers[i].listener(newValue, oldValue);
 
          dirty = true;
 
          this.$$watchers[i].last = newValue;
        }
      }
  } while(dirty);
};

   


接下来,我们将创建一个作用域的实例。我们将这个实例赋值给$scope。我们接着会注册一个监听函数,在更新$scope之后运行$digest!

var Scope = function( ) {
  this.$$watchers = []; 
};
 
Scope.prototype.$watch = function( watchExp, listener ) {
  this.$$watchers.push( {
    watchExp: watchExp,
    listener: listener || function() {}
  } );
};
 
Scope.prototype.$digest = function( ) {
  var dirty;
 
  do {
      dirty = false;
 
      for( var i = 0; i < this.$$watchers.length; i++ ) {
        var newValue = this.$$watchers[i].watchExp(),
          oldValue = this.$$watchers[i].last;
 
        if( oldValue !== newValue ) {
          this.$$watchers[i].listener(newValue, oldValue);
 
          dirty = true;
 
          this.$$watchers[i].last = newValue;
        }
      }
  } while(dirty);
};
 
 
var $scope = new Scope();
 
$scope.name = 'Ryan';
 
$scope.$watch(function(){
  return $scope.name;
}, function( newValue, oldValue ) {
  console.log(newValue, oldValue);
} );
 
  
$scope.$digest();

   


成功了!我们现在已经实现了脏值检查(虽然这是最简单的形式)!上述代码将会在控制台中输出下面的内容:

Ryan undefined

   


这正是我们想要的结果 – $scope.name之前的值是undefined,而现在的值是Ryan。

现在我们把$digest函数绑定到一个input元素的keyup事件上。这就意味着我们不需要自己去调用$digest。这也意味着我们现在可以实现双向数据绑定!

var Scope = function( ) {
  this.$$watchers = []; 
};
 
Scope.prototype.$watch = function( watchExp, listener ) {
  this.$$watchers.push( {
    watchExp: watchExp,
    listener: listener || function() {}
  } );
};
 
Scope.prototype.$digest = function( ) {
  var dirty;
 
  do {
      dirty = false;
 
      for( var i = 0; i < this.$$watchers.length; i++ ) {
        var newValue = this.$$watchers[i].watchExp(),
          oldValue = this.$$watchers[i].last;
 
        if( oldValue !== newValue ) {
          this.$$watchers[i].listener(newValue, oldValue);
 
          dirty = true;
 
          this.$$watchers[i].last = newValue;
        }
      }
  } while(dirty);
};
 
 
var $scope = new Scope();
 
$scope.name = 'Ryan';
 
var element = document.querySelectorAll('input');
 
element[0].onkeyup = function() {
  $scope.name = element[0].value;
 
  $scope.$digest();
};
 
$scope.$watch(function(){
  return $scope.name;
}, function( newValue, oldValue ) {
  console.log('Input value updated - it is now ' + newValue);
 
  element[0].value = $scope.name;
} );
 
var updateScopeValue = function updateScopeValue( ) {
  $scope.name = 'Bob';
  $scope.$digest();
};

   


使用上面的代码,无论何时我们改变了input的值,$scope中的name属性都会相应的发生变化。这就是隐藏在AngularJS神秘外衣之下数据双向绑定的秘密!

更多實例剖析AngularJS框架中資料的雙向綁定運用相关文章请关注PHP中文网!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn