1. 程式人生 > >AngularJs資料繫結原理

AngularJs資料繫結原理

此文為原創翻譯,轉載請註明出處!

這篇博文主要是寫給新手的,是給那些剛剛開始接觸Angular,並且想了解資料幫定是如何工作的人。如果你已經對Angular比較瞭解了,那強烈建議你直接去閱讀原始碼。

Angular使用者都想知道資料繫結是怎麼實現的。你可能會看到各種各樣的詞彙:$watch,$apply,$digest,dirty-checking...它們是什麼?它們是如何工作的呢?這裡我想回答這些問題,其實它們在官方的文件裡都已經回答了,但是我還是想把它們結合在一起來講,但是我只是用一種簡單的方法來講解,如果要想了解技術細節,檢視原始碼。

讓我們從頭開始吧。

瀏覽器事件迴圈和Angular.js擴充套件

我們的瀏覽器一直在等待事件,比如使用者互動。假如你點選一個按鈕或者在輸入框裡輸入東西,事件的回撥函式就會在javascript直譯器裡執行,然後你就可以做任何DOM操作,等回撥函式執行完畢時,瀏覽器就會相應地對DOM做出變化。 Angular拓展了這個事件迴圈,生成一個有時成為angular context的執行環境(記住,這是個重要的概念),為了解釋什麼是context以及它如何工作,我們還需要解釋更多的概念。

$watch 佇列($watch list)

每次你繫結一些東西到你的UI上時你就會往$watch佇列裡插入一條$watch。想象一下$watch就是那個可以檢測它監視的model裡時候有變化的東西。例如你有如下的程式碼

index.html

User:<input type="text" ng-model="user"/>Password:<input type="password" ng-model="pass"/>

在這裡我們有個$scope.user,他被繫結在了第一個輸入框上,還有個$scope.pass,它被繫結在了第二個輸入框上,然後我們在$watch list裡面加入兩個$watch:

controllers.js

app.controller('MainCtrl',function($scope){
  $scope.foo ="Foo";
  $scope.world 
="World";});

index.html

Hello,{{World}}

這裡,即便我們在$scope上添加了兩個東西,但是隻有一個繫結在了UI上,因此在這裡只生成了一個$watch. 再看下面的例子: controllers.js

app.controller('MainCtrl',function($scope){
  $scope.people =[...];});

index.html

<ul><ling-repeat="person in people">
      {{person.name}} - {{person.age}}
  </li></ul>

這裡又生成了多少個$watch呢?每個person有兩個(一個name,一個age),然後ng-repeat又有一個,因此10個person一共是(2 * 10) +1,也就是說有21個$watch。 因此,每一個繫結到了UI上的資料都會生成一個$watch。對,那這寫$watch是什麼時候生成的呢? 當我們的模版載入完畢時,也就是在linking階段(Angular分為compile階段和linking階段---譯者注),Angular直譯器會尋找每個directive,然後生成每個需要的$watch。聽起來不錯哈,但是,然後呢?

$digest迴圈(這個digest不知道怎麼翻譯)

還記得我前面提到的擴充套件的事件迴圈嗎?當瀏覽器接收到可以被angular context處理的事件時,$digest迴圈就會觸發。這個迴圈是由兩個更小的迴圈組合起來的。一個處理evalAsync佇列,另一個處理$watch佇列,這個也是本篇博文的主題。 這個是處理什麼的呢?$digest將會遍歷我們的$watch,然後詢問:

  • 嘿,$watch,你的值是什麼?
    • 是9。
  • 好的,它改變過嗎?
    • 沒有,先生。
  • (這個變數沒變過,那下一個)
  • 你呢,你的值是多少?
    • 報告,是Foo
  • 剛才改變過沒?
    • 改變過,剛才是Bar
  • (很好,我們有DOM需要更新了)
  • 繼續詢問知道$watch佇列都檢查過。

這就是所謂的dirty-checking。既然所有的$watch都檢查完了,那就要問了:有沒有$watch更新過?如果有至少一個更新過,這個迴圈就會再次觸發,直到所有的$watch都沒有變化。這樣就能夠保證每個model都已經不會再變化。記住如果迴圈超過10次的話,它將會丟擲一個異常,防止無限迴圈。 當$digest迴圈結束時,DOM相應地變化。

例如: controllers.js

app.controller('MainCtrl',function(){
  $scope.name ="Foo";

  $scope.changeFoo =function(){
      $scope.name ="Bar";}});

index.html

{{ name }}<button ng-click="changeFoo()">Change the name</button>

這裡我們有一個$watch因為ng-click不生成$watch(函式是不會變的)。

  • 我們按下按鈕
  • 瀏覽器接收到一個事件,進入angular context(後面會解釋為什麼)。
  • $digest迴圈開始執行,查詢每個$watch是否變化。
  • 由於監視$scope.name$watch報告了變化,它會強制再執行一次$digest迴圈。
  • 新的$digest迴圈沒有檢測到變化。
  • 瀏覽器拿回控制權,更新與$scope.name新值相應部分的DOM。

這裡很重要的(也是許多人的很蛋疼的地方)是每一個進入angular context的事件都會執行一個$digest迴圈,也就是說每次我們輸入一個字母迴圈都會檢查整個頁面的所有$watch

通過$apply來進入angular context

誰決定什麼事件進入angular context,而哪些又不進入呢?$apply

如果當事件觸發時,你呼叫$apply,它會進入angular context,如果沒有呼叫就不會進入。現在你可能會問:剛才的例子裡我也沒有呼叫$apply啊,為什麼?Angular為了做了!因此你點選帶有ng-click的元素時,時間就會被封裝到一個$apply呼叫。如果你有一個ng-model="foo"的輸入框,然後你敲一個f,事件就會這樣呼叫$apply("foo = 'f';")

Angular什麼時候不會自動為我們$apply呢?

這是Angular新手共同的痛處。為什麼我的jQuery不會更新我繫結的東西呢?因為jQuery沒有呼叫$apply,事件沒有進入angular context$digest迴圈永遠沒有執行。

我們來看一個有趣的例子:

假設我們有下面這個directive和controller

app.js

app.directive('clickable',function(){return{
  restrict:"E",
  scope:{
    foo:'=',
    bar:'='},template:'<ul style="background-color: lightblue"><li>{{foo}}</li><li>{{bar}}</li></ul>',
  link:function(scope, element, attrs){
    element.bind('click',function(){
      scope.foo++;
      scope.bar++;});}}});

app.controller('MainCtrl',function($scope){
  $scope.foo =0;
  $scope.bar =0;});

它將foobar從controller裡繫結到一個list裡面,每次點選這個元素的時候,foobar都會自增1。

那我們點選元素的時候會發生什麼呢?我們能看到更新嗎?答案是否定的。因為點選事件是一個沒有封裝到$apply裡面的常見的事件,這意味著我們會失去我們的計數嗎?不會

真正的結果是:$scope確實改變了,但是沒有強制$digest迴圈,監視foo 和bar$watch沒有執行。也就是說如果我們自己執行一次$apply那麼這些$watch就會看見這些變化,然後根據需要更新DOM。

如果我們點選這個directive(藍色區域),我們看不到任何變化,但是我們點選按鈕時,點選數就更新了。如剛才說的,在這個directive上點選時我們不會觸發$digest迴圈,但是當按鈕被點選時,ng-click會呼叫$apply,然後就會執行$digest迴圈,於是所有的$watch都會被檢查,當然就包括我們的foobar$watch了。

現在你在想那並不是你想要的,你想要的是點選藍色區域的時候就更新點選數。很簡單,執行一下$apply就可以了:

element.bind('click',function(){
  scope.foo++;
  scope.bar++;

  scope.$apply();});

$apply是我們的$scope(或者是direcvie裡的link函式中的scope)的一個函式,呼叫它會強制一次$digest迴圈(除非當前正在執行迴圈,這種情況下會丟擲一個異常,這是我們不需要在那裡執行$apply的標誌)。

有用啦!但是有一種更好的使用$apply的方法:

element.bind('click',function(){
  scope.$apply(function(){
      scope.foo++;
      scope.bar++;});})

有什麼不一樣的?差別就是在第一個版本中,我們是在angular context的外面更新的資料,如果有發生錯誤,Angular永遠不知道。很明顯在這個像個小玩具的例子裡面不會出什麼大錯,但是想象一下我們如果有個alert框顯示錯誤給使用者,然後我們有個第三方的庫進行一個網路呼叫然後失敗了,如果我們不把它封裝進$apply裡面,Angular永遠不會知道失敗了,alert框就永遠不會彈出來了。

因此,如果你想使用一個jQuery外掛,並且要執行$digest迴圈來更新你的DOM的話,要確保你呼叫了$apply

有時候我想多說一句的是有些人在不得不呼叫$apply時會“感覺不妙”,因為他們會覺得他們做錯了什麼。其實不是這樣的,Angular不是什麼魔術師,他也不知道第三方庫想要更新繫結的資料。

使用$watch來監視你自己的東西

你已經知道了我們設定的任何繫結都有一個它自己的$watch,當需要時更新DOM,但是我們如果要自定義自己的watches呢?簡單

來看個例子:

app.js

app.controller('MainCtrl',function($scope){
  $scope.name ="Angular";

  $scope.updated =-1;

  $scope.$watch('name',function(){
    $scope.updated++;});});

index.html

<bodyng-controller="MainCtrl"><inputng-model="name"/>
  Name updated: {{updated}} times.
</body>

這就是我們創造一個新的$watch的方法。第一個引數是一個字串或者函式,在這裡是只是一個字串,就是我們要監視的變數的名字,在這裡,$scope.name(注意我們只需要用name)。第二個引數是當$watch說我監視的表示式發生變化後要執行的。我們要知道的第一件事就是當controller執行到這個$watch時,它會立即執行一次,因此我們設定updated為-1。

例子2:

app.js

app.controller('MainCtrl',function($scope){
  $scope.name ="Angular";

  $scope.updated =0;

  $scope.$watch('name',function(newValue, oldValue){if(newValue === oldValue){return;}// AKA first run
    $scope.updated++;});});

index.html

<bodyng-controller="MainCtrl"><inputng-model="name"/>
  Name updated: {{updated}} times.
</body>

watch的第二個引數接受兩個引數,新值和舊值。我們可以用他們來略過第一次的執行。通常你不需要略過第一次執行,但在這個例子裡面你是需要的。靈活點嘛少年。

例子3:

app.js

app.controller('MainCtrl',function($scope){
  $scope.user ={ name:"Fox"};

  $scope.updated =0;

  $scope.$watch('user',function(newValue, oldValue){if(newValue === oldValue){return;}
    $scope.updated++;});});

index.html

<bodyng-controller="MainCtrl"><inputng-model="user.name"/>
  Name updated: {{updated}} times.
</body>

我們想要監視$scope.user物件裡的任何變化,和以前一樣這裡只是用一個物件來代替前面的字串。

呃?沒用,為啥?因為$watch預設是比較兩個物件所引用的是否相同,在例子1和2裡面,每次更改$scope.name都會建立一個新的基本變數,因此$watch會執行,因為對這個變數的引用已經改變了。在上面的例子裡,我們在監視$scope.user,當我們改變$scope.user.name時,對$scope.user的引用是不會改變的,我們只是每次建立了一個新的$scope.user.name,但是$scope.user永遠是一樣的。

例子4:

app.js

app.controller('MainCtrl',function($scope){
  $scope.user ={ name:"Fox"};

  $scope.updated =0;

  $scope.$watch('user',function(newValue, oldValue){if(newValue === oldValue){return;}
    $scope.updated++;},true);});

index.html

<bodyng-controller="MainCtrl"><inputng-model="user.name"/>
  Name updated: {{updated}} times.
</body>

現在有用了吧!因為我們對$watch加入了第三個引數,它是一個bool型別的引數,表示的是我們比較的是物件的值而不是引用。由於當我們更新$scope.user.name$scope.user也會改變,所以能夠正確觸發。

關於$watch還有很多tips&tricks,但是這些都是基礎。

總結

好吧,我希望你們已經學會了在Angular中資料繫結是如何工作的。我猜想你的第一印象是dirty-checking很慢,好吧,其實是不對的。它像閃電般快。但是,是的,如果你在一個模版裡有2000-3000個watch,它會開始變慢。但是我覺得如果你達到這個數量級,就可以找個使用者體驗專家諮詢一下了

無論如何,隨著ECMAScript6的到來,在Angular未來的版本里我們將會有Object.observe那樣會極大改善$digest迴圈的速度。同時未來的文章也會涉及一些tips&tricks。

另一方面,這個主題並不容易,如果你發現我落下了什麼重要的東西或者有什麼東西完全錯了,請指正(原文是在GITHUB上PR 或報告issue)

相關推薦

AngularJs資料原理

此文為原創翻譯,轉載請註明出處! 注 這篇博文主要是寫給新手的,是給那些剛剛開始接觸Angular,並且想了解資料幫定是如何工作的人。如果你已經對Angular比較瞭解了,那強烈建議你直接去閱讀原始碼。 Angular使用者都想知道資料繫結是怎麼實現的。你可能會看到各種各樣的詞彙:$watch,$app

再談angularJS資料機制及背後原理angularJS常見問題總結

這篇是對angularJS的一些疑點回顧,是對目前angularJS開發的各種常見問題的整理彙總。如果對文中的題目全部瞭然於胸,覺得對整個angular框架應該掌握的七七八八了。希望志同道合的通知補充內容Angular 的資料繫結採用什麼機制,詳述原理?髒檢查機制。闡釋髒檢查

angularjs雙向資料原理

angular並不存在定時髒檢測。angular對常用的dom事件,xhr事件等做了封裝, 在裡面觸發進入angular的digest流程。在digest流程裡面, 會從rootscope開始遍歷,

angularjs雙向資料原理解析

angularjs的雙向資料繫結 髒值(發生了變化的值)檢查不等於定時輪詢,而是特定事件觸發才會執行 只有指定事件觸發後才會進入髒值輪詢。 - DOM事件,譬如使用者輸入文字,點選按鈕等。(ng-click) - XHR(ajax)響應事件 (http)

vue中實現雙向資料原理,使用了Object.defineproperty()方法,方法簡單

在vue中雙向資料繫結原理,我們一般都是用v-model來實現的 ,但一般在面試話會問到其實現的原理, 方法比較簡單,就是利用了es5中的一個方法.Object.defineproperty(),它有三個引數, Object.defineproperty(obj,'val',attrObject), 引數

Angular雙向資料原理

Angular是通過髒檢測來進行雙向資料繫結 Angular比不是通過定時去進行檢測 Angular在$digest cycle流程裡面,會從rootscope開始遍歷,檢查所有的watcher。

vue雙向資料原理

Vue應用的是mvvm框架,view和model分離,然後通過vm雙向資料繫結,` <code class="hljs handlebars has-numbering" style="display: block; padding: 0px; color: inh

Vue.js雙向資料原理

vue雙向繫結就是指model層與view層的同步,兩者之間任意一個發生變化都會同步更新到另一者。 View為檢視層,Model為資料層,ViewModel為邏輯控制層。 vue.js採用資料劫持結合釋出者-訂閱者模式的方法,通過Object.defin

Vue-資料原理

VueJS 使用 ES5 提供的 Object.defineProperty() 方法實現資料繫結。 感覺實現時主要是在defineProperty的set和get上做了很多文章,在get中確定了data和view的依賴關係,這樣在data改呼叫set時就可以根據依賴修改view。 Object.define

AngularJS 資料,雙向(ng-model)

資料雙向繫結:檢視上的資料通過表單元素繫結到Model模型($scope)上。 (使用者只能通過表單元素輸入資料)demo.html:<!DOCTYPE html> <html lang="en"> <head> <meta cha

iOS混合開發庫(GICXMLLayout)六、資料原理

各位對於MVVM這種架構應該多多少少有一定的瞭解了,而提到MVVM那麼資料繫結應該是繞不過去的一個話題。資料繫結是MVVM架構中的一個重要組成部分,可以做到View跟ViewModel之間的解耦,真正的做到UI、邏輯的分離。 在iOS上要是實現MVVM,那麼一般使用RAC或者RXSwift來實現資料繫結的功

JS框架雙向資料原理及思考

本文章資訊點 雙向資料繫結原理(vue) 如何設計搭建自己的框架 程式碼暫略,詳見,github [地址]https://github.com/yyccmmkk/Bi-directionalDataBindingDemo 什麼是單向什麼是雙向? 單向指是資料從mod

vue 雙向資料原理

Vue的雙向資料繫結原理是什麼?vue.js是採用資料劫持結合釋出者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter, getter,在資料變動時釋出訊息給訂閱者,出發相應的監聽回撥。具體步驟:首先Vue會使用document

雙向資料原理

1. 釋出者-訂閱者模式(backbone.js) 一般通過sub, pub的方式實現資料和檢視的繫結監聽,更新資料方式通常做法是 vm.set(‘property’, value),雖然老套古板,這種方式的優點在於相容ie8以下版本。 2. 髒

Vue雙向資料原理解析

首先上原始碼,模擬vue的雙向資料繫結原理<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Two-way data-

深入vue原始碼,瞭解vue的雙向資料原理

大家都知道vue是一種MVVM開發模式,資料驅動檢視的前端框架,並且內部已經實現了雙向資料繫結,那麼雙向資料繫結是怎麼實現的呢? 先手動擼一個最最最簡單的雙向資料繫結 1 <div> 2 <input type="text" name="" id="te

雙向資料---AngularJS的基本原理學習

Angular JS (Angular.JS) 是一組用來開發Web頁面的框架、模板以及資料繫結和豐富UI元件。它支援整個開發程序,提供web應用的架構,無需進行手工DOM操作。 AngularJS很小,只有60K,相容主流瀏覽器,與 jQuery 配合良好。雙向資料繫結

angularjs的雙向原理實現

angularjs的雙向繫結用js程式碼來實現 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>雙向繫結的js實現</title>

轉 vue實現雙向資料原理及實現篇 vue的雙向原理及實現

轉自:canfoo#! vue的雙向繫結原理及實現 前言 先上個成果圖來吸引各位: 程式碼:                          &nb

vue.js和angular雙向資料的實現原理

一、vue雙向資料繫結 1、原理 資料劫持: vue.js 是採用資料劫持結合釋出者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥。 2、實現步驟 要實現mv