ホームページ  >  記事  >  ウェブフロントエンド  >  AngularJS のデータの双方向バインディング メカニズムについて詳しく学ぶ

AngularJS のデータの双方向バインディング メカニズムについて詳しく学ぶ

高洛峰
高洛峰オリジナル
2016-12-24 10:18:39904ブラウズ

Angular JS (Angular.JS) は、Web ページの開発に使用されるフレームワーク、テンプレート、データ バインディング、および豊富な UI コンポーネントのセットです。開発プロセス全体をサポートし、手動による DOM 操作を必要としない Web アプリケーションのアーキテクチャを提供します。 AngularJS は小さく、わずか 60K で、主流のブラウザと互換性があり、jQuery とうまく連携します。双方向データ バインディングは、AngularJS の最もクールで最も実用的な機能であり、MVC の原理を完全に示しています。AngularJS の動作原理は、HTML テンプレートがブラウザーによって DOM に解析され、DOM 構造が次のようになります。 AngularJS コンパイラの入力。 AngularJS は DOM テンプレートを走査して、対応する NG 命令を生成します。すべての命令は、ビュー (つまり、HTML の ng-model) のデータ バインディングを設定します。したがって、NG フレームワークは、DOM がロードされた後にのみ機能し始めます。

HTML の場合:

<body ng-app="ngApp">
 <div ng-controller="ngCtl">
  <label ng-model="myLabel"></label>
  <input type="text" ng-model="myInput" />
  <button ng-model="myButton" ng-click="btnClicked"></button>
 </div>
</body>


js の場合:

// angular app
var app = angular.module("ngApp", [], function(){
 console.log("ng-app : ngApp");
});
// angular controller
app.controller("ngCtl", [ &#39;$scope&#39;, function($scope){
 console.log("ng-controller : ngCtl");
 $scope.myLabel = "text for label";
 $scope.myInput = "text for input";
 $scope.btnClicked = function() {
  console.log("Label is " + $scope.myLabel);
 }
}]);

アプリ内の angular コントローラーの場合、コントローラーはスコープに対応します ($scope プレフィックスを使用してスコープ内のプロパティとメソッドを指定できます)。ngCtl のスコープ内の HTML タグの値は または Operations になります。 $scope を介して js のプロパティとメソッドにバインドされます。このようにして、NG の双方向のデータ バインディングが実現されます。つまり、HTML で表示されるビューは AngularJS のデータと一致し、対応するもう一方のエンドも同様になります。

このメソッドは非常に使いやすく、js とメソッドの angular コントローラー スコープにバインドされた HTML タグのスタイルとそれに対応する属性のみを考慮するだけで、すべての複雑な DOM 操作が省略されます。

この種の考え方は、実際には jQuery の DOM クエリと操作とはまったく異なるため、AngularJS を使用する場合は混合しないことを推奨しています。もちろん、どちらを使用するかは、好みによって異なります。 NG のアプリはモジュールに相当します。各アプリには複数のコントローラーが定義されており、それぞれのスコープ空間は相互に干渉しません。 AngularJS がそのような落とし穴に陥る可能性があります。

var app = angular.module("test", []);
 
app.directive("myclick", function() {
  return function (scope, element, attr) {
    element.on("click", function() {
      scope.counter++;
    });
  };
});
 
app.controller("CounterCtrl", function($scope) {
  $scope.counter = 0;
});
<body ng-app="test">
  <div ng-controller="CounterCtrl">
    <button myclick>increase</button>
    <span ng-bind="counter"></span>
  </div>
</body>

このとき、ボタンをクリックしても、インターフェース上の数字は増加しません。デバッガーを確認すると、確かにデータが増加していることがわかり、多くの人は混乱するでしょう。なぜ Angular はデータが変更されたときにインターフェイスが更新されないのでしょうか。


scope.counter++; の後にscope.digest(); を追加してみてください。

なぜこれを行う必要があるのですか?どのような状況でこれを行う必要がありますか?最初の例にはダイジェストがないことがわかりました。ダイジェストを記述すると、例外がスローされ、他のダイジェストが作成されていることがわかります。何が起こっているのでしょうか?

まず考えてみましょう。AngularJS を使わずにそのような関数を自分で実装したい場合はどうなるでしょうか?

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>two-way binding</title>
  </head>
  <body onload="init()">
    <button ng-click="inc">
      increase 1
    </button>
    <button ng-click="inc2">
      increase 2
    </button>
    <span style="color:red" ng-bind="counter"></span>
    <span style="color:blue" ng-bind="counter"></span>
    <span style="color:green" ng-bind="counter"></span>
 
    <script type="text/javascript">
      /* 数据模型区开始 */
      var counter = 0;
 
      function inc() {
        counter++;
      }
 
      function inc2() {
        counter+=2;
      }
      /* 数据模型区结束 */
 
      /* 绑定关系区开始 */
      function init() {
        bind();
      }
 
      function bind() {
        var list = document.querySelectorAll("[ng-click]");
        for (var i=0; i<list.length; i++) {
          list[i].onclick = (function(index) {
            return function() {
              window[list[index].getAttribute("ng-click")]();
              apply();
            };
          })(i);
        }
      }
 
      function apply() {
        var list = document.querySelectorAll("[ng-bind=&#39;counter&#39;]");
        for (var i=0; i<list.length; i++) {
          list[i].innerHTML = counter;
        }
      }
      /* 绑定关系区结束 */
    </script>
  </body>
</html>

ご覧のとおり、このような単純な例では、双方向のバインディングが行われています。 2つのボタンのクリックからデータの変更まではわかりやすいですが、DOMのonclickメソッドを直接使うのではなく、ng-clickを作成し、そのngに対応する関数を取り出しています。バインド内の -click onclick イベント ハンドラー関数にバインドします。なぜこのようにしなければならないのでしょうか?データは変更されましたが、まだインターフェイスに入力されていないため、ここで追加の操作を行う必要があります。

別の側面から見ると、データが変更されると、この変更はインターフェース、つまり 3 つのスパンに適用される必要があります。ただし、Angular はダーティ検出を使用するため、データを変更した後、自分で何らかの操作を行ってダーティ検出をトリガーし、それをデータに対応する DOM 要素に適用する必要があります。問題は、ダーティ検出をどのようにトリガーするかということです。それはいつトリガーされますか?

一部のセッターベースのフレームワークでは、データの値を設定するときに DOM 要素にバインドされた変数を再割り当てできることがわかっています。ダーティ検出メカニズムにはこの段階がありません。データ変更の直後に通知を受ける方法がないため、各イベント エントリで apply() を手動で呼び出して、データ変更をインターフェイスに適用することしかできません。実際の Angular 実装では、まずダーティ検出が実行されてデータが変更されたかどうかが判断され、次にインターフェイス値が設定されます。

そこで、実際のクリックを ng-click にカプセル化します。最も重要な関数は、データの変更をインターフェースに適用するために後で apply() を追加することです。

では、なぜ ng-click で $digest を呼び出すとエラーが表示されるのでしょうか? Angular の設計により、同時に実行できる $digest は 1 つだけであり、組み込み命令 ng-click がすでに $digest をトリガーしており、現在の命令がまだ終了していないため、エラーが発生しました。

$digest と $apply

Angular には、$apply と $digest という 2 つの関数があります。$digest を使用してこのデータをインターフェイスに適用しました。ただし、現時点では、$digest の代わりに $apply を使用することもできます。では、それらの違いは何でしょうか。

最直接的差异是,$apply可以带参数,它可以接受一个函数,然后在应用数据之后,调用这个函数。所以,一般在集成非Angular框架的代码时,可以把代码写在这个里面调用。

var app = angular.module("test", []);
 
app.directive("myclick", function() {
  return function (scope, element, attr) {
    element.on("click", function() {
      scope.counter++;
      scope.$apply(function() {
        scope.counter++;
      });
    });
  };
});
 
app.controller("CounterCtrl", function($scope) {
  $scope.counter = 0;
});

   


除此之外,还有别的区别吗?

在简单的数据模型中,这两者没有本质差别,但是当有层次结构的时候,就不一样了。考虑到有两层作用域,我们可以在父作用域上调用这两个函数,也可以在子作用域上调用,这个时候就能看到差别了。

对于$digest来说,在父作用域和子作用域上调用是有差别的,但是,对于$apply来说,这两者一样。我们来构造一个特殊的示例:

var app = angular.module("test", []);
 
app.directive("increasea", function() {
  return function (scope, element, attr) {
    element.on("click", function() {
      scope.a++;
      scope.$digest();
    });
  };
});
 
app.directive("increaseb", function() {
  return function (scope, element, attr) {
    element.on("click", function() {
      scope.b++;
      scope.$digest();  //这个换成$apply即可
    });
  };
});
 
app.controller("OuterCtrl", ["$scope", function($scope) {
  $scope.a = 1;
 
  $scope.$watch("a", function(newVal) {
    console.log("a:" + newVal);
  });
 
  $scope.$on("test", function(evt) {
    $scope.a++;
  });
}]);
 
app.controller("InnerCtrl", ["$scope", function($scope) {
  $scope.b = 2;
 
  $scope.$watch("b", function(newVal) {
    console.log("b:" + newVal);
    $scope.$emit("test", newVal);
  });
}]);
<div ng-app="test">
  <div ng-controller="OuterCtrl">
    <div ng-controller="InnerCtrl">
      <button increaseb>increase b</button>
      <span ng-bind="b"></span>
    </div>
    <button increasea>increase a</button>
    <span ng-bind="a"></span>
  </div>
</div>

   


这时候,我们就能看出差别了,在increase b按钮上点击,这时候,a跟b的值其实都已经变化了,但是界面上的a没有更新,直到点击一次increase a,这时候刚才对a的累加才会一次更新上来。怎么解决这个问题呢?只需在increaseb这个指令的实现中,把$digest换成$apply即可。

当调用$digest的时候,只触发当前作用域和它的子作用域上的监控,但是当调用$apply的时候,会触发作用域树上的所有监控。

因此,从性能上讲,如果能确定自己作的这个数据变更所造成的影响范围,应当尽量调用$digest,只有当无法精确知道数据变更造成的影响范围时,才去用$apply,很暴力地遍历整个作用域树,调用其中所有的监控。

从另外一个角度,我们也可以看到,为什么调用外部框架的时候,是推荐放在$apply中,因为只有这个地方才是对所有数据变更都应用的地方,如果用$digest,有可能临时丢失数据变更。

脏检测的利弊
很多人对Angular的脏检测机制感到不屑,推崇基于setter,getter的观测机制,在我看来,这只是同一个事情的不同实现方式,并没有谁完全胜过谁,两者是各有优劣的。

大家都知道,在循环中批量添加DOM元素的时候,会推荐使用DocumentFragment,为什么呢,因为如果每次都对DOM产生变更,它都要修改DOM树的结构,性能影响大,如果我们能先在文档碎片中把DOM结构创建好,然后整体添加到主文档中,这个DOM树的变更就会一次完成,性能会提高很多。

同理,在Angular框架里,考虑到这样的场景:

function TestCtrl($scope) {
  $scope.numOfCheckedItems = 0;
 
  var list = [];
 
  for (var i=0; i<10000; i++) {
    list.push({
      index: i,
      checked: false
    });
  }
 
  $scope.list = list;
 
  $scope.toggleChecked = function(flag) {
    for (var i=0; i<list.length; i++) {
      list[i].checked = flag;
      $scope.numOfCheckedItems++;
    }
  };
}

如果界面上某个文本绑定这个numOfCheckedItems,会怎样?在脏检测的机制下,这个过程毫无压力,一次做完所有数据变更,然后整体应用到界面上。这时候,基于setter的机制就惨了,除非它也是像Angular这样把批量操作延时到一次更新,否则性能会更低。

所以说,两种不同的监控方式,各有其优缺点,最好的办法是了解各自使用方式的差异,考虑出它们性能的差异所在,在不同的业务场景中,避开最容易造成性能瓶颈的用法。

更多深入学习AngularJS中数据的双向绑定机制相关文章请关注PHP中文网!


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。