Home >Web Front-end >JS Tutorial >Example analysis of two-way binding application of data in AngularJS framework_AngularJS

Example analysis of two-way binding application of data in AngularJS framework_AngularJS

WBOY
WBOYOriginal
2016-05-16 15:12:151399browse

資料綁定

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

201634144020971.png (184×293)

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

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

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

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

試試看:

201634144103466.jpg (321×88)

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

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

app.controller('MyController', 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裡:

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


互動

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

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

<div 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>
</div>

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

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

 請看:

201634144205361.jpg (448×176)

$scope.$watch

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

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

$scope.name = 'Ryan';

$scope.$watch( function( ) {
  return $scope.name;
}, function( newValue, oldValue ) {
  console.log('$scope.name was updated!');
} );

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

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

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

if (typeof watchExp == 'string' && 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: 'Ryan', // 变量的最新值
    exp: function(){}, // 我们提供的watchExp函数
    get: function(){} // Angular's编译后的watchExp函数
  }
];

$watch函数将会返回一个deregisterWatch函数。这意味着如果我们使用$scope.$watch对一个变量进行监视,我们也可以在以后通过调用某个函数来停止监视。

$scope.$apply
当一个控制器/指令/等等东西在AngularJS中运行时,AngularJS内部会运行一个叫做$scope.$apply的函数。这个$apply函数会接收一个函数作为参数并运行它,在这之后才会在rootScope上运行$digest函数。

AngularJS的$apply函数代码如下所示:

$apply: function(expr) {
  try {
   beginPhase('$apply');
   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(
 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
 function(name) {
  var directiveName = directiveNormalize('ng-' + name);
  ngEventDirectives[directiveName] = ['$parse', 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神秘外衣之下数据双向绑定的秘密!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn